@quanta-intellect/vessel-browser 0.1.35 → 0.1.45
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/README.md +27 -2
- package/out/main/index.js +2178 -347
- package/out/preload/content-script.js +27 -2
- package/out/renderer/assets/{index-IYqP5OYH.js → index-BC3sJqLj.js} +170 -135
- package/out/renderer/index.html +1 -1
- package/package.json +8 -7
package/out/main/index.js
CHANGED
|
@@ -2067,8 +2067,8 @@ class TabManager {
|
|
|
2067
2067
|
window;
|
|
2068
2068
|
onStateChange;
|
|
2069
2069
|
highlightCaptureCallback = null;
|
|
2070
|
-
constructor(
|
|
2071
|
-
this.window =
|
|
2070
|
+
constructor(window2, onStateChange) {
|
|
2071
|
+
this.window = window2;
|
|
2072
2072
|
this.onStateChange = onStateChange;
|
|
2073
2073
|
}
|
|
2074
2074
|
createTab(url = "about:blank", options) {
|
|
@@ -5126,215 +5126,6 @@ function makeImageResult(base64, description, mediaType = "image/png") {
|
|
|
5126
5126
|
};
|
|
5127
5127
|
return JSON.stringify(result);
|
|
5128
5128
|
}
|
|
5129
|
-
class AnthropicProvider {
|
|
5130
|
-
agentToolProfile = "default";
|
|
5131
|
-
client;
|
|
5132
|
-
model;
|
|
5133
|
-
abortController = null;
|
|
5134
|
-
constructor(apiKey, model) {
|
|
5135
|
-
this.client = new Anthropic({ apiKey });
|
|
5136
|
-
this.model = model || "claude-sonnet-4-20250514";
|
|
5137
|
-
}
|
|
5138
|
-
async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
|
|
5139
|
-
this.abortController = new AbortController();
|
|
5140
|
-
const messages = [
|
|
5141
|
-
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
5142
|
-
{ role: "user", content: userMessage }
|
|
5143
|
-
];
|
|
5144
|
-
try {
|
|
5145
|
-
const stream = this.client.messages.stream(
|
|
5146
|
-
{
|
|
5147
|
-
model: this.model,
|
|
5148
|
-
max_tokens: 4096,
|
|
5149
|
-
system: systemPrompt,
|
|
5150
|
-
messages
|
|
5151
|
-
},
|
|
5152
|
-
{ signal: this.abortController.signal }
|
|
5153
|
-
);
|
|
5154
|
-
for await (const event of stream) {
|
|
5155
|
-
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
5156
|
-
onChunk(event.delta.text);
|
|
5157
|
-
}
|
|
5158
|
-
}
|
|
5159
|
-
} catch (err) {
|
|
5160
|
-
if (err instanceof Error && err.name !== "AbortError") {
|
|
5161
|
-
onChunk(`
|
|
5162
|
-
|
|
5163
|
-
[Error: ${err.message}]`);
|
|
5164
|
-
}
|
|
5165
|
-
} finally {
|
|
5166
|
-
this.abortController = null;
|
|
5167
|
-
onEnd();
|
|
5168
|
-
}
|
|
5169
|
-
}
|
|
5170
|
-
async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
|
|
5171
|
-
this.abortController = new AbortController();
|
|
5172
|
-
const messages = [
|
|
5173
|
-
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
5174
|
-
{ role: "user", content: userMessage }
|
|
5175
|
-
];
|
|
5176
|
-
try {
|
|
5177
|
-
const maxIterations = getEffectiveMaxIterations();
|
|
5178
|
-
let iterationsUsed = 0;
|
|
5179
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
5180
|
-
iterationsUsed = i + 1;
|
|
5181
|
-
const stream = this.client.messages.stream(
|
|
5182
|
-
{
|
|
5183
|
-
model: this.model,
|
|
5184
|
-
max_tokens: 4096,
|
|
5185
|
-
system: systemPrompt,
|
|
5186
|
-
messages,
|
|
5187
|
-
tools
|
|
5188
|
-
},
|
|
5189
|
-
{ signal: this.abortController.signal }
|
|
5190
|
-
);
|
|
5191
|
-
let textContent = "";
|
|
5192
|
-
const toolUseBlocks = [];
|
|
5193
|
-
let currentToolUse = null;
|
|
5194
|
-
const STREAM_IDLE_TIMEOUT_MS = 3e4;
|
|
5195
|
-
let idleTimer = null;
|
|
5196
|
-
const resetIdleTimer = () => {
|
|
5197
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
5198
|
-
idleTimer = setTimeout(() => {
|
|
5199
|
-
this.abortController?.abort();
|
|
5200
|
-
}, STREAM_IDLE_TIMEOUT_MS);
|
|
5201
|
-
};
|
|
5202
|
-
resetIdleTimer();
|
|
5203
|
-
try {
|
|
5204
|
-
for await (const event of stream) {
|
|
5205
|
-
resetIdleTimer();
|
|
5206
|
-
if (event.type === "content_block_start") {
|
|
5207
|
-
if (event.content_block.type === "tool_use") {
|
|
5208
|
-
currentToolUse = {
|
|
5209
|
-
id: event.content_block.id,
|
|
5210
|
-
name: event.content_block.name,
|
|
5211
|
-
inputJson: ""
|
|
5212
|
-
};
|
|
5213
|
-
}
|
|
5214
|
-
} else if (event.type === "content_block_delta") {
|
|
5215
|
-
if (event.delta.type === "text_delta") {
|
|
5216
|
-
textContent += event.delta.text;
|
|
5217
|
-
onChunk(event.delta.text);
|
|
5218
|
-
} else if (event.delta.type === "input_json_delta" && currentToolUse) {
|
|
5219
|
-
currentToolUse.inputJson += event.delta.partial_json;
|
|
5220
|
-
}
|
|
5221
|
-
} else if (event.type === "content_block_stop" && currentToolUse) {
|
|
5222
|
-
try {
|
|
5223
|
-
toolUseBlocks.push({
|
|
5224
|
-
id: currentToolUse.id,
|
|
5225
|
-
name: currentToolUse.name,
|
|
5226
|
-
input: JSON.parse(currentToolUse.inputJson || "{}")
|
|
5227
|
-
});
|
|
5228
|
-
} catch {
|
|
5229
|
-
toolUseBlocks.push({
|
|
5230
|
-
id: currentToolUse.id,
|
|
5231
|
-
name: currentToolUse.name,
|
|
5232
|
-
input: {},
|
|
5233
|
-
_malformedArgs: currentToolUse.inputJson
|
|
5234
|
-
});
|
|
5235
|
-
}
|
|
5236
|
-
currentToolUse = null;
|
|
5237
|
-
}
|
|
5238
|
-
}
|
|
5239
|
-
} finally {
|
|
5240
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
5241
|
-
}
|
|
5242
|
-
const finalMessage = await stream.finalMessage();
|
|
5243
|
-
const assistantContent = [];
|
|
5244
|
-
if (textContent) {
|
|
5245
|
-
assistantContent.push({ type: "text", text: textContent });
|
|
5246
|
-
}
|
|
5247
|
-
for (const tb of toolUseBlocks) {
|
|
5248
|
-
assistantContent.push({
|
|
5249
|
-
type: "tool_use",
|
|
5250
|
-
id: tb.id,
|
|
5251
|
-
name: tb.name,
|
|
5252
|
-
input: tb.input
|
|
5253
|
-
});
|
|
5254
|
-
}
|
|
5255
|
-
messages.push({ role: "assistant", content: assistantContent });
|
|
5256
|
-
if (toolUseBlocks.length === 0) {
|
|
5257
|
-
break;
|
|
5258
|
-
}
|
|
5259
|
-
const toolResults = [];
|
|
5260
|
-
for (const tb of toolUseBlocks) {
|
|
5261
|
-
if (tb._malformedArgs !== void 0) {
|
|
5262
|
-
onChunk(`
|
|
5263
|
-
<<tool:${tb.name}:⚠ invalid args>>
|
|
5264
|
-
`);
|
|
5265
|
-
toolResults.push({
|
|
5266
|
-
type: "tool_result",
|
|
5267
|
-
tool_use_id: tb.id,
|
|
5268
|
-
content: `Error: Invalid JSON in tool arguments — could not parse. Please retry with valid JSON. Raw input: ${tb._malformedArgs.slice(0, 200)}`,
|
|
5269
|
-
is_error: true
|
|
5270
|
-
});
|
|
5271
|
-
continue;
|
|
5272
|
-
}
|
|
5273
|
-
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
5274
|
-
onChunk(`
|
|
5275
|
-
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
5276
|
-
`);
|
|
5277
|
-
let result;
|
|
5278
|
-
try {
|
|
5279
|
-
result = await onToolCall(tb.name, tb.input);
|
|
5280
|
-
} catch (toolErr) {
|
|
5281
|
-
const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
5282
|
-
result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
|
|
5283
|
-
}
|
|
5284
|
-
let parsedRich = null;
|
|
5285
|
-
try {
|
|
5286
|
-
const parsed = JSON.parse(result);
|
|
5287
|
-
if (isRichToolResult(parsed)) parsedRich = parsed;
|
|
5288
|
-
} catch {
|
|
5289
|
-
}
|
|
5290
|
-
if (parsedRich) {
|
|
5291
|
-
toolResults.push({
|
|
5292
|
-
type: "tool_result",
|
|
5293
|
-
tool_use_id: tb.id,
|
|
5294
|
-
content: parsedRich.content.map((block) => {
|
|
5295
|
-
if (block.type === "image") {
|
|
5296
|
-
return {
|
|
5297
|
-
type: "image",
|
|
5298
|
-
source: {
|
|
5299
|
-
type: "base64",
|
|
5300
|
-
media_type: block.mediaType,
|
|
5301
|
-
data: block.base64
|
|
5302
|
-
}
|
|
5303
|
-
};
|
|
5304
|
-
}
|
|
5305
|
-
return { type: "text", text: block.text };
|
|
5306
|
-
})
|
|
5307
|
-
});
|
|
5308
|
-
} else {
|
|
5309
|
-
toolResults.push({
|
|
5310
|
-
type: "tool_result",
|
|
5311
|
-
tool_use_id: tb.id,
|
|
5312
|
-
content: result
|
|
5313
|
-
});
|
|
5314
|
-
}
|
|
5315
|
-
}
|
|
5316
|
-
messages.push({ role: "user", content: toolResults });
|
|
5317
|
-
}
|
|
5318
|
-
if (iterationsUsed >= maxIterations) {
|
|
5319
|
-
onChunk(`
|
|
5320
|
-
|
|
5321
|
-
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
5322
|
-
}
|
|
5323
|
-
} catch (err) {
|
|
5324
|
-
if (err instanceof Error && err.name !== "AbortError") {
|
|
5325
|
-
onChunk(`
|
|
5326
|
-
|
|
5327
|
-
[Error: ${err.message}]`);
|
|
5328
|
-
}
|
|
5329
|
-
} finally {
|
|
5330
|
-
this.abortController = null;
|
|
5331
|
-
onEnd();
|
|
5332
|
-
}
|
|
5333
|
-
}
|
|
5334
|
-
cancel() {
|
|
5335
|
-
this.abortController?.abort();
|
|
5336
|
-
}
|
|
5337
|
-
}
|
|
5338
5129
|
const PROVIDERS = {
|
|
5339
5130
|
anthropic: {
|
|
5340
5131
|
id: "anthropic",
|
|
@@ -5384,6 +5175,16 @@ const PROVIDERS = {
|
|
|
5384
5175
|
apiKeyPlaceholder: "",
|
|
5385
5176
|
apiKeyHint: "No API key needed — runs locally via Ollama"
|
|
5386
5177
|
},
|
|
5178
|
+
llama_cpp: {
|
|
5179
|
+
id: "llama_cpp",
|
|
5180
|
+
name: "llama.cpp (Local)",
|
|
5181
|
+
defaultModel: "",
|
|
5182
|
+
models: [],
|
|
5183
|
+
requiresApiKey: false,
|
|
5184
|
+
defaultBaseUrl: "http://localhost:8080/v1",
|
|
5185
|
+
apiKeyPlaceholder: "",
|
|
5186
|
+
apiKeyHint: "No API key needed — scans the active llama-server model on localhost:8080"
|
|
5187
|
+
},
|
|
5387
5188
|
mistral: {
|
|
5388
5189
|
id: "mistral",
|
|
5389
5190
|
name: "Mistral AI",
|
|
@@ -5465,7 +5266,7 @@ function isLoopbackBaseUrl(baseUrl) {
|
|
|
5465
5266
|
}
|
|
5466
5267
|
function resolveAgentToolProfile(config) {
|
|
5467
5268
|
const providerId = config.id;
|
|
5468
|
-
const isLocalProvider = providerId === "ollama" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
|
|
5269
|
+
const isLocalProvider = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
|
|
5469
5270
|
if (!isLocalProvider) return "default";
|
|
5470
5271
|
const sizeInBillions = parseModelSizeInBillions(config.model);
|
|
5471
5272
|
if (sizeInBillions === null) {
|
|
@@ -5473,6 +5274,8 @@ function resolveAgentToolProfile(config) {
|
|
|
5473
5274
|
}
|
|
5474
5275
|
return sizeInBillions <= 14 ? "compact" : "default";
|
|
5475
5276
|
}
|
|
5277
|
+
const LLAMA_CPP_MIN_CTX_TOKENS$1 = 16384;
|
|
5278
|
+
const LLAMA_CPP_RECOMMENDED_CTX_TOKENS$1 = 32768;
|
|
5476
5279
|
function shouldDebugAgentLoop() {
|
|
5477
5280
|
const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
|
|
5478
5281
|
return value === "1" || value === "true";
|
|
@@ -5503,8 +5306,8 @@ function followUpReminderForProfile(profile, userMessage, assistantText, latestT
|
|
|
5503
5306
|
const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
|
|
5504
5307
|
const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
|
|
5505
5308
|
return {
|
|
5506
|
-
role: "
|
|
5507
|
-
content: `Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
|
|
5309
|
+
role: "user",
|
|
5310
|
+
content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
|
|
5508
5311
|
Do not ask the user what they want next unless the request is genuinely ambiguous or blocked. After navigation or page reads, keep executing the same task.` + (stateReminder ? `
|
|
5509
5312
|
${stateReminder}` : "") + (phaseReminder ? `
|
|
5510
5313
|
${phaseReminder}` : "")
|
|
@@ -5545,6 +5348,9 @@ function buildPhaseReminder(userMessage, assistantText) {
|
|
|
5545
5348
|
if (!goal || !text) return "";
|
|
5546
5349
|
const wantsCart = /\b(cart|bag|basket|checkout)\b/.test(goal);
|
|
5547
5350
|
const wantsExplanation = /\b(explain|reason|why)\b/.test(goal);
|
|
5351
|
+
const wantsBookRecommendations = /\b(book|books|recommend|recommended|interesting|novel|fiction|nonfiction)\b/.test(
|
|
5352
|
+
goal
|
|
5353
|
+
);
|
|
5548
5354
|
const hasFiveItemList = /(?:^|\n)\s*1\./.test(assistantText) && /(?:^|\n)\s*2\./.test(assistantText) && /(?:^|\n)\s*3\./.test(assistantText) && /(?:^|\n)\s*4\./.test(assistantText) && /(?:^|\n)\s*5\./.test(assistantText);
|
|
5549
5355
|
const selectedItems = hasFiveItemList || /i(?:'| a)?ve chosen/.test(text) || /i have chosen/.test(text) || /i selected/.test(text) || /here are the books/i.test(assistantText) || /here are the items/i.test(assistantText);
|
|
5550
5356
|
const intendsCart = /next[, ]+i will add/.test(text) || /i(?:'| a)?ll start with the first/.test(text) || /proceed systematically/.test(text) || /add (these|the chosen|the selected).*(cart|bag|basket)/.test(text);
|
|
@@ -5552,8 +5358,59 @@ function buildPhaseReminder(userMessage, assistantText) {
|
|
|
5552
5358
|
text
|
|
5553
5359
|
);
|
|
5554
5360
|
const explanationDone = /here is why i chose/.test(text) || /here are my reasons/.test(text) || /reason:/.test(text) || /reasons:/.test(text) || /why i chose/.test(text);
|
|
5361
|
+
const listingLoopSignals = /page contains a list of books|book listings|book cards|visible book|load more results|scroll further|scroll down|inspect the visible|focus on the book listings|targeting the book images|limited to interactive elements|identify the book cards|click one of the visible book/.test(
|
|
5362
|
+
text
|
|
5363
|
+
);
|
|
5364
|
+
const missedResultsSignals = /visible_only mode did not return specific book titles|did not yield a book title link|did not yield specific book titles|navigation links rather than book titles|inspect elements did not yield|inspect the page to find a specific book title|inspect the page to locate a book title|book title link from the search results/.test(
|
|
5365
|
+
text
|
|
5366
|
+
);
|
|
5367
|
+
const falseCartSuccessSignals = /added\s+to\s+the\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+to\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+["“”'a-z0-9 ,:&-]+\s+to the cart|added\s+["“”'a-z0-9 ,:&-]+\s+to cart|added\s+.*\s+by\s+.*\s+to the cart/.test(
|
|
5368
|
+
text
|
|
5369
|
+
) && !/(cart confirmation|view cart|shopping cart|checkout|continue shopping)/.test(
|
|
5370
|
+
text
|
|
5371
|
+
);
|
|
5372
|
+
const skippedSingleResultSignals = /did not yield a direct match|no direct match|no matches|unavailable on powell|out of stock or unavailable/.test(
|
|
5373
|
+
text
|
|
5374
|
+
) && /proceed to (?:add|search for) the next book|move on to the next book|next book from my list/.test(
|
|
5375
|
+
text
|
|
5376
|
+
);
|
|
5377
|
+
const selectedItemsRestartSignals = /navigate back to the search results page|search for ".*" directly in the search box|search for .* directly|page structure has shifted|refresh the page|restart search/.test(
|
|
5378
|
+
text
|
|
5379
|
+
);
|
|
5380
|
+
const multiClickSelectionSignals = /i(?:'| a)?ll start by clicking on the following books|i will start by clicking on the following books|i will click on the following books|clicked on five different book titles|clicked on \d+ different book titles|clicking through the selected titles|click each of the selected titles/.test(
|
|
5381
|
+
text
|
|
5382
|
+
);
|
|
5383
|
+
const staleSelectionSignals = /cannot locate the elements to click|page structure is not being reliably captured|specific titles failed|page may have changed|stale-index|no visible area|not visible/.test(
|
|
5384
|
+
text
|
|
5385
|
+
);
|
|
5386
|
+
const intermediateCartDialogSignals = /(added to cart|has been added to the cart|cart confirmation)/.test(text) && /(continue shopping|search results page|return to the search results page|back button|go back)/.test(
|
|
5387
|
+
text
|
|
5388
|
+
) && !/(all requested books are now in the cart|all 5 books are now in the cart|5 of 5 requested books are now in the cart)/.test(
|
|
5389
|
+
text
|
|
5390
|
+
);
|
|
5391
|
+
if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && listingLoopSignals) {
|
|
5392
|
+
return `Progress reminder: If product results or primary results are already visible, do not keep rereading or rescrolling the same listing page. Open one promising result now. On the detail page, add that item to the cart before returning for the next unseen result.`;
|
|
5393
|
+
}
|
|
5394
|
+
if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && missedResultsSignals) {
|
|
5395
|
+
return `Progress reminder: On a results page, do not use visible_only or generic inspect_element to hunt product results. Call read_page(mode="results_only") once. If Primary Results are shown, click a listed result directly.`;
|
|
5396
|
+
}
|
|
5397
|
+
if (wantsCart && falseCartSuccessSignals && !selectedItems && !explanationDone) {
|
|
5398
|
+
return `Progress reminder: Do not assume an item was added just because its product page is open or you inspected it. Only treat the cart step as complete after a successful Add to Cart click followed by cart confirmation, View Cart, Continue Shopping, or the cart page itself.`;
|
|
5399
|
+
}
|
|
5400
|
+
if (wantsCart && skippedSingleResultSignals && !selectedItems) {
|
|
5401
|
+
return `Progress reminder: Do not skip to a new query just because the match is not exact. If the results page shows even one plausible product result, inspect or click that result before concluding there is no match.`;
|
|
5402
|
+
}
|
|
5403
|
+
if (wantsCart && intermediateCartDialogSignals && !explanationDone) {
|
|
5404
|
+
return `Progress reminder: After an Add to Cart success, prefer the cart-confirmation dialog action Continue Shopping while more items remain. Do not click View Cart or Go to Basket yet, and do not use the browser back button while the dialog is still open.`;
|
|
5405
|
+
}
|
|
5406
|
+
if (wantsCart && selectedItems && !cartDone && selectedItemsRestartSignals) {
|
|
5407
|
+
return `Progress reminder: The chosen items are already decided. Do not restart search, refresh the results page, or navigate back to browse again unless a specific saved link fails. Use the current results page or the chosen result links you already have: open one chosen result, add it to the cart, confirm success, then continue to the next chosen result.`;
|
|
5408
|
+
}
|
|
5409
|
+
if (wantsCart && wantsBookRecommendations && !cartDone && (multiClickSelectionSignals || staleSelectionSignals)) {
|
|
5410
|
+
return `Progress reminder: Do not batch-click multiple results from a listing or category page. Open exactly one visible result, finish that item's Add to Cart flow, confirm success, then use Continue Shopping or go back once to choose the next unseen result. If a remembered label or index fails, trust the latest page state and refresh it with one read_page call before continuing.`;
|
|
5411
|
+
}
|
|
5555
5412
|
if (wantsCart && selectedItems && (intendsCart || !cartDone)) {
|
|
5556
|
-
return `Progress reminder: You already selected the requested items. Do not restart browsing or searching unless a specific cart step fails. Continue adding the selected items to the cart one by one.`;
|
|
5413
|
+
return `Progress reminder: You already selected the requested items. Do not restart browsing or searching unless a specific cart step fails. Continue adding the selected items to the cart one by one. Use the chosen result links you already have, add one selected item to the cart, confirm success, then continue to the next one. Do not click multiple chosen results in a row from the same listing page.`;
|
|
5557
5414
|
}
|
|
5558
5415
|
if (wantsCart && wantsExplanation && cartDone && !explanationDone) {
|
|
5559
5416
|
return `Progress reminder: The cart step appears complete. Do not resume browsing. Finish by explaining why the chosen items were recommended.`;
|
|
@@ -5592,6 +5449,30 @@ function shouldRecoverCompactStall(text, userMessage) {
|
|
|
5592
5449
|
if (userMessage && buildPhaseReminder(userMessage, text)) {
|
|
5593
5450
|
return true;
|
|
5594
5451
|
}
|
|
5452
|
+
const repetitivePlanningSignals = [
|
|
5453
|
+
"next step:",
|
|
5454
|
+
"i will now inspect",
|
|
5455
|
+
"i will now read",
|
|
5456
|
+
"i will now click",
|
|
5457
|
+
"i'll use readpage",
|
|
5458
|
+
"i'll use read_page",
|
|
5459
|
+
"i'll start by clicking",
|
|
5460
|
+
"i have clicked on five different book titles",
|
|
5461
|
+
"clicked on five different book titles",
|
|
5462
|
+
"i'll begin with",
|
|
5463
|
+
"if the selection is unclear"
|
|
5464
|
+
];
|
|
5465
|
+
if (repetitivePlanningSignals.some((pattern) => trimmed.includes(pattern))) {
|
|
5466
|
+
return true;
|
|
5467
|
+
}
|
|
5468
|
+
const falseCartSuccessWithoutConfirmation = /added\s+to\s+the\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+to\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+["“”'a-z0-9 ,:&-]+\s+to the cart|added\s+["“”'a-z0-9 ,:&-]+\s+to cart|added\s+.*\s+by\s+.*\s+to the cart/.test(
|
|
5469
|
+
trimmed
|
|
5470
|
+
) && !/(cart confirmation|view cart|continue shopping|shopping cart|checkout|why i chose|here is why i chose|here are my reasons)/.test(
|
|
5471
|
+
trimmed
|
|
5472
|
+
);
|
|
5473
|
+
if (falseCartSuccessWithoutConfirmation) {
|
|
5474
|
+
return true;
|
|
5475
|
+
}
|
|
5595
5476
|
const completionSignals = [
|
|
5596
5477
|
"i found",
|
|
5597
5478
|
"i chose",
|
|
@@ -5645,6 +5526,17 @@ function stableToolSignature(name, args) {
|
|
|
5645
5526
|
function hasRecentDuplicateToolCall(recentToolSignatures, signature) {
|
|
5646
5527
|
return recentToolSignatures.includes(signature);
|
|
5647
5528
|
}
|
|
5529
|
+
function isClickReadLoop(names) {
|
|
5530
|
+
if (names.length < 6) return false;
|
|
5531
|
+
const tail = names.slice(-6);
|
|
5532
|
+
let clickReadPairs = 0;
|
|
5533
|
+
for (let i = 0; i < tail.length - 1; i++) {
|
|
5534
|
+
if (tail[i] === "click" && tail[i + 1] === "read_page") {
|
|
5535
|
+
clickReadPairs++;
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
return clickReadPairs >= 2;
|
|
5539
|
+
}
|
|
5648
5540
|
function normalizeToolToken(value) {
|
|
5649
5541
|
return value.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
|
|
5650
5542
|
}
|
|
@@ -5663,11 +5555,91 @@ function canonicalizeUrlLike(value) {
|
|
|
5663
5555
|
}
|
|
5664
5556
|
return value.trim();
|
|
5665
5557
|
}
|
|
5666
|
-
function
|
|
5667
|
-
const
|
|
5668
|
-
if (
|
|
5669
|
-
|
|
5670
|
-
|
|
5558
|
+
function toLikelyUrl(value) {
|
|
5559
|
+
const trimmed = value.trim().replace(/^["']|["']$/g, "");
|
|
5560
|
+
if (!trimmed) return null;
|
|
5561
|
+
if (/^https?:\/\//i.test(trimmed)) return trimmed;
|
|
5562
|
+
if (/^[a-z0-9-]+\.(com|org|net|io|dev|app|ai|co|edu|gov)(\/\S*)?$/i.test(trimmed)) {
|
|
5563
|
+
return `https://${trimmed}`;
|
|
5564
|
+
}
|
|
5565
|
+
return null;
|
|
5566
|
+
}
|
|
5567
|
+
function scalarArgsForTool(name, scalar) {
|
|
5568
|
+
const trimmed = scalar.trim();
|
|
5569
|
+
if (!trimmed) return null;
|
|
5570
|
+
if (name === "navigate") {
|
|
5571
|
+
const url = toLikelyUrl(trimmed);
|
|
5572
|
+
return url ? { url } : null;
|
|
5573
|
+
}
|
|
5574
|
+
if (name === "search") {
|
|
5575
|
+
return { query: trimmed.replace(/^["']|["']$/g, "") };
|
|
5576
|
+
}
|
|
5577
|
+
if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
|
|
5578
|
+
return { text: trimmed.replace(/^["']|["']$/g, "") };
|
|
5579
|
+
}
|
|
5580
|
+
if (name === "read_page") {
|
|
5581
|
+
const mode = trimmed.replace(/^["']|["']$/g, "").toLowerCase();
|
|
5582
|
+
if (mode) return { mode };
|
|
5583
|
+
}
|
|
5584
|
+
return null;
|
|
5585
|
+
}
|
|
5586
|
+
function tryParseJsonWithCommonRepairs(raw) {
|
|
5587
|
+
const trimmed = raw.trim();
|
|
5588
|
+
if (!trimmed) return {};
|
|
5589
|
+
const candidates = /* @__PURE__ */ new Set([trimmed]);
|
|
5590
|
+
const objectMatch = trimmed.match(/\{[\s\S]*\}/);
|
|
5591
|
+
if (objectMatch?.[0]) candidates.add(objectMatch[0]);
|
|
5592
|
+
if (!trimmed.startsWith("{") && trimmed.includes(":")) {
|
|
5593
|
+
candidates.add(`{${trimmed}}`);
|
|
5594
|
+
}
|
|
5595
|
+
for (const candidate of candidates) {
|
|
5596
|
+
const normalized = candidate.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
5597
|
+
if (!normalized) continue;
|
|
5598
|
+
const repaired = normalized.replace(/([{,]\s*)([A-Za-z_][A-Za-z0-9_-]*)(\s*:)/g, '$1"$2"$3').replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3').replace(
|
|
5599
|
+
/:\s*'([^'\\]*(?:\\.[^'\\]*)*)'/g,
|
|
5600
|
+
(_match, value) => `: ${JSON.stringify(value)}`
|
|
5601
|
+
).replace(/,\s*([}\]])/g, "$1");
|
|
5602
|
+
try {
|
|
5603
|
+
return JSON.parse(repaired);
|
|
5604
|
+
} catch {
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
throw new Error("invalid-json");
|
|
5608
|
+
}
|
|
5609
|
+
function parseToolArgsWithRepair(name, argsJson) {
|
|
5610
|
+
const trimmed = (argsJson || "").trim();
|
|
5611
|
+
if (!trimmed) return { args: {}, repaired: false };
|
|
5612
|
+
try {
|
|
5613
|
+
const parsed = JSON.parse(trimmed);
|
|
5614
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
5615
|
+
return { args: parsed, repaired: false };
|
|
5616
|
+
}
|
|
5617
|
+
if (typeof parsed === "string") {
|
|
5618
|
+
const scalarArgs2 = scalarArgsForTool(name, parsed);
|
|
5619
|
+
return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
|
|
5620
|
+
}
|
|
5621
|
+
return null;
|
|
5622
|
+
} catch {
|
|
5623
|
+
}
|
|
5624
|
+
try {
|
|
5625
|
+
const repaired = tryParseJsonWithCommonRepairs(trimmed);
|
|
5626
|
+
if (repaired && typeof repaired === "object" && !Array.isArray(repaired)) {
|
|
5627
|
+
return { args: repaired, repaired: true };
|
|
5628
|
+
}
|
|
5629
|
+
if (typeof repaired === "string") {
|
|
5630
|
+
const scalarArgs2 = scalarArgsForTool(name, repaired);
|
|
5631
|
+
return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
|
|
5632
|
+
}
|
|
5633
|
+
} catch {
|
|
5634
|
+
}
|
|
5635
|
+
const scalarArgs = scalarArgsForTool(name, trimmed);
|
|
5636
|
+
return scalarArgs ? { args: scalarArgs, repaired: true } : null;
|
|
5637
|
+
}
|
|
5638
|
+
function coerceToolArgsForExecution(name, args) {
|
|
5639
|
+
const coerced = { ...args };
|
|
5640
|
+
if (name === "search") {
|
|
5641
|
+
if (typeof coerced.query !== "string" || !coerced.query.trim()) {
|
|
5642
|
+
if (typeof coerced.text === "string" && coerced.text.trim()) {
|
|
5671
5643
|
coerced.query = coerced.text.trim();
|
|
5672
5644
|
} else if (typeof coerced.term === "string" && coerced.term.trim()) {
|
|
5673
5645
|
coerced.query = coerced.term.trim();
|
|
@@ -5813,12 +5785,49 @@ function recoverNarratedActionToolCalls(text, availableToolNames) {
|
|
|
5813
5785
|
});
|
|
5814
5786
|
}
|
|
5815
5787
|
}
|
|
5788
|
+
const inlineReadMatch = trimmed.match(
|
|
5789
|
+
/\bread_?page\b\s*\(\s*mode\s*=\s*["']?([a-z_]+)["']?\s*\)/i
|
|
5790
|
+
) ?? trimmed.match(
|
|
5791
|
+
/\breadpage\b\s*\(\s*mode\s*=\s*["']?([a-z_]+)["']?\s*\)/i
|
|
5792
|
+
);
|
|
5793
|
+
if (inlineReadMatch && availableToolNames.has("read_page")) {
|
|
5794
|
+
const rawMode = (inlineReadMatch[1] || "").trim().toLowerCase();
|
|
5795
|
+
const normalizedMode = rawMode === "visibleonly" ? "visible_only" : rawMode === "resultsonly" ? "results_only" : rawMode;
|
|
5796
|
+
if (normalizedMode) {
|
|
5797
|
+
recovered.push({
|
|
5798
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5799
|
+
name: "read_page",
|
|
5800
|
+
argsJson: JSON.stringify({ mode: normalizedMode })
|
|
5801
|
+
});
|
|
5802
|
+
return recovered;
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
const inlineInspectMatch = trimmed.match(
|
|
5806
|
+
/\binspect_?element\b(?:\s+tool)?\b/i
|
|
5807
|
+
) ?? trimmed.match(/\binspectelement\b\b/i);
|
|
5808
|
+
if (inlineInspectMatch && availableToolNames.has("inspect_element")) {
|
|
5809
|
+
recovered.push({
|
|
5810
|
+
id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
5811
|
+
name: "inspect_element",
|
|
5812
|
+
argsJson: "{}"
|
|
5813
|
+
});
|
|
5814
|
+
return recovered;
|
|
5815
|
+
}
|
|
5816
5816
|
return recovered;
|
|
5817
5817
|
}
|
|
5818
|
+
function formatOpenAICompatErrorMessage(providerId, message) {
|
|
5819
|
+
if (providerId === "llama_cpp" && /(available context size|context size exceeded|exceeds the available context size|try increasing it)/i.test(
|
|
5820
|
+
message
|
|
5821
|
+
)) {
|
|
5822
|
+
return `${message} llama.cpp sets context size at server startup, not per request. Vessel's agent prompt plus tool schema is about 6.5k tokens before browsing history, so run llama-server with --ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS$1} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS$1} recommended).`;
|
|
5823
|
+
}
|
|
5824
|
+
return message;
|
|
5825
|
+
}
|
|
5818
5826
|
class OpenAICompatProvider {
|
|
5819
5827
|
agentToolProfile;
|
|
5820
5828
|
client;
|
|
5821
5829
|
model;
|
|
5830
|
+
providerId;
|
|
5822
5831
|
abortController = null;
|
|
5823
5832
|
constructor(config) {
|
|
5824
5833
|
const meta = PROVIDERS[config.id];
|
|
@@ -5827,6 +5836,7 @@ class OpenAICompatProvider {
|
|
|
5827
5836
|
apiKey: config.apiKey || "ollama",
|
|
5828
5837
|
baseURL
|
|
5829
5838
|
});
|
|
5839
|
+
this.providerId = config.id;
|
|
5830
5840
|
this.model = config.model || meta?.defaultModel || "gpt-4o";
|
|
5831
5841
|
this.agentToolProfile = resolveAgentToolProfile(config);
|
|
5832
5842
|
}
|
|
@@ -5841,22 +5851,31 @@ class OpenAICompatProvider {
|
|
|
5841
5851
|
const stream = await this.client.chat.completions.create(
|
|
5842
5852
|
{
|
|
5843
5853
|
model: this.model,
|
|
5854
|
+
max_tokens: 4096,
|
|
5844
5855
|
stream: true,
|
|
5845
5856
|
messages
|
|
5846
5857
|
},
|
|
5847
5858
|
{ signal: this.abortController.signal }
|
|
5848
5859
|
);
|
|
5849
5860
|
for await (const chunk of stream) {
|
|
5850
|
-
const
|
|
5851
|
-
if (
|
|
5852
|
-
|
|
5861
|
+
const choice = chunk.choices[0];
|
|
5862
|
+
if (!choice) continue;
|
|
5863
|
+
const delta = choice.delta;
|
|
5864
|
+
const reasoning = delta?.reasoning_content;
|
|
5865
|
+
if (typeof reasoning === "string" && reasoning.length > 0) {
|
|
5866
|
+
onChunk(reasoning);
|
|
5867
|
+
}
|
|
5868
|
+
if (delta.content) {
|
|
5869
|
+
onChunk(delta.content);
|
|
5853
5870
|
}
|
|
5854
5871
|
}
|
|
5855
5872
|
} catch (err) {
|
|
5856
5873
|
if (err instanceof Error && err.name !== "AbortError") {
|
|
5857
|
-
onChunk(
|
|
5874
|
+
onChunk(
|
|
5875
|
+
`
|
|
5858
5876
|
|
|
5859
|
-
[Error: ${err.message}]`
|
|
5877
|
+
[Error: ${formatOpenAICompatErrorMessage(this.providerId, err.message)}]`
|
|
5878
|
+
);
|
|
5860
5879
|
}
|
|
5861
5880
|
} finally {
|
|
5862
5881
|
this.abortController = null;
|
|
@@ -5878,6 +5897,8 @@ class OpenAICompatProvider {
|
|
|
5878
5897
|
let compactRecoveryCount = 0;
|
|
5879
5898
|
let compactCorrectionCount = 0;
|
|
5880
5899
|
const recentCompactToolSignatures = [];
|
|
5900
|
+
const recentToolNames = [];
|
|
5901
|
+
let clickReadLoopNudged = false;
|
|
5881
5902
|
for (let i = 0; i < maxIterations; i++) {
|
|
5882
5903
|
iterationsUsed = i + 1;
|
|
5883
5904
|
let textAccum = "";
|
|
@@ -5892,6 +5913,7 @@ class OpenAICompatProvider {
|
|
|
5892
5913
|
const stream = await this.client.chat.completions.create(
|
|
5893
5914
|
{
|
|
5894
5915
|
model: this.model,
|
|
5916
|
+
max_tokens: 4096,
|
|
5895
5917
|
stream: true,
|
|
5896
5918
|
messages,
|
|
5897
5919
|
tools: openAITools,
|
|
@@ -5905,6 +5927,10 @@ class OpenAICompatProvider {
|
|
|
5905
5927
|
if (!choice) continue;
|
|
5906
5928
|
const delta = choice.delta;
|
|
5907
5929
|
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
5930
|
+
const reasoning = delta?.reasoning_content;
|
|
5931
|
+
if (typeof reasoning === "string" && reasoning.length > 0) {
|
|
5932
|
+
onChunk(reasoning);
|
|
5933
|
+
}
|
|
5908
5934
|
if (delta.content) {
|
|
5909
5935
|
textAccum += delta.content;
|
|
5910
5936
|
onChunk(delta.content);
|
|
@@ -5925,10 +5951,12 @@ class OpenAICompatProvider {
|
|
|
5925
5951
|
for (const tc of Object.values(toolCallAccums)) {
|
|
5926
5952
|
if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5927
5953
|
let parsedArgs = {};
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5954
|
+
const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
|
|
5955
|
+
if (repairedArgs) {
|
|
5956
|
+
parsedArgs = repairedArgs.args;
|
|
5957
|
+
if (repairedArgs.repaired) {
|
|
5958
|
+
tc.argsJson = JSON.stringify(parsedArgs);
|
|
5959
|
+
}
|
|
5932
5960
|
}
|
|
5933
5961
|
tc.name = resolveToolCallName(tc.name, parsedArgs, availableToolNames);
|
|
5934
5962
|
}
|
|
@@ -5967,11 +5995,14 @@ class OpenAICompatProvider {
|
|
|
5967
5995
|
});
|
|
5968
5996
|
const malformedToolCalls = /* @__PURE__ */ new Set();
|
|
5969
5997
|
for (const tc of toolCalls) {
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
} catch {
|
|
5998
|
+
const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
|
|
5999
|
+
if (!repairedArgs) {
|
|
5973
6000
|
malformedToolCalls.add(tc.id);
|
|
5974
6001
|
tc.argsJson = "{}";
|
|
6002
|
+
continue;
|
|
6003
|
+
}
|
|
6004
|
+
if (repairedArgs.repaired) {
|
|
6005
|
+
tc.argsJson = JSON.stringify(repairedArgs.args);
|
|
5975
6006
|
}
|
|
5976
6007
|
}
|
|
5977
6008
|
const assistantMsg = {
|
|
@@ -5995,12 +6026,12 @@ class OpenAICompatProvider {
|
|
|
5995
6026
|
)) {
|
|
5996
6027
|
compactRecoveryCount += 1;
|
|
5997
6028
|
messages.push({
|
|
5998
|
-
role: "
|
|
5999
|
-
content: buildCompactRecoveryPrompt(
|
|
6029
|
+
role: "user",
|
|
6030
|
+
content: `[System] ${buildCompactRecoveryPrompt(
|
|
6000
6031
|
userMessage,
|
|
6001
6032
|
textAccum,
|
|
6002
6033
|
latestToolMessage ? String(latestToolMessage.content || "") : null
|
|
6003
|
-
)
|
|
6034
|
+
)}`
|
|
6004
6035
|
});
|
|
6005
6036
|
continue;
|
|
6006
6037
|
}
|
|
@@ -6021,9 +6052,8 @@ class OpenAICompatProvider {
|
|
|
6021
6052
|
continue;
|
|
6022
6053
|
}
|
|
6023
6054
|
let args = {};
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
} catch {
|
|
6055
|
+
const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
|
|
6056
|
+
if (!repairedArgs) {
|
|
6027
6057
|
onChunk(`
|
|
6028
6058
|
<<tool:${tc.name}:⚠ invalid args>>
|
|
6029
6059
|
`);
|
|
@@ -6034,6 +6064,7 @@ class OpenAICompatProvider {
|
|
|
6034
6064
|
});
|
|
6035
6065
|
continue;
|
|
6036
6066
|
}
|
|
6067
|
+
args = repairedArgs.args;
|
|
6037
6068
|
args = coerceToolArgsForExecution(tc.name, args);
|
|
6038
6069
|
if (!availableToolNames.has(tc.name)) {
|
|
6039
6070
|
onChunk(`
|
|
@@ -6047,14 +6078,23 @@ class OpenAICompatProvider {
|
|
|
6047
6078
|
compactCorrectionCount += 1;
|
|
6048
6079
|
if (compactCorrectionCount >= 2) {
|
|
6049
6080
|
messages.push({
|
|
6050
|
-
role: "
|
|
6051
|
-
content: `You are calling unsupported tools. Stop inventing tool names. Use the supported tools you were given and take the next concrete step.`
|
|
6081
|
+
role: "user",
|
|
6082
|
+
content: `[System] You are calling unsupported tools. Stop inventing tool names. Use the supported tools you were given and take the next concrete step.`
|
|
6052
6083
|
});
|
|
6053
6084
|
}
|
|
6054
6085
|
continue;
|
|
6055
6086
|
}
|
|
6056
6087
|
const toolSignature = stableToolSignature(tc.name, args);
|
|
6057
|
-
|
|
6088
|
+
const neverSuppressDuplicate = [
|
|
6089
|
+
"read_page",
|
|
6090
|
+
"current_tab",
|
|
6091
|
+
"inspect_element",
|
|
6092
|
+
"screenshot",
|
|
6093
|
+
"go_back",
|
|
6094
|
+
"go_forward",
|
|
6095
|
+
"click"
|
|
6096
|
+
].includes(tc.name);
|
|
6097
|
+
if (this.agentToolProfile === "compact" && !neverSuppressDuplicate && hasRecentDuplicateToolCall(
|
|
6058
6098
|
recentCompactToolSignatures,
|
|
6059
6099
|
toolSignature
|
|
6060
6100
|
)) {
|
|
@@ -6069,8 +6109,8 @@ class OpenAICompatProvider {
|
|
|
6069
6109
|
compactCorrectionCount += 1;
|
|
6070
6110
|
if (compactCorrectionCount >= 2) {
|
|
6071
6111
|
messages.push({
|
|
6072
|
-
role: "
|
|
6073
|
-
content: `You are stuck repeating the same action. Stop repeating navigate/search. Use a different supported tool that advances the task, such as click, read_page, or scroll.`
|
|
6112
|
+
role: "user",
|
|
6113
|
+
content: `[System] You are stuck repeating the same action. Stop repeating navigate/search. Use a different supported tool that advances the task, such as click, read_page, or scroll.`
|
|
6074
6114
|
});
|
|
6075
6115
|
}
|
|
6076
6116
|
continue;
|
|
@@ -6100,6 +6140,15 @@ class OpenAICompatProvider {
|
|
|
6100
6140
|
recentCompactToolSignatures.shift();
|
|
6101
6141
|
}
|
|
6102
6142
|
}
|
|
6143
|
+
recentToolNames.push(tc.name);
|
|
6144
|
+
if (recentToolNames.length > 8) recentToolNames.shift();
|
|
6145
|
+
if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
|
|
6146
|
+
clickReadLoopNudged = true;
|
|
6147
|
+
messages.push({
|
|
6148
|
+
role: "user",
|
|
6149
|
+
content: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
|
|
6150
|
+
});
|
|
6151
|
+
}
|
|
6103
6152
|
compactCorrectionCount = 0;
|
|
6104
6153
|
iterationToolResultPreviews.push(toolContent);
|
|
6105
6154
|
messages.push({
|
|
@@ -6121,6 +6170,228 @@ class OpenAICompatProvider {
|
|
|
6121
6170
|
if (iterationsUsed >= maxIterations) {
|
|
6122
6171
|
onChunk(`
|
|
6123
6172
|
|
|
6173
|
+
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
6174
|
+
}
|
|
6175
|
+
} catch (err) {
|
|
6176
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
6177
|
+
onChunk(
|
|
6178
|
+
`
|
|
6179
|
+
|
|
6180
|
+
[Error: ${formatOpenAICompatErrorMessage(this.providerId, err.message)}]`
|
|
6181
|
+
);
|
|
6182
|
+
}
|
|
6183
|
+
} finally {
|
|
6184
|
+
this.abortController = null;
|
|
6185
|
+
onEnd();
|
|
6186
|
+
}
|
|
6187
|
+
}
|
|
6188
|
+
cancel() {
|
|
6189
|
+
this.abortController?.abort();
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
class AnthropicProvider {
|
|
6193
|
+
agentToolProfile = "default";
|
|
6194
|
+
client;
|
|
6195
|
+
model;
|
|
6196
|
+
abortController = null;
|
|
6197
|
+
constructor(apiKey, model) {
|
|
6198
|
+
this.client = new Anthropic({ apiKey });
|
|
6199
|
+
this.model = model || "claude-sonnet-4-20250514";
|
|
6200
|
+
}
|
|
6201
|
+
async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
|
|
6202
|
+
this.abortController = new AbortController();
|
|
6203
|
+
const messages = [
|
|
6204
|
+
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
6205
|
+
{ role: "user", content: userMessage }
|
|
6206
|
+
];
|
|
6207
|
+
try {
|
|
6208
|
+
const stream = this.client.messages.stream(
|
|
6209
|
+
{
|
|
6210
|
+
model: this.model,
|
|
6211
|
+
max_tokens: 4096,
|
|
6212
|
+
system: systemPrompt,
|
|
6213
|
+
messages
|
|
6214
|
+
},
|
|
6215
|
+
{ signal: this.abortController.signal }
|
|
6216
|
+
);
|
|
6217
|
+
for await (const event of stream) {
|
|
6218
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
6219
|
+
onChunk(event.delta.text);
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
} catch (err) {
|
|
6223
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
6224
|
+
onChunk(`
|
|
6225
|
+
|
|
6226
|
+
[Error: ${err.message}]`);
|
|
6227
|
+
}
|
|
6228
|
+
} finally {
|
|
6229
|
+
this.abortController = null;
|
|
6230
|
+
onEnd();
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
|
|
6234
|
+
this.abortController = new AbortController();
|
|
6235
|
+
const messages = [
|
|
6236
|
+
...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
|
|
6237
|
+
{ role: "user", content: userMessage }
|
|
6238
|
+
];
|
|
6239
|
+
try {
|
|
6240
|
+
const maxIterations = getEffectiveMaxIterations();
|
|
6241
|
+
let iterationsUsed = 0;
|
|
6242
|
+
const recentToolNames = [];
|
|
6243
|
+
let clickReadLoopNudged = false;
|
|
6244
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
6245
|
+
iterationsUsed = i + 1;
|
|
6246
|
+
const stream = this.client.messages.stream(
|
|
6247
|
+
{
|
|
6248
|
+
model: this.model,
|
|
6249
|
+
max_tokens: 4096,
|
|
6250
|
+
system: systemPrompt,
|
|
6251
|
+
messages,
|
|
6252
|
+
tools
|
|
6253
|
+
},
|
|
6254
|
+
{ signal: this.abortController.signal }
|
|
6255
|
+
);
|
|
6256
|
+
let textContent = "";
|
|
6257
|
+
const toolUseBlocks = [];
|
|
6258
|
+
let currentToolUse = null;
|
|
6259
|
+
const STREAM_IDLE_TIMEOUT_MS = 3e4;
|
|
6260
|
+
let idleTimer = null;
|
|
6261
|
+
const resetIdleTimer = () => {
|
|
6262
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
6263
|
+
idleTimer = setTimeout(() => {
|
|
6264
|
+
this.abortController?.abort();
|
|
6265
|
+
}, STREAM_IDLE_TIMEOUT_MS);
|
|
6266
|
+
};
|
|
6267
|
+
resetIdleTimer();
|
|
6268
|
+
try {
|
|
6269
|
+
for await (const event of stream) {
|
|
6270
|
+
resetIdleTimer();
|
|
6271
|
+
if (event.type === "content_block_start") {
|
|
6272
|
+
if (event.content_block.type === "tool_use") {
|
|
6273
|
+
currentToolUse = {
|
|
6274
|
+
id: event.content_block.id,
|
|
6275
|
+
name: event.content_block.name,
|
|
6276
|
+
inputJson: ""
|
|
6277
|
+
};
|
|
6278
|
+
}
|
|
6279
|
+
} else if (event.type === "content_block_delta") {
|
|
6280
|
+
if (event.delta.type === "text_delta") {
|
|
6281
|
+
textContent += event.delta.text;
|
|
6282
|
+
onChunk(event.delta.text);
|
|
6283
|
+
} else if (event.delta.type === "input_json_delta" && currentToolUse) {
|
|
6284
|
+
currentToolUse.inputJson += event.delta.partial_json;
|
|
6285
|
+
}
|
|
6286
|
+
} else if (event.type === "content_block_stop" && currentToolUse) {
|
|
6287
|
+
try {
|
|
6288
|
+
toolUseBlocks.push({
|
|
6289
|
+
id: currentToolUse.id,
|
|
6290
|
+
name: currentToolUse.name,
|
|
6291
|
+
input: JSON.parse(currentToolUse.inputJson || "{}")
|
|
6292
|
+
});
|
|
6293
|
+
} catch {
|
|
6294
|
+
toolUseBlocks.push({
|
|
6295
|
+
id: currentToolUse.id,
|
|
6296
|
+
name: currentToolUse.name,
|
|
6297
|
+
input: {},
|
|
6298
|
+
_malformedArgs: currentToolUse.inputJson
|
|
6299
|
+
});
|
|
6300
|
+
}
|
|
6301
|
+
currentToolUse = null;
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
} finally {
|
|
6305
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
6306
|
+
}
|
|
6307
|
+
const finalMessage = await stream.finalMessage();
|
|
6308
|
+
const assistantContent = [];
|
|
6309
|
+
if (textContent) {
|
|
6310
|
+
assistantContent.push({ type: "text", text: textContent });
|
|
6311
|
+
}
|
|
6312
|
+
for (const tb of toolUseBlocks) {
|
|
6313
|
+
assistantContent.push({
|
|
6314
|
+
type: "tool_use",
|
|
6315
|
+
id: tb.id,
|
|
6316
|
+
name: tb.name,
|
|
6317
|
+
input: tb.input
|
|
6318
|
+
});
|
|
6319
|
+
}
|
|
6320
|
+
messages.push({ role: "assistant", content: assistantContent });
|
|
6321
|
+
if (toolUseBlocks.length === 0) {
|
|
6322
|
+
break;
|
|
6323
|
+
}
|
|
6324
|
+
const toolResults = [];
|
|
6325
|
+
for (const tb of toolUseBlocks) {
|
|
6326
|
+
if (tb._malformedArgs !== void 0) {
|
|
6327
|
+
onChunk(`
|
|
6328
|
+
<<tool:${tb.name}:⚠ invalid args>>
|
|
6329
|
+
`);
|
|
6330
|
+
toolResults.push({
|
|
6331
|
+
type: "tool_result",
|
|
6332
|
+
tool_use_id: tb.id,
|
|
6333
|
+
content: `Error: Invalid JSON in tool arguments — could not parse. Please retry with valid JSON. Raw input: ${tb._malformedArgs.slice(0, 200)}`,
|
|
6334
|
+
is_error: true
|
|
6335
|
+
});
|
|
6336
|
+
continue;
|
|
6337
|
+
}
|
|
6338
|
+
const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
|
|
6339
|
+
onChunk(`
|
|
6340
|
+
<<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
|
|
6341
|
+
`);
|
|
6342
|
+
let result;
|
|
6343
|
+
try {
|
|
6344
|
+
result = await onToolCall(tb.name, tb.input);
|
|
6345
|
+
} catch (toolErr) {
|
|
6346
|
+
const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
|
|
6347
|
+
result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
|
|
6348
|
+
}
|
|
6349
|
+
let parsedRich = null;
|
|
6350
|
+
try {
|
|
6351
|
+
const parsed = JSON.parse(result);
|
|
6352
|
+
if (isRichToolResult(parsed)) parsedRich = parsed;
|
|
6353
|
+
} catch {
|
|
6354
|
+
}
|
|
6355
|
+
if (parsedRich) {
|
|
6356
|
+
toolResults.push({
|
|
6357
|
+
type: "tool_result",
|
|
6358
|
+
tool_use_id: tb.id,
|
|
6359
|
+
content: parsedRich.content.map((block) => {
|
|
6360
|
+
if (block.type === "image") {
|
|
6361
|
+
return {
|
|
6362
|
+
type: "image",
|
|
6363
|
+
source: {
|
|
6364
|
+
type: "base64",
|
|
6365
|
+
media_type: block.mediaType,
|
|
6366
|
+
data: block.base64
|
|
6367
|
+
}
|
|
6368
|
+
};
|
|
6369
|
+
}
|
|
6370
|
+
return { type: "text", text: block.text };
|
|
6371
|
+
})
|
|
6372
|
+
});
|
|
6373
|
+
} else {
|
|
6374
|
+
toolResults.push({
|
|
6375
|
+
type: "tool_result",
|
|
6376
|
+
tool_use_id: tb.id,
|
|
6377
|
+
content: result
|
|
6378
|
+
});
|
|
6379
|
+
}
|
|
6380
|
+
recentToolNames.push(tb.name);
|
|
6381
|
+
if (recentToolNames.length > 8) recentToolNames.shift();
|
|
6382
|
+
}
|
|
6383
|
+
messages.push({ role: "user", content: toolResults });
|
|
6384
|
+
if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
|
|
6385
|
+
clickReadLoopNudged = true;
|
|
6386
|
+
messages.push({
|
|
6387
|
+
role: "user",
|
|
6388
|
+
content: `You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
|
|
6389
|
+
});
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
if (iterationsUsed >= maxIterations) {
|
|
6393
|
+
onChunk(`
|
|
6394
|
+
|
|
6124
6395
|
[Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
|
|
6125
6396
|
}
|
|
6126
6397
|
} catch (err) {
|
|
@@ -6155,16 +6426,65 @@ function validateProviderConnection(config, options = { requireModel: true }) {
|
|
|
6155
6426
|
if (!meta) {
|
|
6156
6427
|
return "Selected AI provider is not supported.";
|
|
6157
6428
|
}
|
|
6158
|
-
if (meta.requiresApiKey && !normalized.apiKey) {
|
|
6159
|
-
return `${meta.name} requires an API key. Open settings (Ctrl+,) to add one.`;
|
|
6429
|
+
if (meta.requiresApiKey && !normalized.apiKey) {
|
|
6430
|
+
return `${meta.name} requires an API key. Open settings (Ctrl+,) to add one.`;
|
|
6431
|
+
}
|
|
6432
|
+
if (options.requireModel && !normalized.model) {
|
|
6433
|
+
return normalized.id === "custom" ? "Custom provider requires a model name." : `Select a ${meta.name} model in settings.`;
|
|
6434
|
+
}
|
|
6435
|
+
if (normalized.id === "custom" && !normalized.baseUrl) {
|
|
6436
|
+
return "Custom provider requires a base URL.";
|
|
6437
|
+
}
|
|
6438
|
+
return null;
|
|
6439
|
+
}
|
|
6440
|
+
const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
|
|
6441
|
+
const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
|
|
6442
|
+
function extractLlamaCppCtxSize(payload) {
|
|
6443
|
+
if (!payload || typeof payload !== "object") return null;
|
|
6444
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6445
|
+
const queue = [payload];
|
|
6446
|
+
while (queue.length > 0) {
|
|
6447
|
+
const current = queue.shift();
|
|
6448
|
+
if (!current || typeof current !== "object" || visited.has(current)) continue;
|
|
6449
|
+
visited.add(current);
|
|
6450
|
+
for (const [key, value] of Object.entries(current)) {
|
|
6451
|
+
if (typeof value === "number" && Number.isFinite(value) && /^(n_ctx|ctx_size|context_size)$/i.test(key)) {
|
|
6452
|
+
return value;
|
|
6453
|
+
}
|
|
6454
|
+
if (value && typeof value === "object") {
|
|
6455
|
+
queue.push(value);
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
return null;
|
|
6460
|
+
}
|
|
6461
|
+
function buildLlamaCppCtxWarning(ctxSize) {
|
|
6462
|
+
if (ctxSize == null) {
|
|
6463
|
+
return `Could not detect llama-server context size. Run llama-server with --ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} recommended for Vessel agent loops).`;
|
|
6464
|
+
}
|
|
6465
|
+
if (ctxSize < LLAMA_CPP_MIN_CTX_TOKENS) {
|
|
6466
|
+
return `Detected llama-server ctx-size ${ctxSize}, which is too small for reliable Vessel agent loops. Run llama-server with --ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} recommended).`;
|
|
6160
6467
|
}
|
|
6161
|
-
if (
|
|
6162
|
-
return
|
|
6468
|
+
if (ctxSize < LLAMA_CPP_RECOMMENDED_CTX_TOKENS) {
|
|
6469
|
+
return `Detected llama-server ctx-size ${ctxSize}. This should work, but ${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} is recommended for longer Vessel agent runs.`;
|
|
6163
6470
|
}
|
|
6164
|
-
|
|
6165
|
-
|
|
6471
|
+
return void 0;
|
|
6472
|
+
}
|
|
6473
|
+
async function probeLlamaCppCtxWarning(baseURL) {
|
|
6474
|
+
try {
|
|
6475
|
+
const root = new URL(baseURL);
|
|
6476
|
+
root.pathname = "/props";
|
|
6477
|
+
root.search = "";
|
|
6478
|
+
root.hash = "";
|
|
6479
|
+
const response = await fetch(root.toString());
|
|
6480
|
+
if (!response.ok) {
|
|
6481
|
+
return buildLlamaCppCtxWarning(null);
|
|
6482
|
+
}
|
|
6483
|
+
const payload = await response.json();
|
|
6484
|
+
return buildLlamaCppCtxWarning(extractLlamaCppCtxSize(payload));
|
|
6485
|
+
} catch {
|
|
6486
|
+
return buildLlamaCppCtxWarning(null);
|
|
6166
6487
|
}
|
|
6167
|
-
return null;
|
|
6168
6488
|
}
|
|
6169
6489
|
async function fetchProviderModels(config) {
|
|
6170
6490
|
const normalized = sanitizeProviderConfig(config);
|
|
@@ -6175,7 +6495,7 @@ async function fetchProviderModels(config) {
|
|
|
6175
6495
|
if (normalized.id === "anthropic") {
|
|
6176
6496
|
const client2 = new Anthropic({ apiKey: normalized.apiKey });
|
|
6177
6497
|
const page2 = await client2.models.list();
|
|
6178
|
-
return page2.data.map((model) => model.id);
|
|
6498
|
+
return { ok: true, models: page2.data.map((model) => model.id) };
|
|
6179
6499
|
}
|
|
6180
6500
|
const meta = PROVIDERS[normalized.id];
|
|
6181
6501
|
const baseURL = normalized.baseUrl || meta?.defaultBaseUrl || "https://api.openai.com/v1";
|
|
@@ -6184,7 +6504,13 @@ async function fetchProviderModels(config) {
|
|
|
6184
6504
|
baseURL
|
|
6185
6505
|
});
|
|
6186
6506
|
const page = await client.models.list();
|
|
6187
|
-
|
|
6507
|
+
const models = page.data.map((model) => model.id);
|
|
6508
|
+
const warning = normalized.id === "llama_cpp" ? await probeLlamaCppCtxWarning(baseURL) : void 0;
|
|
6509
|
+
return {
|
|
6510
|
+
ok: true,
|
|
6511
|
+
models,
|
|
6512
|
+
...warning ? { warning } : {}
|
|
6513
|
+
};
|
|
6188
6514
|
}
|
|
6189
6515
|
function createProvider(config) {
|
|
6190
6516
|
const normalized = sanitizeProviderConfig(config);
|
|
@@ -6241,7 +6567,7 @@ function createTraceSession(query, url, title) {
|
|
|
6241
6567
|
}
|
|
6242
6568
|
const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
|
|
6243
6569
|
const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
|
|
6244
|
-
function elementLabel(el) {
|
|
6570
|
+
function elementLabel$1(el) {
|
|
6245
6571
|
return el.text?.trim() || el.label?.trim() || el.value?.trim() || el.placeholder?.trim() || void 0;
|
|
6246
6572
|
}
|
|
6247
6573
|
function isOverlayAction(el) {
|
|
@@ -6257,11 +6583,11 @@ function isRadioOption(el) {
|
|
|
6257
6583
|
function normalizeAction(el) {
|
|
6258
6584
|
return {
|
|
6259
6585
|
index: el.index,
|
|
6260
|
-
label: elementLabel(el),
|
|
6586
|
+
label: elementLabel$1(el),
|
|
6261
6587
|
selector: el.selector,
|
|
6262
6588
|
role: el.role,
|
|
6263
6589
|
labelSource: el.labelSource,
|
|
6264
|
-
looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel(el))
|
|
6590
|
+
looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel$1(el))
|
|
6265
6591
|
};
|
|
6266
6592
|
}
|
|
6267
6593
|
function normalizeStoredAction(action) {
|
|
@@ -6557,7 +6883,7 @@ function getCartItemLinks(page) {
|
|
|
6557
6883
|
return false;
|
|
6558
6884
|
}
|
|
6559
6885
|
if (blockedText.test(text) || blockedHref.test(href)) return false;
|
|
6560
|
-
const key = `${normalizeComparable(text)}|${normalizeUrlForMatch(href) || href}`;
|
|
6886
|
+
const key = `${normalizeComparable$1(text)}|${normalizeUrlForMatch$1(href) || href}`;
|
|
6561
6887
|
if (seen.has(key)) return false;
|
|
6562
6888
|
seen.add(key);
|
|
6563
6889
|
return true;
|
|
@@ -6626,6 +6952,67 @@ function formatCartSnapshot(page) {
|
|
|
6626
6952
|
function isVisibleToUser(el) {
|
|
6627
6953
|
return el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true;
|
|
6628
6954
|
}
|
|
6955
|
+
function purchaseActionPriority(el) {
|
|
6956
|
+
const haystack = normalizeComparable$1(
|
|
6957
|
+
[
|
|
6958
|
+
el.text,
|
|
6959
|
+
el.label,
|
|
6960
|
+
el.name,
|
|
6961
|
+
el.placeholder,
|
|
6962
|
+
el.description,
|
|
6963
|
+
el.href
|
|
6964
|
+
].filter(Boolean).join(" ")
|
|
6965
|
+
);
|
|
6966
|
+
if (!haystack) return Number.POSITIVE_INFINITY;
|
|
6967
|
+
if (/\badd(?: item)? to (?:cart|bag|basket)\b/.test(haystack)) return 0;
|
|
6968
|
+
if (/\b(?:buy now|preorder|pre-order|reserve now|shop now)\b/.test(haystack)) {
|
|
6969
|
+
return 1;
|
|
6970
|
+
}
|
|
6971
|
+
if (/\b(?:checkout|view cart|view basket|go to cart|view bag)\b/.test(haystack)) {
|
|
6972
|
+
return 2;
|
|
6973
|
+
}
|
|
6974
|
+
return Number.POSITIVE_INFINITY;
|
|
6975
|
+
}
|
|
6976
|
+
function isPurchaseActionElement(el) {
|
|
6977
|
+
if (el.type !== "button" && el.type !== "link" && !(el.type === "input" && (el.inputType === "submit" || el.inputType === "button"))) {
|
|
6978
|
+
return false;
|
|
6979
|
+
}
|
|
6980
|
+
return Number.isFinite(purchaseActionPriority(el));
|
|
6981
|
+
}
|
|
6982
|
+
function getPurchaseActionElements(page, options) {
|
|
6983
|
+
const visibleOnly = options?.visibleOnly !== false;
|
|
6984
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6985
|
+
return page.interactiveElements.filter((el) => {
|
|
6986
|
+
if (!isPurchaseActionElement(el)) return false;
|
|
6987
|
+
if (visibleOnly && !isVisibleToUser(el)) return false;
|
|
6988
|
+
if (el.blockedByOverlay) return false;
|
|
6989
|
+
const key = String(
|
|
6990
|
+
el.index ?? el.selector ?? `${el.type}|${el.text || ""}|${el.label || ""}|${el.href || ""}`
|
|
6991
|
+
);
|
|
6992
|
+
if (seen.has(key)) return false;
|
|
6993
|
+
seen.add(key);
|
|
6994
|
+
return true;
|
|
6995
|
+
}).sort((a, b) => {
|
|
6996
|
+
const delta = purchaseActionPriority(a) - purchaseActionPriority(b);
|
|
6997
|
+
if (delta !== 0) return delta;
|
|
6998
|
+
return (a.index ?? Number.MAX_SAFE_INTEGER) - (b.index ?? Number.MAX_SAFE_INTEGER);
|
|
6999
|
+
}).slice(0, 8);
|
|
7000
|
+
}
|
|
7001
|
+
function getOffscreenPurchaseActionElements(page) {
|
|
7002
|
+
const visibleKeys = new Set(
|
|
7003
|
+
getPurchaseActionElements(page, { visibleOnly: true }).map(
|
|
7004
|
+
(el) => String(
|
|
7005
|
+
el.index ?? el.selector ?? `${el.type}|${el.text || ""}|${el.label || ""}|${el.href || ""}`
|
|
7006
|
+
)
|
|
7007
|
+
)
|
|
7008
|
+
);
|
|
7009
|
+
return getPurchaseActionElements(page, { visibleOnly: false }).filter((el) => {
|
|
7010
|
+
const key = String(
|
|
7011
|
+
el.index ?? el.selector ?? `${el.type}|${el.text || ""}|${el.label || ""}|${el.href || ""}`
|
|
7012
|
+
);
|
|
7013
|
+
return !visibleKeys.has(key) && el.visible !== false;
|
|
7014
|
+
});
|
|
7015
|
+
}
|
|
6629
7016
|
function getDialogFocusedElements(page) {
|
|
6630
7017
|
return page.interactiveElements.filter(
|
|
6631
7018
|
(el) => isVisibleToUser(el) && el.context === "dialog"
|
|
@@ -6679,6 +7066,10 @@ function formatInteractiveElements(elements) {
|
|
|
6679
7066
|
const scoreEl = (el) => {
|
|
6680
7067
|
let s = 0;
|
|
6681
7068
|
if (el.context === "dialog") s -= 40;
|
|
7069
|
+
const purchasePriority = purchaseActionPriority(el);
|
|
7070
|
+
if (Number.isFinite(purchasePriority)) {
|
|
7071
|
+
s -= 25 - purchasePriority * 5;
|
|
7072
|
+
}
|
|
6682
7073
|
if (el.visible === false) s += 100;
|
|
6683
7074
|
if (el.inViewport === false) s += 50;
|
|
6684
7075
|
if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
|
|
@@ -7038,10 +7429,10 @@ function chooseAgentReadMode(page) {
|
|
|
7038
7429
|
return "visible_only";
|
|
7039
7430
|
}
|
|
7040
7431
|
}
|
|
7041
|
-
function normalizeComparable(value) {
|
|
7432
|
+
function normalizeComparable$1(value) {
|
|
7042
7433
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
|
|
7043
7434
|
}
|
|
7044
|
-
function normalizeUrlForMatch(value) {
|
|
7435
|
+
function normalizeUrlForMatch$1(value) {
|
|
7045
7436
|
if (!value) return null;
|
|
7046
7437
|
try {
|
|
7047
7438
|
const url = new URL(value);
|
|
@@ -7051,7 +7442,7 @@ function normalizeUrlForMatch(value) {
|
|
|
7051
7442
|
return value.trim().replace(/\/+$/, "").toLowerCase() || null;
|
|
7052
7443
|
}
|
|
7053
7444
|
}
|
|
7054
|
-
function getUrlPathSegments(value) {
|
|
7445
|
+
function getUrlPathSegments$1(value) {
|
|
7055
7446
|
if (!value) return [];
|
|
7056
7447
|
try {
|
|
7057
7448
|
return new URL(value).pathname.split("/").filter(Boolean);
|
|
@@ -7060,7 +7451,7 @@ function getUrlPathSegments(value) {
|
|
|
7060
7451
|
}
|
|
7061
7452
|
}
|
|
7062
7453
|
function isSearchOrListingPage(page) {
|
|
7063
|
-
const haystack = normalizeComparable(
|
|
7454
|
+
const haystack = normalizeComparable$1(
|
|
7064
7455
|
[
|
|
7065
7456
|
page.url,
|
|
7066
7457
|
page.title,
|
|
@@ -7100,30 +7491,30 @@ function getResultCandidates(page) {
|
|
|
7100
7491
|
const entityItems = collectJsonLdEntityItems(page.jsonLd ?? []);
|
|
7101
7492
|
const entityNames = new Set(
|
|
7102
7493
|
entityItems.map(
|
|
7103
|
-
(item) => typeof item.name === "string" ? normalizeComparable(item.name) : ""
|
|
7494
|
+
(item) => typeof item.name === "string" ? normalizeComparable$1(item.name) : ""
|
|
7104
7495
|
).filter(Boolean)
|
|
7105
7496
|
);
|
|
7106
7497
|
const entityUrls = new Set(
|
|
7107
7498
|
entityItems.map(
|
|
7108
|
-
(item) => typeof item.url === "string" ? normalizeUrlForMatch(item.url) : null
|
|
7499
|
+
(item) => typeof item.url === "string" ? normalizeUrlForMatch$1(item.url) : null
|
|
7109
7500
|
).filter((value) => Boolean(value))
|
|
7110
7501
|
);
|
|
7111
|
-
const pageHost = normalizeUrlForMatch(page.url);
|
|
7502
|
+
const pageHost = normalizeUrlForMatch$1(page.url);
|
|
7112
7503
|
const searchOrListingPage = isSearchOrListingPage(page);
|
|
7113
7504
|
const scored = page.interactiveElements.filter(
|
|
7114
7505
|
(element) => element.type === "link" && element.text?.trim() && element.href
|
|
7115
7506
|
).map((element) => {
|
|
7116
7507
|
const text = element.text?.trim() || "";
|
|
7117
|
-
const comparableText = normalizeComparable(text);
|
|
7118
|
-
const href = normalizeUrlForMatch(element.href);
|
|
7119
|
-
const haystack = normalizeComparable(
|
|
7508
|
+
const comparableText = normalizeComparable$1(text);
|
|
7509
|
+
const href = normalizeUrlForMatch$1(element.href);
|
|
7510
|
+
const haystack = normalizeComparable$1(
|
|
7120
7511
|
[element.text, element.description, element.selector, element.href].filter(Boolean).join(" ")
|
|
7121
7512
|
);
|
|
7122
7513
|
let score = 0;
|
|
7123
7514
|
if (entityNames.has(comparableText)) score += 6;
|
|
7124
7515
|
if (href && entityUrls.has(href)) score += 6;
|
|
7125
7516
|
if (entityItems.some((item) => {
|
|
7126
|
-
const name = typeof item.name === "string" ? normalizeComparable(item.name) : "";
|
|
7517
|
+
const name = typeof item.name === "string" ? normalizeComparable$1(item.name) : "";
|
|
7127
7518
|
return Boolean(name) && (name.includes(comparableText) || comparableText.includes(name));
|
|
7128
7519
|
})) {
|
|
7129
7520
|
score += 4;
|
|
@@ -7137,7 +7528,7 @@ function getResultCandidates(page) {
|
|
|
7137
7528
|
} catch {
|
|
7138
7529
|
}
|
|
7139
7530
|
}
|
|
7140
|
-
const hrefSegments = getUrlPathSegments(element.href);
|
|
7531
|
+
const hrefSegments = getUrlPathSegments$1(element.href);
|
|
7141
7532
|
if (hrefSegments.length >= 2) score += 1;
|
|
7142
7533
|
if (text.includes("/")) score += 1;
|
|
7143
7534
|
if (searchOrListingPage && (element.context === "article" || element.context === "main" || element.context === "content")) {
|
|
@@ -7170,7 +7561,7 @@ function getResultCandidates(page) {
|
|
|
7170
7561
|
);
|
|
7171
7562
|
const seen = /* @__PURE__ */ new Set();
|
|
7172
7563
|
return scored.map(({ element }) => element).filter((element) => {
|
|
7173
|
-
const key = `${normalizeComparable(element.text || "")}|${normalizeUrlForMatch(element.href) || ""}`;
|
|
7564
|
+
const key = `${normalizeComparable$1(element.text || "")}|${normalizeUrlForMatch$1(element.href) || ""}`;
|
|
7174
7565
|
if (seen.has(key)) return false;
|
|
7175
7566
|
seen.add(key);
|
|
7176
7567
|
return true;
|
|
@@ -7406,6 +7797,10 @@ function buildScopedContext(page, mode) {
|
|
|
7406
7797
|
})).filter((form) => form.fields.length > 0)
|
|
7407
7798
|
};
|
|
7408
7799
|
const quantityElements = getQuantityElements(visiblePage);
|
|
7800
|
+
const purchaseActions = getPurchaseActionElements(visiblePage, {
|
|
7801
|
+
visibleOnly: true
|
|
7802
|
+
});
|
|
7803
|
+
const offscreenPurchaseActions = getOffscreenPurchaseActionElements(page);
|
|
7409
7804
|
const cartSnapshot = formatCartSnapshot(visiblePage);
|
|
7410
7805
|
const visibleForms = visiblePage.forms;
|
|
7411
7806
|
const dialogFocus = formatDialogFocus(page);
|
|
@@ -7461,6 +7856,19 @@ function buildScopedContext(page, mode) {
|
|
|
7461
7856
|
sections.push(formatQuantityElements(quantityElements));
|
|
7462
7857
|
sections.push("");
|
|
7463
7858
|
}
|
|
7859
|
+
if (purchaseActions.length > 0) {
|
|
7860
|
+
sections.push("### Primary Purchase Actions");
|
|
7861
|
+
sections.push(formatInteractiveElements(purchaseActions));
|
|
7862
|
+
sections.push("");
|
|
7863
|
+
}
|
|
7864
|
+
if (offscreenPurchaseActions.length > 0) {
|
|
7865
|
+
sections.push("### Offscreen Purchase Actions");
|
|
7866
|
+
sections.push(
|
|
7867
|
+
"These purchase controls are present on the page but currently outside the viewport. You can scroll to reveal them or click them by index."
|
|
7868
|
+
);
|
|
7869
|
+
sections.push(formatInteractiveElements(offscreenPurchaseActions));
|
|
7870
|
+
sections.push("");
|
|
7871
|
+
}
|
|
7464
7872
|
if (visiblePage.interactiveElements.length > 0) {
|
|
7465
7873
|
sections.push(
|
|
7466
7874
|
`### Visible In-Viewport Interactive Elements (${visiblePage.interactiveElements.length})`
|
|
@@ -7774,7 +8182,7 @@ const SHARED_READ_INSTRUCTIONS = [
|
|
|
7774
8182
|
'If read_page returns empty or times out, do not retry with the same mode. Switch to read_page(mode="glance") or use screenshot.',
|
|
7775
8183
|
"Use screenshot when you need the exact rendered page or text extraction is failing.",
|
|
7776
8184
|
"read_page inspects the page without moving the human-visible viewport. If you say you are going to scroll, call scroll or scroll_to_element so the user sees the page move too.",
|
|
7777
|
-
"After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode."
|
|
8185
|
+
"After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. When a click navigates to a new page, the click result includes a page snapshot — you only need read_page if you need details beyond what the snapshot shows."
|
|
7778
8186
|
];
|
|
7779
8187
|
const DEFAULT_EXTRA_INSTRUCTIONS = [
|
|
7780
8188
|
"Create a checkpoint before risky multi-step flows or before leaving an important state.",
|
|
@@ -7789,7 +8197,8 @@ const DEFAULT_EXTRA_INSTRUCTIONS = [
|
|
|
7789
8197
|
"After completing a task or answering a question, offer 1-2 brief, natural follow-up suggestions that make sense in context.",
|
|
7790
8198
|
'MINIMIZE TOOL CALLS: Every tool call takes time and costs a round trip. Be efficient. The fastest path is usually: navigate -> search -> wait_for or read_page(mode="results_only") -> click.',
|
|
7791
8199
|
"USE YOUR KNOWLEDGE: When the user asks for recommendations, make a clear recommendation, explain your reasoning briefly, and then execute.",
|
|
7792
|
-
"NEVER USE EMOJIS unless the user uses them first."
|
|
8200
|
+
"NEVER USE EMOJIS unless the user uses them first.",
|
|
8201
|
+
"When adding multiple items to a cart, track which products you've already added. After adding an item, go back and select a DIFFERENT product. The system blocks duplicate cart additions and shows 'Already in cart' warnings."
|
|
7793
8202
|
];
|
|
7794
8203
|
const COMPACT_FOCUS_INSTRUCTIONS = [
|
|
7795
8204
|
"Trust the latest tool result over the initial page context. If a tool result shows a new URL/title/results page, that is the current truth.",
|
|
@@ -7799,6 +8208,12 @@ const COMPACT_FOCUS_INSTRUCTIONS = [
|
|
|
7799
8208
|
"If search or read_page returns results on the target site, continue from those results. Do not assume the search failed unless the tool result says it failed.",
|
|
7800
8209
|
"Use current_tab only if you are genuinely unsure of the current page after reading the latest tool result.",
|
|
7801
8210
|
"On retail tasks, prefer this sequence: navigate -> site search or curated section -> inspect/read results -> click a product -> add to cart -> explain.",
|
|
8211
|
+
'On product/detail pages, prefer read_page(mode="visible_only") to find indexed purchase controls like Add to Cart, quantity, and checkout actions.',
|
|
8212
|
+
"When read_page or inspect_element gives you an element index, prefer click(index=N) over selector-based clicks.",
|
|
8213
|
+
'If a product page has no visible purchase control, scroll and call read_page(mode="visible_only") again. Do not loop on generic inspect_element calls against navigation or unrelated regions.',
|
|
8214
|
+
"After adding an item to cart and going back, ALWAYS call read_page to see the current results. The system shows which products are already in your cart — do NOT click those again. Pick a DIFFERENT product from the list. If all visible results are already in cart, scroll down for more.",
|
|
8215
|
+
'On search results pages, always call read_page(mode="results_only") first. Click products by their [#N] index from the Results section. Never click filter or sort links (e.g. Used, New, Format, Price).',
|
|
8216
|
+
"After go_back, always call read_page before clicking. The page may have changed.",
|
|
7802
8217
|
"Keep your reasoning short. Prefer taking the next tool action over writing a long plan."
|
|
7803
8218
|
];
|
|
7804
8219
|
function buildInstructionBlock(instructions) {
|
|
@@ -7849,6 +8264,342 @@ function buildAgentSystemPrompt(input) {
|
|
|
7849
8264
|
...instructionBlocks
|
|
7850
8265
|
].join("\n\n");
|
|
7851
8266
|
}
|
|
8267
|
+
function normalizeComparable(value) {
|
|
8268
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
|
|
8269
|
+
}
|
|
8270
|
+
function normalizeUrlForMatch(value) {
|
|
8271
|
+
if (!value) return null;
|
|
8272
|
+
try {
|
|
8273
|
+
const url = new URL(value);
|
|
8274
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
8275
|
+
return `${url.origin}${pathname}`.toLowerCase();
|
|
8276
|
+
} catch {
|
|
8277
|
+
return value.trim().replace(/\/+$/, "").toLowerCase() || null;
|
|
8278
|
+
}
|
|
8279
|
+
}
|
|
8280
|
+
function getUrlPathSegments(value) {
|
|
8281
|
+
if (!value) return [];
|
|
8282
|
+
try {
|
|
8283
|
+
return new URL(value).pathname.split("/").filter(Boolean);
|
|
8284
|
+
} catch {
|
|
8285
|
+
return value.split("?")[0].split("#")[0].split("/").filter(Boolean);
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
8288
|
+
function isVisible(element) {
|
|
8289
|
+
return element.visible !== false && element.obscured !== true && element.blockedByOverlay !== true;
|
|
8290
|
+
}
|
|
8291
|
+
function isInViewport(element) {
|
|
8292
|
+
return element.inViewport !== false;
|
|
8293
|
+
}
|
|
8294
|
+
function looksLikeListingPage(page) {
|
|
8295
|
+
const haystack = normalizeComparable(
|
|
8296
|
+
[
|
|
8297
|
+
page.url,
|
|
8298
|
+
page.title,
|
|
8299
|
+
page.excerpt,
|
|
8300
|
+
page.content.slice(0, 2e3),
|
|
8301
|
+
page.headings.map((heading) => heading.text).join(" ")
|
|
8302
|
+
].filter(Boolean).join(" ")
|
|
8303
|
+
);
|
|
8304
|
+
return /\b(search|results|browse|discover|arrivals|new arrivals|staff picks|picks of the month|monthly picks|featured|bestsellers|best sellers|category|categories|books|book list|fiction|nonfiction|poetry|history|science|children)\b/.test(
|
|
8305
|
+
haystack
|
|
8306
|
+
);
|
|
8307
|
+
}
|
|
8308
|
+
function isBlockedLabel(label) {
|
|
8309
|
+
return /\b(home|menu|about|contact|privacy|terms|login|sign in|sign up|subscribe|newsletter|facebook|instagram|pinterest|share|print|next|previous|prev|sort|filter|wishlist|account|cart|checkout|view all|see all|refine|narrow|clear all|remove filter)\b/.test(
|
|
8310
|
+
label
|
|
8311
|
+
);
|
|
8312
|
+
}
|
|
8313
|
+
function resultScore(page, element, listingLike) {
|
|
8314
|
+
if (element.type !== "link" || !element.href || !element.text?.trim()) return -1;
|
|
8315
|
+
const text = element.text.trim();
|
|
8316
|
+
const comparableText = normalizeComparable(text);
|
|
8317
|
+
const href = normalizeUrlForMatch(element.href);
|
|
8318
|
+
const pageUrl = normalizeUrlForMatch(page.url);
|
|
8319
|
+
const hrefSegments = getUrlPathSegments(element.href);
|
|
8320
|
+
const haystack = normalizeComparable(
|
|
8321
|
+
[text, element.description, element.selector, element.href, element.context].filter(Boolean).join(" ")
|
|
8322
|
+
);
|
|
8323
|
+
if (!comparableText || isBlockedLabel(comparableText)) return -1;
|
|
8324
|
+
let score = 0;
|
|
8325
|
+
if (element.context === "article") score += 5;
|
|
8326
|
+
if (element.context === "main" || element.context === "content") score += 4;
|
|
8327
|
+
if (!element.context) score += 1;
|
|
8328
|
+
if (isVisible(element)) score += 2;
|
|
8329
|
+
if (isInViewport(element)) score += 3;
|
|
8330
|
+
if (text.length >= 8 && text.length <= 140) score += 2;
|
|
8331
|
+
if (text.split(/\s+/).length >= 2) score += 2;
|
|
8332
|
+
if (text.split(/\s+/).length >= 4) score += 1;
|
|
8333
|
+
if (hrefSegments.length >= 2) score += 1;
|
|
8334
|
+
if (hrefSegments.some((segment) => /book|books|item|product|title|catalog/.test(segment))) {
|
|
8335
|
+
score += 3;
|
|
8336
|
+
}
|
|
8337
|
+
if (href && pageUrl) {
|
|
8338
|
+
try {
|
|
8339
|
+
if (new URL(href).origin === new URL(pageUrl).origin) score += 2;
|
|
8340
|
+
} catch {
|
|
8341
|
+
}
|
|
8342
|
+
}
|
|
8343
|
+
if (listingLike) score += 2;
|
|
8344
|
+
if (/\b(book|novel|story|poems|poetry|essays|memoir|history|science|fiction)\b/.test(haystack)) {
|
|
8345
|
+
score += 2;
|
|
8346
|
+
}
|
|
8347
|
+
if (/\b(author|hardcover|paperback|preorder|pre-order|signed edition)\b/.test(haystack)) {
|
|
8348
|
+
score += 1;
|
|
8349
|
+
}
|
|
8350
|
+
if (element.context === "nav" || element.context === "header" || element.context === "footer" || element.context === "sidebar" || element.context === "dialog") {
|
|
8351
|
+
score -= 6;
|
|
8352
|
+
}
|
|
8353
|
+
if (/\b(filter|sort|format|price|signed|staff picks|more results|view all)\b/.test(comparableText)) {
|
|
8354
|
+
score -= 3;
|
|
8355
|
+
}
|
|
8356
|
+
try {
|
|
8357
|
+
const linkUrl = new URL(element.href);
|
|
8358
|
+
const filterParams = ["condition", "binding", "format", "availability", "sort", "filter", "price", "category_id", "view"];
|
|
8359
|
+
if (filterParams.some((p) => linkUrl.searchParams.has(p))) {
|
|
8360
|
+
score -= 10;
|
|
8361
|
+
}
|
|
8362
|
+
const filterPathSegments = ["format", "condition", "binding", "availability", "sort"];
|
|
8363
|
+
const hasFilterPath = filterPathSegments.some(
|
|
8364
|
+
(seg) => hrefSegments.some((s) => s.toLowerCase() === seg)
|
|
8365
|
+
);
|
|
8366
|
+
if (hasFilterPath) {
|
|
8367
|
+
score -= 10;
|
|
8368
|
+
}
|
|
8369
|
+
} catch {
|
|
8370
|
+
}
|
|
8371
|
+
return score;
|
|
8372
|
+
}
|
|
8373
|
+
function getCompactPrimaryResultLinks(page, options) {
|
|
8374
|
+
const listingLike = looksLikeListingPage(page);
|
|
8375
|
+
const max = options?.max ?? 8;
|
|
8376
|
+
const visibleOnly = options?.visibleOnly === true;
|
|
8377
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8378
|
+
return page.interactiveElements.filter((element) => !visibleOnly || isVisible(element)).map((element) => ({
|
|
8379
|
+
element,
|
|
8380
|
+
score: resultScore(page, element, listingLike)
|
|
8381
|
+
})).filter(({ score }) => score >= (listingLike ? 5 : 7)).sort(
|
|
8382
|
+
(a, b) => b.score - a.score || (a.element.index ?? Number.MAX_SAFE_INTEGER) - (b.element.index ?? Number.MAX_SAFE_INTEGER)
|
|
8383
|
+
).map(({ element }) => element).filter((element) => {
|
|
8384
|
+
const key = `${normalizeComparable(element.text)}|${normalizeUrlForMatch(element.href) || ""}`;
|
|
8385
|
+
if (seen.has(key)) return false;
|
|
8386
|
+
seen.add(key);
|
|
8387
|
+
return true;
|
|
8388
|
+
}).slice(0, max);
|
|
8389
|
+
}
|
|
8390
|
+
function looksLikeProductPage(page) {
|
|
8391
|
+
const url = (page.url || "").toLowerCase();
|
|
8392
|
+
return /\/(book|product|item|detail|dp|gp\/product)\//i.test(url) || /\b(add to cart|add to bag|add to basket|buy now)\b/i.test(
|
|
8393
|
+
page.content.slice(0, 3e3)
|
|
8394
|
+
);
|
|
8395
|
+
}
|
|
8396
|
+
function chooseCompactReadMode(page, fallbackMode) {
|
|
8397
|
+
if (fallbackMode === "results_only") return fallbackMode;
|
|
8398
|
+
if (looksLikeProductPage(page)) return fallbackMode;
|
|
8399
|
+
const candidates = getCompactPrimaryResultLinks(page, { max: 6 });
|
|
8400
|
+
if (candidates.length >= 4) return "results_only";
|
|
8401
|
+
if (candidates.length >= 2 && looksLikeListingPage(page)) return "results_only";
|
|
8402
|
+
return fallbackMode;
|
|
8403
|
+
}
|
|
8404
|
+
const MAX_RESULTS = 6;
|
|
8405
|
+
const MAX_CONTROLS = 8;
|
|
8406
|
+
const MAX_FIELDS = 8;
|
|
8407
|
+
const MAX_HEADINGS = 5;
|
|
8408
|
+
const MAX_TEXT_CHARS = 420;
|
|
8409
|
+
function compactText(value, max = MAX_TEXT_CHARS) {
|
|
8410
|
+
const text = (value || "").replace(/\s+/g, " ").trim();
|
|
8411
|
+
if (!text) return "";
|
|
8412
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
8413
|
+
}
|
|
8414
|
+
function isVisibleElement(element) {
|
|
8415
|
+
return element.visible !== false && element.inViewport !== false && element.blockedByOverlay !== true && element.obscured !== true;
|
|
8416
|
+
}
|
|
8417
|
+
function elementLabel(element) {
|
|
8418
|
+
return compactText(
|
|
8419
|
+
element.text || element.label || element.placeholder || element.name || element.href || element.description,
|
|
8420
|
+
96
|
|
8421
|
+
) || "Element";
|
|
8422
|
+
}
|
|
8423
|
+
function formatElement(element) {
|
|
8424
|
+
const prefix = element.index != null ? `[#${element.index}] ` : "";
|
|
8425
|
+
const kind = element.type === "input" ? `${element.inputType || "text"} input` : element.type === "select" ? "select" : element.type;
|
|
8426
|
+
const href = element.type === "link" && element.href ? ` -> ${element.href}` : "";
|
|
8427
|
+
return `${prefix}${elementLabel(element)} (${kind})${href}`;
|
|
8428
|
+
}
|
|
8429
|
+
function uniqueElements(elements) {
|
|
8430
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8431
|
+
return elements.filter((element) => {
|
|
8432
|
+
const key = `${element.index ?? ""}|${element.type}|${elementLabel(element)}|${element.href ?? ""}`;
|
|
8433
|
+
if (seen.has(key)) return false;
|
|
8434
|
+
seen.add(key);
|
|
8435
|
+
return true;
|
|
8436
|
+
});
|
|
8437
|
+
}
|
|
8438
|
+
function isPaginationLike(element) {
|
|
8439
|
+
const text = `${element.text || ""} ${element.label || ""}`.toLowerCase();
|
|
8440
|
+
return /\b(next|prev|previous|load more|more results)\b/.test(text);
|
|
8441
|
+
}
|
|
8442
|
+
function getPrimaryResultLinks(page) {
|
|
8443
|
+
return uniqueElements(
|
|
8444
|
+
getCompactPrimaryResultLinks(page, {
|
|
8445
|
+
visibleOnly: true,
|
|
8446
|
+
max: MAX_RESULTS
|
|
8447
|
+
}).filter((element) => !isPaginationLike(element))
|
|
8448
|
+
);
|
|
8449
|
+
}
|
|
8450
|
+
function isPurchaseControl(element) {
|
|
8451
|
+
const text = `${element.text || ""} ${element.label || ""}`.toLowerCase();
|
|
8452
|
+
return /\b(add to cart|add to bag|add to basket|buy now|checkout|view cart)\b/.test(
|
|
8453
|
+
text
|
|
8454
|
+
);
|
|
8455
|
+
}
|
|
8456
|
+
function isAddToCartControl(element) {
|
|
8457
|
+
const text = `${element.text || ""} ${element.label || ""}`.toLowerCase();
|
|
8458
|
+
return /\badd to cart|add to bag|add to basket\b/.test(text);
|
|
8459
|
+
}
|
|
8460
|
+
function looksLikeProductDetailPage(page) {
|
|
8461
|
+
return /\/book\//i.test(page.url) || /\bbook\b/i.test(page.title);
|
|
8462
|
+
}
|
|
8463
|
+
function hasCartConfirmationState(page) {
|
|
8464
|
+
const haystack = compactText(
|
|
8465
|
+
[
|
|
8466
|
+
page.url,
|
|
8467
|
+
page.title,
|
|
8468
|
+
page.excerpt,
|
|
8469
|
+
page.content.slice(0, 1200),
|
|
8470
|
+
page.overlays.map((overlay) => overlay.label || overlay.message || overlay.text || overlay.kind || "").join(" ")
|
|
8471
|
+
].filter(Boolean).join(" "),
|
|
8472
|
+
1600
|
|
8473
|
+
).toLowerCase();
|
|
8474
|
+
return /\/cart\b/.test(page.url.toLowerCase()) || /\b(cart confirmation|added to cart|added to bag|added to basket|continue shopping|shopping cart|view cart|checkout)\b/.test(
|
|
8475
|
+
haystack
|
|
8476
|
+
);
|
|
8477
|
+
}
|
|
8478
|
+
function getVisibleControls(page) {
|
|
8479
|
+
return uniqueElements(page.interactiveElements.filter(isVisibleElement)).slice(
|
|
8480
|
+
0,
|
|
8481
|
+
MAX_CONTROLS
|
|
8482
|
+
);
|
|
8483
|
+
}
|
|
8484
|
+
function getVisiblePurchaseControls(page) {
|
|
8485
|
+
return uniqueElements(
|
|
8486
|
+
page.interactiveElements.filter(isVisibleElement).filter(isPurchaseControl).sort((a, b) => (a.index ?? Number.MAX_SAFE_INTEGER) - (b.index ?? Number.MAX_SAFE_INTEGER))
|
|
8487
|
+
).slice(0, 6);
|
|
8488
|
+
}
|
|
8489
|
+
function getOffscreenPurchaseControls(page) {
|
|
8490
|
+
const visibleKeys = new Set(getVisiblePurchaseControls(page).map(controlKey));
|
|
8491
|
+
return uniqueElements(
|
|
8492
|
+
page.interactiveElements.filter((element) => isPurchaseControl(element)).filter((element) => element.blockedByOverlay !== true).filter((element) => element.visible !== false).filter((element) => !visibleKeys.has(controlKey(element))).sort((a, b) => (a.index ?? Number.MAX_SAFE_INTEGER) - (b.index ?? Number.MAX_SAFE_INTEGER))
|
|
8493
|
+
).slice(0, 4);
|
|
8494
|
+
}
|
|
8495
|
+
function controlKey(element) {
|
|
8496
|
+
return `${element.index ?? ""}|${element.type}|${elementLabel(element)}|${element.href ?? ""}`;
|
|
8497
|
+
}
|
|
8498
|
+
function isLowValueListingControl(element) {
|
|
8499
|
+
const label = elementLabel(element).toLowerCase();
|
|
8500
|
+
return /\b(filter|sort|format|price|availability|signed edition|binding|language|refine|clear all|remove filter)\b/.test(
|
|
8501
|
+
label
|
|
8502
|
+
);
|
|
8503
|
+
}
|
|
8504
|
+
function getVisibleFormFields(page) {
|
|
8505
|
+
return uniqueElements(
|
|
8506
|
+
page.forms.flatMap((form) => form.fields).filter(isVisibleElement)
|
|
8507
|
+
).slice(0, MAX_FIELDS);
|
|
8508
|
+
}
|
|
8509
|
+
function pushSection(lines, title, items) {
|
|
8510
|
+
if (items.length === 0) return;
|
|
8511
|
+
lines.push("");
|
|
8512
|
+
lines.push(title);
|
|
8513
|
+
lines.push(...items.map((item) => `- ${item}`));
|
|
8514
|
+
}
|
|
8515
|
+
function buildTextSnapshot(page) {
|
|
8516
|
+
const excerpt = compactText(page.excerpt);
|
|
8517
|
+
if (excerpt) return [excerpt];
|
|
8518
|
+
const content = compactText(page.content);
|
|
8519
|
+
return content ? [content] : [];
|
|
8520
|
+
}
|
|
8521
|
+
function buildCompactScopedContext(page, mode, pageType = detectPageType(page)) {
|
|
8522
|
+
const lines = [
|
|
8523
|
+
`**URL:** ${page.url}`,
|
|
8524
|
+
`**Title:** ${page.title}`,
|
|
8525
|
+
`**Page Type:** ${pageType}`,
|
|
8526
|
+
`**Mode:** ${mode}`
|
|
8527
|
+
];
|
|
8528
|
+
if (page.byline) {
|
|
8529
|
+
lines.push(`**Author:** ${compactText(page.byline, 120)}`);
|
|
8530
|
+
}
|
|
8531
|
+
const warnings = (page.pageIssues || []).slice(0, 3).map((issue) => compactText(issue.summary, 140));
|
|
8532
|
+
pushSection(lines, "### Access Warnings", warnings);
|
|
8533
|
+
const blockingOverlays = page.overlays.filter((overlay) => overlay.blocksInteraction).slice(0, 3).map(
|
|
8534
|
+
(overlay) => compactText(
|
|
8535
|
+
overlay.label || overlay.message || overlay.text || overlay.kind || overlay.type,
|
|
8536
|
+
140
|
|
8537
|
+
)
|
|
8538
|
+
);
|
|
8539
|
+
pushSection(lines, "### Immediate Blockers", blockingOverlays);
|
|
8540
|
+
const visiblePurchaseControls = getVisiblePurchaseControls(page);
|
|
8541
|
+
const offscreenPurchaseControls = getOffscreenPurchaseControls(page);
|
|
8542
|
+
const purchaseControls = visiblePurchaseControls.map(formatElement);
|
|
8543
|
+
const addToCartVisible = visiblePurchaseControls.some(isAddToCartControl);
|
|
8544
|
+
const addToCartOffscreen = offscreenPurchaseControls.some(isAddToCartControl);
|
|
8545
|
+
if (looksLikeProductDetailPage(page) && !hasCartConfirmationState(page)) {
|
|
8546
|
+
if (addToCartVisible) {
|
|
8547
|
+
pushSection(lines, "### Action Status", [
|
|
8548
|
+
"Product detail page open. This item is not in the cart yet.",
|
|
8549
|
+
"Click Add to Cart and wait for cart confirmation before moving on."
|
|
8550
|
+
]);
|
|
8551
|
+
} else if (addToCartOffscreen) {
|
|
8552
|
+
pushSection(lines, "### Action Status", [
|
|
8553
|
+
"Product detail page open. This item is not in the cart yet.",
|
|
8554
|
+
"Add to Cart is present but outside the current viewport.",
|
|
8555
|
+
"Scroll once or use the offscreen purchase control below, then wait for cart confirmation."
|
|
8556
|
+
]);
|
|
8557
|
+
}
|
|
8558
|
+
}
|
|
8559
|
+
pushSection(lines, "### Visible Purchase Controls", purchaseControls);
|
|
8560
|
+
pushSection(
|
|
8561
|
+
lines,
|
|
8562
|
+
"### Offscreen Purchase Actions",
|
|
8563
|
+
offscreenPurchaseControls.map(formatElement)
|
|
8564
|
+
);
|
|
8565
|
+
const primaryResultElements = getPrimaryResultLinks(page);
|
|
8566
|
+
const primaryResults = primaryResultElements.map(formatElement);
|
|
8567
|
+
if (primaryResults.length > 0) {
|
|
8568
|
+
lines.push("");
|
|
8569
|
+
lines.push("### Results — click one of these to open a product");
|
|
8570
|
+
lines.push(...primaryResults.map((item) => `- ${item}`));
|
|
8571
|
+
lines.push("");
|
|
8572
|
+
lines.push("IMPORTANT: Use click(index=N) on a result above. Do NOT click filter or sort links.");
|
|
8573
|
+
}
|
|
8574
|
+
if (pageType === "FORM" || pageType === "LOGIN" || mode === "forms_only") {
|
|
8575
|
+
pushSection(
|
|
8576
|
+
lines,
|
|
8577
|
+
"### Form Fields",
|
|
8578
|
+
getVisibleFormFields(page).map(formatElement)
|
|
8579
|
+
);
|
|
8580
|
+
}
|
|
8581
|
+
if (mode === "visible_only" || mode === "interactives_only" || pageType === "SEARCH_READY" || pageType === "GENERAL") {
|
|
8582
|
+
const primaryResultKeys = new Set(primaryResultElements.map(controlKey));
|
|
8583
|
+
const visibleControls = getVisibleControls(page).filter((element) => !primaryResultKeys.has(controlKey(element))).filter(
|
|
8584
|
+
(element) => primaryResultElements.length > 0 ? !isLowValueListingControl(element) : true
|
|
8585
|
+
).map(formatElement);
|
|
8586
|
+
pushSection(
|
|
8587
|
+
lines,
|
|
8588
|
+
"### Page Controls (filters, sorts — avoid when selecting products)",
|
|
8589
|
+
visibleControls
|
|
8590
|
+
);
|
|
8591
|
+
}
|
|
8592
|
+
const headingItems = page.headings.slice(0, MAX_HEADINGS).map((heading) => `H${heading.level}: ${compactText(heading.text, 100)}`);
|
|
8593
|
+
pushSection(lines, "### Top Headings", headingItems);
|
|
8594
|
+
if (mode === "summary" || mode === "text_only" || lines.length <= 6) {
|
|
8595
|
+
pushSection(lines, "### Text Snapshot", buildTextSnapshot(page));
|
|
8596
|
+
}
|
|
8597
|
+
lines.push("");
|
|
8598
|
+
lines.push(
|
|
8599
|
+
`Stats: ${page.interactiveElements.length} interactives, ${page.forms.length} forms, ${page.navigation.length} nav links, ${page.headings.length} headings`
|
|
8600
|
+
);
|
|
8601
|
+
return lines.join("\n");
|
|
8602
|
+
}
|
|
7852
8603
|
const WRAPPING_QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
|
|
7853
8604
|
function stripWrappingQuotes(value) {
|
|
7854
8605
|
const trimmed = value.trim();
|
|
@@ -7994,10 +8745,11 @@ const TOOL_DEFINITIONS = [
|
|
|
7994
8745
|
{
|
|
7995
8746
|
name: "click",
|
|
7996
8747
|
title: "Click Element",
|
|
7997
|
-
description: "Click an element on the page by its index number or
|
|
8748
|
+
description: "Click an element on the page by its index number, CSS selector, or visible text/section name. If you know the label on the page but not the index yet, pass text instead of guessing a selector.",
|
|
7998
8749
|
inputSchema: {
|
|
7999
8750
|
index: zod.z.number().optional().describe("Element index from the page content listing"),
|
|
8000
|
-
selector: zod.z.string().optional().describe("CSS selector as fallback")
|
|
8751
|
+
selector: zod.z.string().optional().describe("CSS selector as fallback"),
|
|
8752
|
+
text: zod.z.string().optional().describe("Visible label, link text, button text, or section name to match")
|
|
8001
8753
|
},
|
|
8002
8754
|
tier: 0
|
|
8003
8755
|
},
|
|
@@ -8118,10 +8870,11 @@ const TOOL_DEFINITIONS = [
|
|
|
8118
8870
|
{
|
|
8119
8871
|
name: "inspect_element",
|
|
8120
8872
|
title: "Inspect Element",
|
|
8121
|
-
description: "Inspect one element and its nearest local UI region such as a product card, result row, form section, or modal.
|
|
8873
|
+
description: "Inspect one element and its nearest local UI region such as a product card, result row, form section, or modal. You can target it by index, selector, or visible text/section name when you know what it says but not where it is.",
|
|
8122
8874
|
inputSchema: {
|
|
8123
8875
|
index: zod.z.number().optional().describe("Element index to inspect"),
|
|
8124
8876
|
selector: zod.z.string().optional().describe("CSS selector to inspect"),
|
|
8877
|
+
text: zod.z.string().optional().describe("Visible label or section text to locate before inspecting"),
|
|
8125
8878
|
limit: zod.z.number().optional().describe("Maximum nearby controls to include (default 8)")
|
|
8126
8879
|
},
|
|
8127
8880
|
tier: 1,
|
|
@@ -8473,10 +9226,11 @@ const TOOL_DEFINITIONS = [
|
|
|
8473
9226
|
{
|
|
8474
9227
|
name: "scroll_to_element",
|
|
8475
9228
|
title: "Scroll To Element",
|
|
8476
|
-
description: "Scroll a specific element into view by index or
|
|
9229
|
+
description: "Scroll a specific element into view by index, selector, or visible text/section name. Useful for navigating to off-screen content when you know the heading or label you want.",
|
|
8477
9230
|
inputSchema: {
|
|
8478
9231
|
index: zod.z.number().optional().describe("Element index to scroll to"),
|
|
8479
9232
|
selector: zod.z.string().optional().describe("CSS selector to scroll to"),
|
|
9233
|
+
text: zod.z.string().optional().describe("Visible label or section text to scroll into view"),
|
|
8480
9234
|
position: zod.z.enum(["center", "top", "bottom"]).optional().describe(
|
|
8481
9235
|
"Where to position the element in the viewport (default center)"
|
|
8482
9236
|
)
|
|
@@ -9776,6 +10530,351 @@ function deleteNamedSession(name) {
|
|
|
9776
10530
|
fs$1.unlinkSync(filePath);
|
|
9777
10531
|
return true;
|
|
9778
10532
|
}
|
|
10533
|
+
function isInvalidTextTargetQuery(rawQuery) {
|
|
10534
|
+
const trimmed = String(rawQuery || "").trim();
|
|
10535
|
+
if (!trimmed) return true;
|
|
10536
|
+
if (/<\/?[a-z][^>]*>/i.test(trimmed)) return true;
|
|
10537
|
+
if (/^<\/?[a-z][^&]*>$/i.test(trimmed)) return true;
|
|
10538
|
+
if (/^<\/?[a-z][a-z0-9:-]*>$/i.test(trimmed)) return true;
|
|
10539
|
+
return false;
|
|
10540
|
+
}
|
|
10541
|
+
function resolveTextTargetInDocument(doc, rawQuery, mode) {
|
|
10542
|
+
function normalize(value) {
|
|
10543
|
+
return String(value || "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
10544
|
+
}
|
|
10545
|
+
function text(value) {
|
|
10546
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
10547
|
+
}
|
|
10548
|
+
function escapeSelectorValue(value) {
|
|
10549
|
+
const cssObject = typeof CSS !== "undefined" ? CSS : void 0;
|
|
10550
|
+
if (cssObject && typeof cssObject.escape === "function") {
|
|
10551
|
+
return cssObject.escape(value);
|
|
10552
|
+
}
|
|
10553
|
+
return String(value).replace(/["\\]/g, "\\$&");
|
|
10554
|
+
}
|
|
10555
|
+
function uniqueSelector(candidate) {
|
|
10556
|
+
if (!candidate) return null;
|
|
10557
|
+
try {
|
|
10558
|
+
return doc.querySelectorAll(candidate).length === 1 ? candidate : null;
|
|
10559
|
+
} catch {
|
|
10560
|
+
return null;
|
|
10561
|
+
}
|
|
10562
|
+
}
|
|
10563
|
+
function uniqueAttributeSelector(el, attribute) {
|
|
10564
|
+
const value = text(el.getAttribute(attribute));
|
|
10565
|
+
if (!value) return null;
|
|
10566
|
+
const candidate = `${el.tagName.toLowerCase()}[${attribute}="${escapeSelectorValue(value)}"]`;
|
|
10567
|
+
return uniqueSelector(candidate);
|
|
10568
|
+
}
|
|
10569
|
+
function selectorFor(el) {
|
|
10570
|
+
if (!el) return null;
|
|
10571
|
+
const htmlEl = el;
|
|
10572
|
+
if (htmlEl.id) return `#${escapeSelectorValue(htmlEl.id)}`;
|
|
10573
|
+
for (const attribute of [
|
|
10574
|
+
"data-testid",
|
|
10575
|
+
"name",
|
|
10576
|
+
"aria-label",
|
|
10577
|
+
"title",
|
|
10578
|
+
"href"
|
|
10579
|
+
]) {
|
|
10580
|
+
const candidate2 = uniqueAttributeSelector(el, attribute);
|
|
10581
|
+
if (candidate2) return candidate2;
|
|
10582
|
+
}
|
|
10583
|
+
const parts = [];
|
|
10584
|
+
let current = el;
|
|
10585
|
+
while (current) {
|
|
10586
|
+
const currentEl = current;
|
|
10587
|
+
if (currentEl.id) {
|
|
10588
|
+
parts.unshift(`#${escapeSelectorValue(currentEl.id)}`);
|
|
10589
|
+
break;
|
|
10590
|
+
}
|
|
10591
|
+
const parent = current.parentElement;
|
|
10592
|
+
const tag = current.tagName.toLowerCase();
|
|
10593
|
+
if (!parent) {
|
|
10594
|
+
parts.unshift(tag);
|
|
10595
|
+
break;
|
|
10596
|
+
}
|
|
10597
|
+
const siblings = Array.from(parent.children).filter(
|
|
10598
|
+
(child) => child.tagName === current.tagName
|
|
10599
|
+
);
|
|
10600
|
+
const index = siblings.indexOf(current) + 1;
|
|
10601
|
+
parts.unshift(
|
|
10602
|
+
siblings.length > 1 ? `${tag}:nth-of-type(${index})` : tag
|
|
10603
|
+
);
|
|
10604
|
+
current = parent;
|
|
10605
|
+
}
|
|
10606
|
+
const candidate = parts.join(" > ");
|
|
10607
|
+
return uniqueSelector(candidate) || candidate;
|
|
10608
|
+
}
|
|
10609
|
+
function isVisible2(el) {
|
|
10610
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
10611
|
+
if (el.hidden || el.getAttribute("aria-hidden") === "true") return false;
|
|
10612
|
+
const style = typeof getComputedStyle === "function" ? getComputedStyle(el) : null;
|
|
10613
|
+
if (style && (style.display === "none" || style.visibility === "hidden" || style.opacity === "0")) {
|
|
10614
|
+
return false;
|
|
10615
|
+
}
|
|
10616
|
+
return true;
|
|
10617
|
+
}
|
|
10618
|
+
function inViewport(el) {
|
|
10619
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
10620
|
+
if (typeof el.getBoundingClientRect !== "function") return true;
|
|
10621
|
+
const rect = el.getBoundingClientRect();
|
|
10622
|
+
if (rect.width === 0 && rect.height === 0) return true;
|
|
10623
|
+
const vw = window.innerWidth || doc.documentElement?.clientWidth || 0;
|
|
10624
|
+
const vh = window.innerHeight || doc.documentElement?.clientHeight || 0;
|
|
10625
|
+
return rect.bottom > 0 && rect.right > 0 && rect.top < vh && rect.left < vw;
|
|
10626
|
+
}
|
|
10627
|
+
function labelFor(el) {
|
|
10628
|
+
const htmlEl = el;
|
|
10629
|
+
return text(
|
|
10630
|
+
htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || htmlEl.getAttribute("name") || htmlEl.getAttribute("placeholder") || ("value" in htmlEl ? String(htmlEl.value || "") : "") || htmlEl.textContent
|
|
10631
|
+
) || el.tagName.toLowerCase();
|
|
10632
|
+
}
|
|
10633
|
+
function contentFor(el) {
|
|
10634
|
+
const ariaLabel = text(el.getAttribute?.("aria-label"));
|
|
10635
|
+
const title = text(el.getAttribute?.("title"));
|
|
10636
|
+
const ownText = text(el.textContent).slice(0, 300);
|
|
10637
|
+
return [ariaLabel, title, ownText].filter(Boolean).join(" ");
|
|
10638
|
+
}
|
|
10639
|
+
function scoreText(query2, candidate) {
|
|
10640
|
+
const normalizedCandidate = normalize(candidate);
|
|
10641
|
+
if (!normalizedCandidate) return -1;
|
|
10642
|
+
if (normalizedCandidate === query2) return 180;
|
|
10643
|
+
if (normalizedCandidate.startsWith(query2)) return 150;
|
|
10644
|
+
if (normalizedCandidate.includes(query2)) {
|
|
10645
|
+
return 130 - Math.min(30, normalizedCandidate.length - query2.length);
|
|
10646
|
+
}
|
|
10647
|
+
const words = query2.split(" ").filter((word) => word.length >= 3);
|
|
10648
|
+
if (words.length === 0) return -1;
|
|
10649
|
+
const overlap = words.filter((word) => normalizedCandidate.includes(word));
|
|
10650
|
+
if (overlap.length === 0) return -1;
|
|
10651
|
+
return overlap.length * 18;
|
|
10652
|
+
}
|
|
10653
|
+
function interactiveBonus(el) {
|
|
10654
|
+
const htmlEl = el;
|
|
10655
|
+
const tag = el.tagName.toLowerCase();
|
|
10656
|
+
const label = normalize(labelFor(el));
|
|
10657
|
+
let score = 0;
|
|
10658
|
+
if (tag === "button") score += 40;
|
|
10659
|
+
if (tag === "a") score += 35;
|
|
10660
|
+
if (tag === "input") score += 20;
|
|
10661
|
+
if (htmlEl.getAttribute("role") === "button") score += 25;
|
|
10662
|
+
if (/\b(shop|view|see|explore|browse|open|details|collection|discover)\b/.test(label)) {
|
|
10663
|
+
score += 30;
|
|
10664
|
+
}
|
|
10665
|
+
if (inViewport(el)) score += 25;
|
|
10666
|
+
if (tag === "a") {
|
|
10667
|
+
const href = htmlEl.href || "";
|
|
10668
|
+
const filterParams = /\b(condition|binding|format|availability|sort|filter|price|category_id)\b=[^&]/i;
|
|
10669
|
+
const filterPath = /\/(condition|binding|format|availability|sort|filter|price|category)\/[^/?#]+/i;
|
|
10670
|
+
if (filterParams.test(href) || filterPath.test(href)) {
|
|
10671
|
+
score -= 40;
|
|
10672
|
+
}
|
|
10673
|
+
if (/\b(used|new|paperback|hardcover|hardback|ebook|kindle|refine|clear all|remove filter)\b/.test(label)) {
|
|
10674
|
+
score -= 30;
|
|
10675
|
+
}
|
|
10676
|
+
}
|
|
10677
|
+
return score;
|
|
10678
|
+
}
|
|
10679
|
+
function regionBonus(el) {
|
|
10680
|
+
const tag = el.tagName.toLowerCase();
|
|
10681
|
+
let score = 0;
|
|
10682
|
+
if (/^h[1-4]$/.test(tag)) score += 45;
|
|
10683
|
+
if (tag === "section" || tag === "article" || tag === "main") score += 30;
|
|
10684
|
+
if (el.getAttribute("role") === "heading") score += 35;
|
|
10685
|
+
if (inViewport(el)) score += 20;
|
|
10686
|
+
return score;
|
|
10687
|
+
}
|
|
10688
|
+
function consider(best, el, score, matchedText) {
|
|
10689
|
+
if (!Number.isFinite(score) || score < 0) return best;
|
|
10690
|
+
if (!best || score > best.score) return { el, score, matchedText };
|
|
10691
|
+
return best;
|
|
10692
|
+
}
|
|
10693
|
+
if (isInvalidTextTargetQuery(rawQuery)) return null;
|
|
10694
|
+
const query = normalize(rawQuery);
|
|
10695
|
+
if (!query) return null;
|
|
10696
|
+
let bestInteractive = null;
|
|
10697
|
+
const interactiveSelector = "a[href], button, [role='button'], input[type='submit'], input[type='button'], input[type='radio'], input[type='checkbox'], select, textarea";
|
|
10698
|
+
doc.querySelectorAll(interactiveSelector).forEach((el) => {
|
|
10699
|
+
if (!isVisible2(el)) return;
|
|
10700
|
+
const matchedText = labelFor(el);
|
|
10701
|
+
const score = scoreText(query, matchedText) + scoreText(query, contentFor(el)) + interactiveBonus(el);
|
|
10702
|
+
bestInteractive = consider(bestInteractive, el, score, matchedText);
|
|
10703
|
+
});
|
|
10704
|
+
if (mode === "interactive" && bestInteractive && bestInteractive.score >= 120) {
|
|
10705
|
+
const selector2 = selectorFor(bestInteractive.el);
|
|
10706
|
+
if (selector2) {
|
|
10707
|
+
return {
|
|
10708
|
+
selector: selector2,
|
|
10709
|
+
label: labelFor(bestInteractive.el),
|
|
10710
|
+
kind: bestInteractive.el.tagName.toLowerCase(),
|
|
10711
|
+
matchedText: bestInteractive.matchedText
|
|
10712
|
+
};
|
|
10713
|
+
}
|
|
10714
|
+
}
|
|
10715
|
+
let bestRegion = null;
|
|
10716
|
+
const regionSelector = "h1, h2, h3, h4, [role='heading'], section, article, main, aside, li, [data-testid], div";
|
|
10717
|
+
let seenRegions = 0;
|
|
10718
|
+
doc.querySelectorAll(regionSelector).forEach((el) => {
|
|
10719
|
+
if (seenRegions >= 400 || !isVisible2(el)) return;
|
|
10720
|
+
seenRegions += 1;
|
|
10721
|
+
const matchedText = contentFor(el);
|
|
10722
|
+
const score = scoreText(query, matchedText) + regionBonus(el);
|
|
10723
|
+
bestRegion = consider(bestRegion, el, score, matchedText);
|
|
10724
|
+
});
|
|
10725
|
+
if (!bestRegion || bestRegion.score < 80) {
|
|
10726
|
+
if (mode === "interactive" && bestInteractive) {
|
|
10727
|
+
const selector2 = selectorFor(bestInteractive.el);
|
|
10728
|
+
if (!selector2) return null;
|
|
10729
|
+
return {
|
|
10730
|
+
selector: selector2,
|
|
10731
|
+
label: labelFor(bestInteractive.el),
|
|
10732
|
+
kind: bestInteractive.el.tagName.toLowerCase(),
|
|
10733
|
+
matchedText: bestInteractive.matchedText
|
|
10734
|
+
};
|
|
10735
|
+
}
|
|
10736
|
+
return null;
|
|
10737
|
+
}
|
|
10738
|
+
if (mode === "context") {
|
|
10739
|
+
const selector2 = selectorFor(bestRegion.el);
|
|
10740
|
+
if (!selector2) return null;
|
|
10741
|
+
return {
|
|
10742
|
+
selector: selector2,
|
|
10743
|
+
label: labelFor(bestRegion.el),
|
|
10744
|
+
kind: bestRegion.el.tagName.toLowerCase(),
|
|
10745
|
+
matchedText: bestRegion.matchedText
|
|
10746
|
+
};
|
|
10747
|
+
}
|
|
10748
|
+
let regionAction = null;
|
|
10749
|
+
bestRegion.el.querySelectorAll(interactiveSelector).forEach((el) => {
|
|
10750
|
+
if (!isVisible2(el)) return;
|
|
10751
|
+
const matchedText = labelFor(el);
|
|
10752
|
+
const score = scoreText(query, matchedText) + interactiveBonus(el) + (bestRegion ? Math.floor(bestRegion.score / 4) : 0);
|
|
10753
|
+
regionAction = consider(regionAction, el, score, matchedText);
|
|
10754
|
+
});
|
|
10755
|
+
const chosen = regionAction || bestInteractive;
|
|
10756
|
+
if (!chosen) return null;
|
|
10757
|
+
const selector = selectorFor(chosen.el);
|
|
10758
|
+
if (!selector) return null;
|
|
10759
|
+
return {
|
|
10760
|
+
selector,
|
|
10761
|
+
label: labelFor(chosen.el),
|
|
10762
|
+
kind: chosen.el.tagName.toLowerCase(),
|
|
10763
|
+
matchedText: chosen.matchedText
|
|
10764
|
+
};
|
|
10765
|
+
}
|
|
10766
|
+
function normalizeWhitespace(text) {
|
|
10767
|
+
return text.replace(/\r/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
10768
|
+
}
|
|
10769
|
+
function limitText(text, maxLines, maxChars) {
|
|
10770
|
+
const normalized = normalizeWhitespace(text);
|
|
10771
|
+
const lines = normalized.split("\n");
|
|
10772
|
+
const trimmedLines = lines.slice(0, maxLines);
|
|
10773
|
+
let limited = trimmedLines.join("\n");
|
|
10774
|
+
if (limited.length > maxChars) {
|
|
10775
|
+
limited = `${limited.slice(0, maxChars - 1)}…`;
|
|
10776
|
+
} else if (lines.length > maxLines) {
|
|
10777
|
+
limited += "\n…";
|
|
10778
|
+
}
|
|
10779
|
+
return limited;
|
|
10780
|
+
}
|
|
10781
|
+
function extractSection(text, heading) {
|
|
10782
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10783
|
+
const match = text.match(
|
|
10784
|
+
new RegExp(`${escaped}\\n([\\s\\S]*?)(?=\\n### |\\n## |$)`)
|
|
10785
|
+
);
|
|
10786
|
+
if (!match) return null;
|
|
10787
|
+
return `${heading}
|
|
10788
|
+
${match[1].trim()}`;
|
|
10789
|
+
}
|
|
10790
|
+
function compactReadPageResult(text) {
|
|
10791
|
+
const cleaned = normalizeWhitespace(
|
|
10792
|
+
text.replace(
|
|
10793
|
+
/\n+Need more detail\? Escalate with read_page\(mode="debug"\) only if the narrow modes are insufficient\.\s*$/s,
|
|
10794
|
+
""
|
|
10795
|
+
)
|
|
10796
|
+
);
|
|
10797
|
+
const readHeader = cleaned.match(/^\[read_page mode=[^\]]+\]/m)?.[0];
|
|
10798
|
+
const mandatoryHeadings = [
|
|
10799
|
+
"### Action Status",
|
|
10800
|
+
"### Visible Purchase Controls",
|
|
10801
|
+
"### Offscreen Purchase Actions"
|
|
10802
|
+
];
|
|
10803
|
+
const optionalHeadings = [
|
|
10804
|
+
"### Access Warnings",
|
|
10805
|
+
"### Immediate Blockers",
|
|
10806
|
+
"### Likely Search Results",
|
|
10807
|
+
"### Primary Results",
|
|
10808
|
+
"### Form Fields",
|
|
10809
|
+
"### Visible Controls",
|
|
10810
|
+
"### Top Headings",
|
|
10811
|
+
"### Text Snapshot"
|
|
10812
|
+
];
|
|
10813
|
+
const mandatorySections = mandatoryHeadings.map((heading) => extractSection(cleaned, heading)).filter((value) => Boolean(value));
|
|
10814
|
+
const optionalSections = optionalHeadings.map((heading) => extractSection(cleaned, heading)).filter((value) => Boolean(value));
|
|
10815
|
+
const allSections = [...mandatorySections, ...optionalSections];
|
|
10816
|
+
if (allSections.length === 0) {
|
|
10817
|
+
return limitText(cleaned, 22, 1800);
|
|
10818
|
+
}
|
|
10819
|
+
const metaLines = cleaned.split("\n").filter((line) => /^\*\*(URL|Title|Page Type|Mode|Author):\*\*/.test(line)).slice(0, 5);
|
|
10820
|
+
const maxOptional = Math.max(0, 5 - mandatorySections.length);
|
|
10821
|
+
const keptSections = [
|
|
10822
|
+
...mandatorySections,
|
|
10823
|
+
...optionalSections.slice(0, maxOptional)
|
|
10824
|
+
];
|
|
10825
|
+
return [
|
|
10826
|
+
readHeader,
|
|
10827
|
+
metaLines.join("\n"),
|
|
10828
|
+
keptSections.join("\n\n")
|
|
10829
|
+
].filter(Boolean).join("\n\n");
|
|
10830
|
+
}
|
|
10831
|
+
function compactSearchLikeResult(text) {
|
|
10832
|
+
const cleaned = normalizeWhitespace(text);
|
|
10833
|
+
const marker = "\nSearch results snapshot:\n";
|
|
10834
|
+
const markerIndex = cleaned.indexOf(marker);
|
|
10835
|
+
if (markerIndex === -1) {
|
|
10836
|
+
return limitText(cleaned, 16, 1400);
|
|
10837
|
+
}
|
|
10838
|
+
const summary = cleaned.slice(0, markerIndex).trim();
|
|
10839
|
+
const snapshot = cleaned.slice(markerIndex + marker.length).trim();
|
|
10840
|
+
return [summary, compactReadPageResult(snapshot)].filter(Boolean).join("\n\n");
|
|
10841
|
+
}
|
|
10842
|
+
function compactCurrentTabResult(text) {
|
|
10843
|
+
try {
|
|
10844
|
+
const parsed = JSON.parse(text);
|
|
10845
|
+
if (!parsed || typeof parsed !== "object") return limitText(text, 8, 500);
|
|
10846
|
+
return [
|
|
10847
|
+
`Current tab: ${parsed.title || "(untitled)"}`,
|
|
10848
|
+
parsed.url ? `URL: ${parsed.url}` : "",
|
|
10849
|
+
`State: loading=${parsed.isLoading ? "yes" : "no"}, back=${parsed.canGoBack ? "yes" : "no"}, forward=${parsed.canGoForward ? "yes" : "no"}`
|
|
10850
|
+
].filter(Boolean).join("\n");
|
|
10851
|
+
} catch {
|
|
10852
|
+
return limitText(text, 8, 500);
|
|
10853
|
+
}
|
|
10854
|
+
}
|
|
10855
|
+
function looksLikeRichToolResult(text) {
|
|
10856
|
+
return text.startsWith("{") && text.includes('"__richResult":true');
|
|
10857
|
+
}
|
|
10858
|
+
function formatCompactToolResult(name, result) {
|
|
10859
|
+
if (!result || looksLikeRichToolResult(result)) return result;
|
|
10860
|
+
switch (name) {
|
|
10861
|
+
case "current_tab":
|
|
10862
|
+
return compactCurrentTabResult(result);
|
|
10863
|
+
case "read_page":
|
|
10864
|
+
return compactReadPageResult(result);
|
|
10865
|
+
case "search":
|
|
10866
|
+
case "navigate":
|
|
10867
|
+
case "go_back":
|
|
10868
|
+
case "go_forward":
|
|
10869
|
+
case "paginate":
|
|
10870
|
+
case "wait_for_navigation":
|
|
10871
|
+
return compactSearchLikeResult(result);
|
|
10872
|
+
case "list_tabs":
|
|
10873
|
+
return limitText(result, 10, 900);
|
|
10874
|
+
default:
|
|
10875
|
+
return limitText(result, 18, 1400);
|
|
10876
|
+
}
|
|
10877
|
+
}
|
|
9779
10878
|
const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS = 1500;
|
|
9780
10879
|
const QUIET_NAVIGATION_WINDOW_MS = 1200;
|
|
9781
10880
|
const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
|
|
@@ -9994,9 +11093,9 @@ function waitForLoad$1(wc, timeout = 5e3) {
|
|
|
9994
11093
|
finish();
|
|
9995
11094
|
return;
|
|
9996
11095
|
}
|
|
9997
|
-
wc.
|
|
9998
|
-
wc.
|
|
9999
|
-
wc.
|
|
11096
|
+
wc.once("did-finish-load", onLoadEvent);
|
|
11097
|
+
wc.once("did-stop-loading", onLoadEvent);
|
|
11098
|
+
wc.once("did-fail-load", onLoadEvent);
|
|
10000
11099
|
});
|
|
10001
11100
|
}
|
|
10002
11101
|
function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
|
|
@@ -10049,8 +11148,8 @@ function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
|
|
|
10049
11148
|
wc.once("did-start-loading", onStart);
|
|
10050
11149
|
wc.once("did-navigate", onNavigate);
|
|
10051
11150
|
wc.once("did-navigate-in-page", onNavigateInPage);
|
|
10052
|
-
wc.
|
|
10053
|
-
wc.
|
|
11151
|
+
wc.once("did-stop-loading", onNativeChange);
|
|
11152
|
+
wc.once("page-title-updated", onNativeChange);
|
|
10054
11153
|
});
|
|
10055
11154
|
}
|
|
10056
11155
|
async function getPostNavSummary(wc) {
|
|
@@ -10132,6 +11231,25 @@ ${truncated}`;
|
|
|
10132
11231
|
Search results snapshot unavailable. Use read_page(mode="results_only") if needed.` : `
|
|
10133
11232
|
Search results snapshot unavailable. Use read_page(mode="results_only") if needed.`;
|
|
10134
11233
|
}
|
|
11234
|
+
async function getPostClickNavSummary(wc, toolProfile) {
|
|
11235
|
+
try {
|
|
11236
|
+
const content = await Promise.race([
|
|
11237
|
+
extractContent(wc),
|
|
11238
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
|
|
11239
|
+
]);
|
|
11240
|
+
if (content && content.content.length > 0) {
|
|
11241
|
+
const scoped = toolProfile === "compact" ? buildCompactScopedContext(content, "visible_only") : buildScopedContext(content, "visible_only");
|
|
11242
|
+
const maxLen = toolProfile === "compact" ? 1800 : 3e3;
|
|
11243
|
+
const truncated = scoped.length > maxLen ? `${scoped.slice(0, maxLen)}
|
|
11244
|
+
[Page snapshot truncated. Use read_page for full details.]` : scoped;
|
|
11245
|
+
return `
|
|
11246
|
+
Page snapshot after navigation:
|
|
11247
|
+
${truncated}`;
|
|
11248
|
+
}
|
|
11249
|
+
} catch {
|
|
11250
|
+
}
|
|
11251
|
+
return "";
|
|
11252
|
+
}
|
|
10135
11253
|
async function scrollPage$1(wc, deltaY) {
|
|
10136
11254
|
const getScrollY = async () => {
|
|
10137
11255
|
const scrollY = await executePageScript(
|
|
@@ -10231,7 +11349,7 @@ async function clickElement$1(wc, selector) {
|
|
|
10231
11349
|
|
|
10232
11350
|
const rect = el.getBoundingClientRect();
|
|
10233
11351
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
10234
|
-
return { error: "Error[hidden]: Element has no visible area" };
|
|
11352
|
+
return { error: "Error[hidden]: Element has no visible area. It may be inside a collapsed, lazy-loaded, or virtual-scroll section. Scroll toward it (scroll or scroll_to_element) then call read_page to refresh visible elements before clicking again." };
|
|
10235
11353
|
}
|
|
10236
11354
|
|
|
10237
11355
|
const points = samplePoints(rect);
|
|
@@ -10324,9 +11442,17 @@ async function describeElementForClick$1(wc, selector) {
|
|
|
10324
11442
|
if (!el) return { error: "Element not found" };
|
|
10325
11443
|
const anchor = el instanceof HTMLAnchorElement ? el : el.closest("a[href]");
|
|
10326
11444
|
const text = (el.textContent || el.tagName || "Element").trim().slice(0, 100);
|
|
11445
|
+
const tag = el.tagName.toLowerCase();
|
|
11446
|
+
const interactiveTags = new Set(["a","button","input","select","textarea","summary","details","option"]);
|
|
11447
|
+
const hasRole = el.getAttribute("role") === "button" || el.getAttribute("role") === "link" || el.getAttribute("role") === "tab";
|
|
11448
|
+
const hasClickListener = el.onclick != null || el.getAttribute("onclick") != null;
|
|
11449
|
+
const isInteractive = interactiveTags.has(tag) || hasRole || hasClickListener || !!anchor;
|
|
10327
11450
|
return {
|
|
10328
11451
|
text: text || "Element",
|
|
10329
11452
|
href: anchor instanceof HTMLAnchorElement ? anchor.href : undefined,
|
|
11453
|
+
target: anchor instanceof HTMLAnchorElement ? (anchor.getAttribute("target") || "") : undefined,
|
|
11454
|
+
tag,
|
|
11455
|
+
isInteractive,
|
|
10330
11456
|
};
|
|
10331
11457
|
})()
|
|
10332
11458
|
`,
|
|
@@ -10345,7 +11471,10 @@ async function describeElementForClick$1(wc, selector) {
|
|
|
10345
11471
|
}
|
|
10346
11472
|
return {
|
|
10347
11473
|
text: "text" in result && typeof result.text === "string" ? result.text : "Element",
|
|
10348
|
-
href: "href" in result && typeof result.href === "string" ? result.href : void 0
|
|
11474
|
+
href: "href" in result && typeof result.href === "string" ? result.href : void 0,
|
|
11475
|
+
target: "target" in result && typeof result.target === "string" ? result.target : void 0,
|
|
11476
|
+
tag: "tag" in result && typeof result.tag === "string" ? result.tag : void 0,
|
|
11477
|
+
isInteractive: "isInteractive" in result && typeof result.isInteractive === "boolean" ? result.isInteractive : void 0
|
|
10349
11478
|
};
|
|
10350
11479
|
}
|
|
10351
11480
|
async function inspectElement(wc, selector, limit = 8) {
|
|
@@ -10429,6 +11558,18 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
10429
11558
|
) || "element";
|
|
10430
11559
|
}
|
|
10431
11560
|
|
|
11561
|
+
function purchasePriority(label, href) {
|
|
11562
|
+
const haystack = ((label || "") + " " + (href || ""))
|
|
11563
|
+
.toLowerCase()
|
|
11564
|
+
.replace(/s+/g, " ")
|
|
11565
|
+
.trim();
|
|
11566
|
+
if (!haystack) return null;
|
|
11567
|
+
if (/\badd(?: item)? to (?:cart|bag|basket)\b/.test(haystack)) return 0;
|
|
11568
|
+
if (/\b(?:buy now|preorder|pre-order|reserve now|shop now)\b/.test(haystack)) return 1;
|
|
11569
|
+
if (/\b(?:checkout|view cart|view basket|go to cart|view bag)\b/.test(haystack)) return 2;
|
|
11570
|
+
return null;
|
|
11571
|
+
}
|
|
11572
|
+
|
|
10432
11573
|
function chooseRegion(target) {
|
|
10433
11574
|
const preferred = target.closest(
|
|
10434
11575
|
"[data-testid], article, [role='article'], [role='listitem'], li, tr, form, section, aside, dialog, [role='dialog']"
|
|
@@ -10454,19 +11595,69 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
10454
11595
|
const region = chooseRegion(target);
|
|
10455
11596
|
const nearby = [];
|
|
10456
11597
|
const seen = new Set();
|
|
11598
|
+
const purchaseActions = [];
|
|
11599
|
+
const purchaseSeen = new Set();
|
|
10457
11600
|
region.querySelectorAll("a[href], button, input:not([type='hidden']), select, textarea").forEach((el) => {
|
|
10458
11601
|
if (!(el instanceof HTMLElement) || !isVisible(el)) return;
|
|
10459
11602
|
const candidateSelector = selectorFor(el);
|
|
10460
11603
|
if (!candidateSelector || seen.has(candidateSelector)) return;
|
|
10461
11604
|
seen.add(candidateSelector);
|
|
11605
|
+
const candidateLabel = labelFor(el).slice(0, 100);
|
|
11606
|
+
const candidateHref = el instanceof HTMLAnchorElement ? text(el.href) : undefined;
|
|
10462
11607
|
nearby.push({
|
|
10463
|
-
|
|
11608
|
+
index: typeof window.__vessel?.getElementIndexBySelector === "function"
|
|
11609
|
+
? window.__vessel.getElementIndexBySelector(candidateSelector) ?? undefined
|
|
11610
|
+
: undefined,
|
|
11611
|
+
label: candidateLabel,
|
|
11612
|
+
type: el.tagName.toLowerCase(),
|
|
11613
|
+
selector: candidateSelector,
|
|
11614
|
+
href: candidateHref,
|
|
11615
|
+
});
|
|
11616
|
+
const purchaseRank = purchasePriority(candidateLabel, candidateHref);
|
|
11617
|
+
if (purchaseRank !== null && !purchaseSeen.has(candidateSelector)) {
|
|
11618
|
+
purchaseSeen.add(candidateSelector);
|
|
11619
|
+
purchaseActions.push({
|
|
11620
|
+
index: typeof window.__vessel?.getElementIndexBySelector === "function"
|
|
11621
|
+
? window.__vessel.getElementIndexBySelector(candidateSelector) ?? undefined
|
|
11622
|
+
: undefined,
|
|
11623
|
+
label: candidateLabel,
|
|
11624
|
+
type: el.tagName.toLowerCase(),
|
|
11625
|
+
selector: candidateSelector,
|
|
11626
|
+
href: candidateHref,
|
|
11627
|
+
source: "nearby",
|
|
11628
|
+
rank: purchaseRank,
|
|
11629
|
+
});
|
|
11630
|
+
}
|
|
11631
|
+
});
|
|
11632
|
+
|
|
11633
|
+
document.querySelectorAll("button, a[href], input[type='submit'], input[type='button']").forEach((el) => {
|
|
11634
|
+
if (!(el instanceof HTMLElement) || !isVisible(el)) return;
|
|
11635
|
+
const candidateSelector = selectorFor(el);
|
|
11636
|
+
if (!candidateSelector || purchaseSeen.has(candidateSelector)) return;
|
|
11637
|
+
const candidateLabel = labelFor(el).slice(0, 100);
|
|
11638
|
+
const candidateHref = el instanceof HTMLAnchorElement ? text(el.href) : undefined;
|
|
11639
|
+
const purchaseRank = purchasePriority(candidateLabel, candidateHref);
|
|
11640
|
+
if (purchaseRank === null) return;
|
|
11641
|
+
purchaseSeen.add(candidateSelector);
|
|
11642
|
+
purchaseActions.push({
|
|
11643
|
+
index: typeof window.__vessel?.getElementIndexBySelector === "function"
|
|
11644
|
+
? window.__vessel.getElementIndexBySelector(candidateSelector) ?? undefined
|
|
11645
|
+
: undefined,
|
|
11646
|
+
label: candidateLabel,
|
|
10464
11647
|
type: el.tagName.toLowerCase(),
|
|
10465
11648
|
selector: candidateSelector,
|
|
10466
|
-
href:
|
|
11649
|
+
href: candidateHref,
|
|
11650
|
+
source: "page",
|
|
11651
|
+
rank: purchaseRank,
|
|
10467
11652
|
});
|
|
10468
11653
|
});
|
|
10469
11654
|
|
|
11655
|
+
purchaseActions.sort((a, b) => {
|
|
11656
|
+
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
11657
|
+
if (a.source !== b.source) return a.source === "nearby" ? -1 : 1;
|
|
11658
|
+
return a.label.localeCompare(b.label);
|
|
11659
|
+
});
|
|
11660
|
+
|
|
10470
11661
|
return {
|
|
10471
11662
|
target: {
|
|
10472
11663
|
label: labelFor(target).slice(0, 120),
|
|
@@ -10483,6 +11674,14 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
10483
11674
|
text: text(region.textContent)?.slice(0, 400),
|
|
10484
11675
|
},
|
|
10485
11676
|
nearby: nearby.slice(0, ${Math.max(1, Math.min(20, limit))}),
|
|
11677
|
+
purchaseActions: purchaseActions.slice(0, 8).map((item) => ({
|
|
11678
|
+
index: item.index,
|
|
11679
|
+
label: item.label,
|
|
11680
|
+
type: item.type,
|
|
11681
|
+
selector: item.selector,
|
|
11682
|
+
href: item.href,
|
|
11683
|
+
source: item.source,
|
|
11684
|
+
})),
|
|
10486
11685
|
};
|
|
10487
11686
|
})()
|
|
10488
11687
|
`,
|
|
@@ -10515,10 +11714,25 @@ async function inspectElement(wc, selector, limit = 8) {
|
|
|
10515
11714
|
lines.push("Nearby controls:");
|
|
10516
11715
|
for (const item of result.nearby) {
|
|
10517
11716
|
const hrefSuffix = item.href ? ` -> ${item.href}` : "";
|
|
11717
|
+
const indexPrefix = typeof item.index === "number" ? `[#${item.index}] ` : "";
|
|
11718
|
+
lines.push(
|
|
11719
|
+
`- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
|
|
11720
|
+
);
|
|
11721
|
+
}
|
|
11722
|
+
}
|
|
11723
|
+
if (Array.isArray(result.purchaseActions) && result.purchaseActions.length > 0) {
|
|
11724
|
+
lines.push("Likely purchase actions:");
|
|
11725
|
+
for (const item of result.purchaseActions) {
|
|
11726
|
+
const hrefSuffix = item.href ? ` -> ${item.href}` : "";
|
|
11727
|
+
const sourceSuffix = item.source === "nearby" ? " (same region)" : " (elsewhere on page)";
|
|
11728
|
+
const indexPrefix = typeof item.index === "number" ? `[#${item.index}] ` : "";
|
|
10518
11729
|
lines.push(
|
|
10519
|
-
`- ${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
|
|
11730
|
+
`- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}${sourceSuffix}`
|
|
10520
11731
|
);
|
|
10521
11732
|
}
|
|
11733
|
+
lines.push(
|
|
11734
|
+
"When an index is available, prefer click(index=N) over selector-based clicks because it is more stable."
|
|
11735
|
+
);
|
|
10522
11736
|
}
|
|
10523
11737
|
return lines.join("\n");
|
|
10524
11738
|
}
|
|
@@ -10607,6 +11821,11 @@ const ADD_TO_CART_PATTERNS = [
|
|
|
10607
11821
|
];
|
|
10608
11822
|
const recentCartClicks = /* @__PURE__ */ new Map();
|
|
10609
11823
|
const CART_CLICK_COOLDOWN_MS = 15e3;
|
|
11824
|
+
const CART_ADDED_TTL_MS = 30 * 6e4;
|
|
11825
|
+
const cartAddedProducts = /* @__PURE__ */ new Map();
|
|
11826
|
+
let clickStreakUrl = null;
|
|
11827
|
+
let clickStreakCount = 0;
|
|
11828
|
+
const CLICK_STREAK_THRESHOLD = 3;
|
|
10610
11829
|
function isAddToCartText(text) {
|
|
10611
11830
|
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
10612
11831
|
return ADD_TO_CART_PATTERNS.some((p) => normalized.includes(p));
|
|
@@ -10628,18 +11847,119 @@ function isDuplicateCartClick(url, text) {
|
|
|
10628
11847
|
}
|
|
10629
11848
|
return isAddToCartText(text);
|
|
10630
11849
|
}
|
|
11850
|
+
async function getProductPageTitle(wc) {
|
|
11851
|
+
try {
|
|
11852
|
+
const heading = await executePageScript(
|
|
11853
|
+
wc,
|
|
11854
|
+
`(function() {
|
|
11855
|
+
var h1 = document.querySelector('h1');
|
|
11856
|
+
if (h1 && h1.textContent.trim().length > 3 && h1.textContent.trim().length < 200) {
|
|
11857
|
+
return h1.textContent.trim();
|
|
11858
|
+
}
|
|
11859
|
+
var meta = document.querySelector('meta[property="og:title"]');
|
|
11860
|
+
if (meta && meta.content && meta.content.trim().length > 3) {
|
|
11861
|
+
return meta.content.trim();
|
|
11862
|
+
}
|
|
11863
|
+
return '';
|
|
11864
|
+
})()`,
|
|
11865
|
+
{ timeoutMs: 800, label: "get product title" }
|
|
11866
|
+
);
|
|
11867
|
+
if (heading && heading !== PAGE_SCRIPT_TIMEOUT && typeof heading === "string" && heading.length > 0) {
|
|
11868
|
+
return heading;
|
|
11869
|
+
}
|
|
11870
|
+
} catch {
|
|
11871
|
+
}
|
|
11872
|
+
return wc.getTitle() || "";
|
|
11873
|
+
}
|
|
11874
|
+
function normalizeCartProductKey(url) {
|
|
11875
|
+
try {
|
|
11876
|
+
const parsed = new URL(url);
|
|
11877
|
+
const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
|
|
11878
|
+
return `${parsed.origin}${pathname}`;
|
|
11879
|
+
} catch {
|
|
11880
|
+
return url;
|
|
11881
|
+
}
|
|
11882
|
+
}
|
|
11883
|
+
function pruneCartAddedProducts(now = Date.now()) {
|
|
11884
|
+
for (const [key, entry] of cartAddedProducts) {
|
|
11885
|
+
if (now - entry.ts > CART_ADDED_TTL_MS) {
|
|
11886
|
+
cartAddedProducts.delete(key);
|
|
11887
|
+
}
|
|
11888
|
+
}
|
|
11889
|
+
}
|
|
11890
|
+
function cartOrigin(url) {
|
|
11891
|
+
if (!url) return null;
|
|
11892
|
+
try {
|
|
11893
|
+
return new URL(url).origin;
|
|
11894
|
+
} catch {
|
|
11895
|
+
return null;
|
|
11896
|
+
}
|
|
11897
|
+
}
|
|
11898
|
+
function recordProductAddedToCart(url, productName) {
|
|
11899
|
+
pruneCartAddedProducts();
|
|
11900
|
+
cartAddedProducts.set(normalizeCartProductKey(url), {
|
|
11901
|
+
title: productName || url,
|
|
11902
|
+
ts: Date.now()
|
|
11903
|
+
});
|
|
11904
|
+
}
|
|
11905
|
+
function isProductAlreadyInCart(url) {
|
|
11906
|
+
pruneCartAddedProducts();
|
|
11907
|
+
return cartAddedProducts.has(normalizeCartProductKey(url));
|
|
11908
|
+
}
|
|
11909
|
+
function getCartAddedSummary(url) {
|
|
11910
|
+
pruneCartAddedProducts();
|
|
11911
|
+
const origin = cartOrigin(url);
|
|
11912
|
+
const items = Array.from(cartAddedProducts.entries()).filter(([key]) => !origin || key.startsWith(`${origin}/`)).map(([_path, info]) => `- ${info.title}`).join("\n");
|
|
11913
|
+
if (!items) return "";
|
|
11914
|
+
const count = items.split("\n").length;
|
|
11915
|
+
return `
|
|
11916
|
+
Already in cart (${count} items):
|
|
11917
|
+
${items}`;
|
|
11918
|
+
}
|
|
11919
|
+
function clearCartState() {
|
|
11920
|
+
cartAddedProducts.clear();
|
|
11921
|
+
recentCartClicks.clear();
|
|
11922
|
+
clickStreakUrl = null;
|
|
11923
|
+
clickStreakCount = 0;
|
|
11924
|
+
}
|
|
11925
|
+
async function buildCartSuccessSuffix(wc, productUrl, overlayHint) {
|
|
11926
|
+
const productTitle = await getProductPageTitle(wc);
|
|
11927
|
+
recordProductAddedToCart(productUrl, productTitle);
|
|
11928
|
+
const cartSummary = getCartAddedSummary(productUrl);
|
|
11929
|
+
const dismissResult = await tryAutoDismissCartDialog(wc);
|
|
11930
|
+
if (dismissResult) {
|
|
11931
|
+
return `
|
|
11932
|
+
Item added to cart. ${dismissResult}${cartSummary}
|
|
11933
|
+
Go back to search results to select the next product.`;
|
|
11934
|
+
}
|
|
11935
|
+
if (!overlayHint) {
|
|
11936
|
+
return cartSummary;
|
|
11937
|
+
}
|
|
11938
|
+
const dialogActions = await getCartDialogActions$1(wc);
|
|
11939
|
+
const actionsSuffix = dialogActions ? `
|
|
11940
|
+
${dialogActions}
|
|
11941
|
+
Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
11942
|
+
return `
|
|
11943
|
+
${overlayHint}${actionsSuffix}${cartSummary}`;
|
|
11944
|
+
}
|
|
10631
11945
|
async function clickResolvedSelector$1(wc, selector) {
|
|
10632
11946
|
if (selector.startsWith("__vessel_idx:")) {
|
|
10633
11947
|
const idx = Number(selector.slice("__vessel_idx:".length));
|
|
10634
11948
|
const beforeUrl2 = wc.getURL();
|
|
11949
|
+
let idxCartMatch = false;
|
|
10635
11950
|
const idxLabel = await executePageScript(
|
|
10636
11951
|
wc,
|
|
10637
11952
|
`window.__vessel?.getElementText?.(${idx}) || ""`,
|
|
10638
11953
|
{ label: "shadow element text" }
|
|
10639
11954
|
);
|
|
10640
|
-
if (typeof idxLabel === "string" && isAddToCartText(idxLabel) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
|
|
11955
|
+
if (typeof idxLabel === "string" && (idxCartMatch = isAddToCartText(idxLabel)) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
|
|
10641
11956
|
return `Blocked: "${idxLabel}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
|
|
10642
11957
|
}
|
|
11958
|
+
if (idxCartMatch && isProductAlreadyInCart(beforeUrl2)) {
|
|
11959
|
+
const summary = getCartAddedSummary(beforeUrl2);
|
|
11960
|
+
return `Blocked: This product was already added to the cart.${summary}
|
|
11961
|
+
Go back and select a different product.`;
|
|
11962
|
+
}
|
|
10643
11963
|
const result = await executePageScript(
|
|
10644
11964
|
wc,
|
|
10645
11965
|
`window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`,
|
|
@@ -10649,18 +11969,40 @@ async function clickResolvedSelector$1(wc, selector) {
|
|
|
10649
11969
|
);
|
|
10650
11970
|
if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
|
|
10651
11971
|
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
10652
|
-
if (
|
|
11972
|
+
if (idxCartMatch) {
|
|
10653
11973
|
recordCartClick(beforeUrl2, idxLabel);
|
|
10654
11974
|
}
|
|
10655
11975
|
await waitForPotentialNavigation$1(wc, beforeUrl2);
|
|
10656
11976
|
const afterUrl2 = wc.getURL();
|
|
10657
11977
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
10658
|
-
|
|
11978
|
+
let idxOverlay = await detectPostClickOverlay$1(wc);
|
|
11979
|
+
if (!idxOverlay && idxCartMatch) {
|
|
11980
|
+
await sleep$1(1200);
|
|
11981
|
+
idxOverlay = await detectPostClickOverlay$1(wc);
|
|
11982
|
+
}
|
|
11983
|
+
if (idxCartMatch) {
|
|
11984
|
+
return `${result}${await buildCartSuccessSuffix(wc, beforeUrl2, idxOverlay)}`;
|
|
11985
|
+
}
|
|
11986
|
+
if (!idxOverlay) {
|
|
11987
|
+
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
11988
|
+
if (hrefMatch) {
|
|
11989
|
+
try {
|
|
11990
|
+
assertSafeURL(hrefMatch[1]);
|
|
11991
|
+
await wc.loadURL(hrefMatch[1]);
|
|
11992
|
+
await waitForLoad$1(wc, 8e3);
|
|
11993
|
+
const hrefUrl = wc.getURL();
|
|
11994
|
+
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
11995
|
+
} catch {
|
|
11996
|
+
}
|
|
11997
|
+
}
|
|
11998
|
+
}
|
|
10659
11999
|
return idxOverlay ? `${result}
|
|
10660
|
-
${idxOverlay}` : result
|
|
12000
|
+
${idxOverlay}` : `${result}
|
|
12001
|
+
Note: Page did not change after click.`;
|
|
10661
12002
|
}
|
|
10662
12003
|
if (selector.includes(" >>> ")) {
|
|
10663
12004
|
const beforeUrl2 = wc.getURL();
|
|
12005
|
+
let shadowCartMatch = false;
|
|
10664
12006
|
const shadowLabel = await executePageScript(
|
|
10665
12007
|
wc,
|
|
10666
12008
|
`(function() {
|
|
@@ -10669,17 +12011,24 @@ ${idxOverlay}` : result;
|
|
|
10669
12011
|
})()`,
|
|
10670
12012
|
{ label: "shadow element text" }
|
|
10671
12013
|
);
|
|
10672
|
-
if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
|
|
12014
|
+
if (typeof shadowLabel === "string" && (shadowCartMatch = isAddToCartText(shadowLabel)) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
|
|
10673
12015
|
return `Blocked: "${shadowLabel}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
|
|
10674
12016
|
}
|
|
12017
|
+
if (shadowCartMatch && isProductAlreadyInCart(beforeUrl2)) {
|
|
12018
|
+
const summary = getCartAddedSummary(beforeUrl2);
|
|
12019
|
+
return `Blocked: This product was already added to the cart.${summary}
|
|
12020
|
+
Go back and select a different product.`;
|
|
12021
|
+
}
|
|
10675
12022
|
const result = await executePageScript(
|
|
10676
12023
|
wc,
|
|
10677
12024
|
`
|
|
10678
12025
|
(function() {
|
|
10679
12026
|
var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
|
|
10680
|
-
if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
12027
|
+
if (!el || !document.contains(el)) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
10681
12028
|
if (el instanceof HTMLElement) { el.focus(); el.click(); }
|
|
10682
|
-
|
|
12029
|
+
var anchor = el instanceof HTMLAnchorElement ? el : el.closest('a[href]');
|
|
12030
|
+
var href = anchor instanceof HTMLAnchorElement ? anchor.href : null;
|
|
12031
|
+
return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase()) + (href ? "\\nhref: " + href : "");
|
|
10683
12032
|
})()
|
|
10684
12033
|
`,
|
|
10685
12034
|
{
|
|
@@ -10688,15 +12037,36 @@ ${idxOverlay}` : result;
|
|
|
10688
12037
|
);
|
|
10689
12038
|
if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
|
|
10690
12039
|
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
10691
|
-
if (
|
|
12040
|
+
if (shadowCartMatch) {
|
|
10692
12041
|
recordCartClick(beforeUrl2, shadowLabel);
|
|
10693
12042
|
}
|
|
10694
12043
|
await waitForPotentialNavigation$1(wc, beforeUrl2);
|
|
10695
12044
|
const afterUrl2 = wc.getURL();
|
|
10696
12045
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
10697
|
-
|
|
12046
|
+
let shadowOverlay = await detectPostClickOverlay$1(wc);
|
|
12047
|
+
if (!shadowOverlay && shadowCartMatch) {
|
|
12048
|
+
await sleep$1(1200);
|
|
12049
|
+
shadowOverlay = await detectPostClickOverlay$1(wc);
|
|
12050
|
+
}
|
|
12051
|
+
if (shadowCartMatch) {
|
|
12052
|
+
return `${result}${await buildCartSuccessSuffix(wc, beforeUrl2, shadowOverlay)}`;
|
|
12053
|
+
}
|
|
12054
|
+
if (!shadowOverlay) {
|
|
12055
|
+
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
12056
|
+
if (hrefMatch) {
|
|
12057
|
+
try {
|
|
12058
|
+
assertSafeURL(hrefMatch[1]);
|
|
12059
|
+
await wc.loadURL(hrefMatch[1]);
|
|
12060
|
+
await waitForLoad$1(wc, 8e3);
|
|
12061
|
+
const hrefUrl = wc.getURL();
|
|
12062
|
+
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
12063
|
+
} catch {
|
|
12064
|
+
}
|
|
12065
|
+
}
|
|
12066
|
+
}
|
|
10698
12067
|
return shadowOverlay ? `${result}
|
|
10699
|
-
${shadowOverlay}` : result
|
|
12068
|
+
${shadowOverlay}` : `${result}
|
|
12069
|
+
Note: Page did not change after click.`;
|
|
10700
12070
|
}
|
|
10701
12071
|
const beforeUrl = wc.getURL();
|
|
10702
12072
|
const elInfo = await describeElementForClick$1(wc, selector);
|
|
@@ -10719,10 +12089,16 @@ Click one of these dialog actions instead.`;
|
|
|
10719
12089
|
return formatDeadLinkMessage(elInfo.text, validation);
|
|
10720
12090
|
}
|
|
10721
12091
|
}
|
|
12092
|
+
if (cartMatch && isProductAlreadyInCart(beforeUrl)) {
|
|
12093
|
+
const summary = getCartAddedSummary(beforeUrl);
|
|
12094
|
+
return `Blocked: This product was already added to the cart.${summary}
|
|
12095
|
+
Go back and select a different product.`;
|
|
12096
|
+
}
|
|
10722
12097
|
if (cartMatch) {
|
|
10723
12098
|
recordCartClick(beforeUrl, elInfo.text);
|
|
10724
12099
|
}
|
|
10725
|
-
const
|
|
12100
|
+
const tagLabel = elInfo.tag && elInfo.tag !== "a" && elInfo.tag !== "button" ? ` <${elInfo.tag}>` : "";
|
|
12101
|
+
const clickText = `Clicked: ${elInfo.text}${tagLabel}`;
|
|
10726
12102
|
const clickResult = await clickElement$1(wc, selector);
|
|
10727
12103
|
if (clickResult.startsWith("Error:")) return clickResult;
|
|
10728
12104
|
await waitForPotentialNavigation$1(wc, beforeUrl);
|
|
@@ -10732,15 +12108,30 @@ Click one of these dialog actions instead.`;
|
|
|
10732
12108
|
}
|
|
10733
12109
|
const overlayHint = await detectPostClickOverlay$1(wc);
|
|
10734
12110
|
if (overlayHint) {
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
12111
|
+
if (cartMatch) {
|
|
12112
|
+
return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
|
|
12113
|
+
wc,
|
|
12114
|
+
beforeUrl,
|
|
12115
|
+
overlayHint
|
|
12116
|
+
)}`;
|
|
12117
|
+
}
|
|
10739
12118
|
return `${clickText} (${clickResult})
|
|
10740
|
-
${overlayHint}
|
|
12119
|
+
${overlayHint}`;
|
|
10741
12120
|
}
|
|
10742
12121
|
if (cartMatch) {
|
|
10743
|
-
|
|
12122
|
+
await sleep$1(1200);
|
|
12123
|
+
const delayedOverlayHint = await detectPostClickOverlay$1(wc);
|
|
12124
|
+
if (delayedOverlayHint) {
|
|
12125
|
+
return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
|
|
12126
|
+
wc,
|
|
12127
|
+
beforeUrl,
|
|
12128
|
+
delayedOverlayHint
|
|
12129
|
+
)}`;
|
|
12130
|
+
}
|
|
12131
|
+
return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
|
|
12132
|
+
wc,
|
|
12133
|
+
beforeUrl
|
|
12134
|
+
)}`;
|
|
10744
12135
|
}
|
|
10745
12136
|
const activationResult = await activateElement$1(wc, selector);
|
|
10746
12137
|
if (!activationResult.startsWith("Error:")) {
|
|
@@ -10755,7 +12146,65 @@ ${overlayHint}${actionsSuffix}`;
|
|
|
10755
12146
|
return `${clickText} (${clickResult})
|
|
10756
12147
|
${postActivationOverlayHint}`;
|
|
10757
12148
|
}
|
|
10758
|
-
|
|
12149
|
+
const sameTabLinkTarget = typeof elInfo.href === "string" && elInfo.href.trim().length > 0 && (!elInfo.target || !/^_blank$/i.test(elInfo.target.trim()));
|
|
12150
|
+
if (sameTabLinkTarget) {
|
|
12151
|
+
const validation = await validateLinkDestination(elInfo.href);
|
|
12152
|
+
if (validation.status !== "dead") {
|
|
12153
|
+
try {
|
|
12154
|
+
assertSafeURL(elInfo.href);
|
|
12155
|
+
await wc.loadURL(elInfo.href);
|
|
12156
|
+
await waitForLoad$1(wc, 8e3);
|
|
12157
|
+
const hrefFallbackUrl = wc.getURL();
|
|
12158
|
+
if (hrefFallbackUrl !== beforeUrl) {
|
|
12159
|
+
return `${clickText} -> ${hrefFallbackUrl} (recovered via href fallback)`;
|
|
12160
|
+
}
|
|
12161
|
+
} catch {
|
|
12162
|
+
}
|
|
12163
|
+
}
|
|
12164
|
+
}
|
|
12165
|
+
const nonInteractiveWarning = elInfo.isInteractive === false && !elInfo.href ? `
|
|
12166
|
+
Note: The clicked element (<${elInfo.tag || "unknown"}>) is not a link or button. Nothing happened. Try clicking the actual link element nearby or use read_page to find the correct interactive element.` : `
|
|
12167
|
+
Note: Page did not change after click. The element may need a different interaction method. Consider read_page or inspect_element.`;
|
|
12168
|
+
return `${clickText} (${clickResult})${nonInteractiveWarning}`;
|
|
12169
|
+
}
|
|
12170
|
+
async function tryAutoDismissCartDialog(wc) {
|
|
12171
|
+
try {
|
|
12172
|
+
const result = await executePageScript(
|
|
12173
|
+
wc,
|
|
12174
|
+
`
|
|
12175
|
+
(function() {
|
|
12176
|
+
var dialog = document.querySelector('[role="dialog"], dialog[open], [role="alertdialog"], [aria-modal="true"]');
|
|
12177
|
+
if (!dialog) return null;
|
|
12178
|
+
var cs = getComputedStyle(dialog);
|
|
12179
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') return null;
|
|
12180
|
+
var buttons = dialog.querySelectorAll('button, a[href], [role="button"]');
|
|
12181
|
+
var continueBtn = null;
|
|
12182
|
+
var closeBtn = null;
|
|
12183
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
12184
|
+
var label = (buttons[i].getAttribute('aria-label') || buttons[i].textContent || '').trim().toLowerCase();
|
|
12185
|
+
if (/continue shopping|keep shopping|back to shopping/.test(label)) { continueBtn = buttons[i]; break; }
|
|
12186
|
+
if (/close|dismiss|×/.test(label) && !closeBtn) { closeBtn = buttons[i]; }
|
|
12187
|
+
}
|
|
12188
|
+
var target = continueBtn || closeBtn;
|
|
12189
|
+
if (!target) return null;
|
|
12190
|
+
var actionLabel = (target.getAttribute('aria-label') || target.textContent || '').trim();
|
|
12191
|
+
if (target.tagName === 'A' && target.href) {
|
|
12192
|
+
window.location.href = target.href;
|
|
12193
|
+
return "Navigated via: " + actionLabel;
|
|
12194
|
+
}
|
|
12195
|
+
target.click();
|
|
12196
|
+
return "Dismissed dialog via: " + actionLabel;
|
|
12197
|
+
})()
|
|
12198
|
+
`,
|
|
12199
|
+
{ timeoutMs: 1500, label: "auto dismiss cart dialog" }
|
|
12200
|
+
);
|
|
12201
|
+
if (result && result !== PAGE_SCRIPT_TIMEOUT && typeof result === "string") {
|
|
12202
|
+
await sleep$1(500);
|
|
12203
|
+
return result;
|
|
12204
|
+
}
|
|
12205
|
+
} catch {
|
|
12206
|
+
}
|
|
12207
|
+
return null;
|
|
10759
12208
|
}
|
|
10760
12209
|
async function getCartDialogActions$1(wc) {
|
|
10761
12210
|
const result = await executePageScript(
|
|
@@ -11561,6 +13010,24 @@ async function resolveSelector$1(wc, index, selector) {
|
|
|
11561
13010
|
if (extractedSelector) return extractedSelector;
|
|
11562
13011
|
return null;
|
|
11563
13012
|
}
|
|
13013
|
+
async function resolveTargetByText(wc, query, mode) {
|
|
13014
|
+
const trimmed = query.trim();
|
|
13015
|
+
if (!trimmed) return null;
|
|
13016
|
+
if (isInvalidTextTargetQuery(trimmed)) return null;
|
|
13017
|
+
const result = await executePageScript(
|
|
13018
|
+
wc,
|
|
13019
|
+
`(${resolveTextTargetInDocument.toString()})(document, ${JSON.stringify(trimmed)}, ${JSON.stringify(mode)})`,
|
|
13020
|
+
{
|
|
13021
|
+
timeoutMs: 2200,
|
|
13022
|
+
label: `resolve ${mode} target by text`
|
|
13023
|
+
}
|
|
13024
|
+
);
|
|
13025
|
+
if (result === PAGE_SCRIPT_TIMEOUT) return PAGE_SCRIPT_TIMEOUT;
|
|
13026
|
+
if (!result || typeof result.selector !== "string" || !result.selector) {
|
|
13027
|
+
return null;
|
|
13028
|
+
}
|
|
13029
|
+
return result.selector;
|
|
13030
|
+
}
|
|
11564
13031
|
function normalizeFieldToken(value) {
|
|
11565
13032
|
return typeof value === "string" ? value.trim() : "";
|
|
11566
13033
|
}
|
|
@@ -13011,8 +14478,61 @@ async function getPostActionState$1(ctx, name) {
|
|
|
13011
14478
|
if (wc.isLoading()) {
|
|
13012
14479
|
await waitForLoad$1(wc);
|
|
13013
14480
|
}
|
|
14481
|
+
const currentUrl = wc.getURL();
|
|
14482
|
+
let warnings = "";
|
|
14483
|
+
if (isProductAlreadyInCart(currentUrl)) {
|
|
14484
|
+
warnings += `
|
|
14485
|
+
WARNING: This product is already in your cart.${getCartAddedSummary(currentUrl)}
|
|
14486
|
+
Go back and select a different product.`;
|
|
14487
|
+
}
|
|
14488
|
+
const taskGoal = ctx.runtime.getState().taskTracker?.goal;
|
|
14489
|
+
if (taskGoal && name === "click") {
|
|
14490
|
+
const drift = shouldBlockOffGoalDomainNavigation(taskGoal, currentUrl);
|
|
14491
|
+
if (drift) {
|
|
14492
|
+
warnings += `
|
|
14493
|
+
WARNING: You drifted to ${drift.targetDomain} but the task requires staying on ${drift.requestedDomain}. Call go_back immediately to return to the previous page.`;
|
|
14494
|
+
}
|
|
14495
|
+
}
|
|
14496
|
+
if (name === "go_back" || name === "search") {
|
|
14497
|
+
const cartSummary = getCartAddedSummary(currentUrl);
|
|
14498
|
+
if (cartSummary) {
|
|
14499
|
+
warnings += `${cartSummary}
|
|
14500
|
+
Select a DIFFERENT product that is not in the cart. Call read_page if needed to see available results.`;
|
|
14501
|
+
}
|
|
14502
|
+
if (ctx.toolProfile === "compact" && name === "go_back") {
|
|
14503
|
+
warnings += `
|
|
14504
|
+
Call read_page(mode="results_only") to see available products before clicking.`;
|
|
14505
|
+
}
|
|
14506
|
+
}
|
|
14507
|
+
if (name === "click" && ctx.toolProfile === "compact") {
|
|
14508
|
+
const filterParams = /\b(condition|binding|format|availability|sort|filter|price|category_id|view)\b=[^&]/i;
|
|
14509
|
+
const filterPath = /\/(condition|binding|format|availability|sort|filter|price|category)\/[^/?#]+/i;
|
|
14510
|
+
if (filterParams.test(currentUrl) || filterPath.test(currentUrl)) {
|
|
14511
|
+
warnings += `
|
|
14512
|
+
WARNING: The clicked link appears to be a filter or sort control, not a product. If you intended to click a product, call go_back and use click(index=N) on a result from read_page(mode="results_only").`;
|
|
14513
|
+
}
|
|
14514
|
+
}
|
|
13014
14515
|
return `
|
|
13015
|
-
[state: url=${
|
|
14516
|
+
[state: url=${currentUrl}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]${warnings}`;
|
|
14517
|
+
}
|
|
14518
|
+
if (name === "click" && !wc.isLoading()) {
|
|
14519
|
+
try {
|
|
14520
|
+
const emptyPage = await executePageScript(
|
|
14521
|
+
wc,
|
|
14522
|
+
`(function() {
|
|
14523
|
+
var body = (document.body.textContent || '').toLowerCase();
|
|
14524
|
+
return /\b(no results|no items found|nothing matched|0 results|zero results|no products|your search.*did not match|no books found)\b/.test(body)
|
|
14525
|
+
&& body.length < 8000;
|
|
14526
|
+
})()`,
|
|
14527
|
+
{ timeoutMs: 1e3, label: "empty page check" }
|
|
14528
|
+
);
|
|
14529
|
+
if (emptyPage && emptyPage !== PAGE_SCRIPT_TIMEOUT) {
|
|
14530
|
+
return `
|
|
14531
|
+
[state: url=${wc.getURL()}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=false]
|
|
14532
|
+
WARNING: This page shows no results. You likely clicked a filter or category link instead of a product. Call go_back to return to the search results.`;
|
|
14533
|
+
}
|
|
14534
|
+
} catch {
|
|
14535
|
+
}
|
|
13016
14536
|
}
|
|
13017
14537
|
if (interactActions.includes(name)) {
|
|
13018
14538
|
return `
|
|
@@ -13253,14 +14773,48 @@ async function executeAction(name, args, ctx) {
|
|
|
13253
14773
|
}
|
|
13254
14774
|
case "click": {
|
|
13255
14775
|
if (!wc) return "Error: No active tab";
|
|
13256
|
-
|
|
13257
|
-
|
|
14776
|
+
let selector = null;
|
|
14777
|
+
const textTarget = typeof args.text === "string" && args.text.trim() ? args.text.trim() : "";
|
|
14778
|
+
if (typeof args.selector === "string" && args.selector.trim()) {
|
|
14779
|
+
selector = await resolveSelector$1(wc, void 0, args.selector);
|
|
14780
|
+
} else if (textTarget) {
|
|
14781
|
+
if (isInvalidTextTargetQuery(textTarget)) {
|
|
14782
|
+
return `Error: "${textTarget}" looks like HTML or markup, not a visible page label. Use a book title, button text, or element index instead.`;
|
|
14783
|
+
}
|
|
14784
|
+
selector = await resolveTargetByText(wc, textTarget, "interactive");
|
|
14785
|
+
if (!selector && typeof args.index === "number") {
|
|
14786
|
+
selector = `__vessel_idx:${args.index}`;
|
|
14787
|
+
}
|
|
14788
|
+
} else if (typeof args.index === "number") {
|
|
14789
|
+
selector = await resolveSelector$1(wc, args.index);
|
|
14790
|
+
if (!selector) selector = `__vessel_idx:${args.index}`;
|
|
14791
|
+
} else {
|
|
14792
|
+
selector = await resolveSelector$1(wc, args.index, args.selector);
|
|
14793
|
+
}
|
|
14794
|
+
if (selector === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
|
|
14795
|
+
if (!selector) {
|
|
14796
|
+
return "Error: No element index, selector, or visible text provided";
|
|
14797
|
+
}
|
|
13258
14798
|
return clickResolvedSelector$1(wc, selector);
|
|
13259
14799
|
}
|
|
13260
14800
|
case "inspect_element": {
|
|
13261
14801
|
if (!wc) return "Error: No active tab";
|
|
13262
|
-
|
|
13263
|
-
|
|
14802
|
+
let selector = null;
|
|
14803
|
+
const textTarget = typeof args.text === "string" && args.text.trim() ? args.text.trim() : "";
|
|
14804
|
+
if (textTarget) {
|
|
14805
|
+
if (isInvalidTextTargetQuery(textTarget)) {
|
|
14806
|
+
return `Error: "${textTarget}" looks like HTML or markup, not visible page text. Use a section title, book title, or element index instead.`;
|
|
14807
|
+
}
|
|
14808
|
+
selector = await resolveTargetByText(wc, textTarget, "context");
|
|
14809
|
+
} else {
|
|
14810
|
+
selector = await resolveSelector$1(wc, args.index, args.selector);
|
|
14811
|
+
}
|
|
14812
|
+
if (selector === PAGE_SCRIPT_TIMEOUT) {
|
|
14813
|
+
return pageBusyError("inspect_element");
|
|
14814
|
+
}
|
|
14815
|
+
if (!selector) {
|
|
14816
|
+
return "Error: No element index, selector, or visible text provided";
|
|
14817
|
+
}
|
|
13264
14818
|
return inspectElement(
|
|
13265
14819
|
wc,
|
|
13266
14820
|
selector,
|
|
@@ -13409,7 +14963,8 @@ async function executeAction(name, args, ctx) {
|
|
|
13409
14963
|
const livePrefix = liveSelectionSection ? `${liveSelectionSection}
|
|
13410
14964
|
|
|
13411
14965
|
` : "";
|
|
13412
|
-
const
|
|
14966
|
+
const baseMode = normalizeReadPageMode(args.mode, content);
|
|
14967
|
+
const requestedMode = ctx.toolProfile === "compact" && (args.mode == null || typeof args.mode === "string" && !args.mode.trim()) ? chooseCompactReadMode(content, baseMode) : baseMode;
|
|
13413
14968
|
if (requestedMode === "debug" || requestedMode === "full") {
|
|
13414
14969
|
const structured = buildStructuredContext(content);
|
|
13415
14970
|
const truncated = content.content.length > 2e4 ? content.content.slice(0, 2e4) + "\n[Content truncated...]" : content.content;
|
|
@@ -13421,7 +14976,7 @@ ${structured}
|
|
|
13421
14976
|
|
|
13422
14977
|
${truncated}`;
|
|
13423
14978
|
}
|
|
13424
|
-
const scoped = buildScopedContext(content, requestedMode);
|
|
14979
|
+
const scoped = ctx.toolProfile === "compact" ? buildCompactScopedContext(content, requestedMode) : buildScopedContext(content, requestedMode);
|
|
13425
14980
|
return [
|
|
13426
14981
|
livePrefix ? livePrefix.trimEnd() : "",
|
|
13427
14982
|
`[read_page mode=${requestedMode}]`,
|
|
@@ -14098,9 +15653,20 @@ ${JSON.stringify(tableJson, null, 2)}`;
|
|
|
14098
15653
|
}
|
|
14099
15654
|
case "scroll_to_element": {
|
|
14100
15655
|
if (!wc) return "Error: No active tab";
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
15656
|
+
let sel = null;
|
|
15657
|
+
const textTarget = typeof args.text === "string" && args.text.trim() ? args.text.trim() : "";
|
|
15658
|
+
if (textTarget) {
|
|
15659
|
+
if (isInvalidTextTargetQuery(textTarget)) {
|
|
15660
|
+
return `Error: "${textTarget}" looks like HTML or markup, not visible page text. Use a section title or element index instead.`;
|
|
15661
|
+
}
|
|
15662
|
+
sel = await resolveTargetByText(wc, textTarget, "context");
|
|
15663
|
+
} else {
|
|
15664
|
+
sel = await resolveSelector$1(wc, args.index, args.selector);
|
|
15665
|
+
}
|
|
15666
|
+
if (sel === PAGE_SCRIPT_TIMEOUT) return pageBusyError("scroll_to_element");
|
|
15667
|
+
if (!sel) {
|
|
15668
|
+
return "Error: Provide an index, selector, or visible text for the element to scroll to.";
|
|
15669
|
+
}
|
|
14104
15670
|
const block = args.position === "top" ? "start" : args.position === "bottom" ? "end" : "center";
|
|
14105
15671
|
if (sel.startsWith("__vessel_idx:")) {
|
|
14106
15672
|
const idx = Number(sel.slice("__vessel_idx:".length));
|
|
@@ -14138,8 +15704,36 @@ ${JSON.stringify(tableJson, null, 2)}`;
|
|
|
14138
15704
|
}
|
|
14139
15705
|
}
|
|
14140
15706
|
});
|
|
15707
|
+
const formattedResult = ctx.toolProfile === "compact" ? formatCompactToolResult(name, result) : result;
|
|
14141
15708
|
const flowCtx = ctx.runtime.getFlowContext();
|
|
14142
|
-
|
|
15709
|
+
let clickNavSummary = "";
|
|
15710
|
+
if (name === "click" && !result.startsWith("Error") && !result.startsWith("Blocked") && result.includes(" -> ")) {
|
|
15711
|
+
const summaryWc = ctx.tabManager.getActiveTab()?.view.webContents;
|
|
15712
|
+
if (summaryWc) {
|
|
15713
|
+
clickNavSummary = await getPostClickNavSummary(
|
|
15714
|
+
summaryWc,
|
|
15715
|
+
ctx.toolProfile
|
|
15716
|
+
);
|
|
15717
|
+
}
|
|
15718
|
+
}
|
|
15719
|
+
let streakWarning = "";
|
|
15720
|
+
if (name === "click" && !result.startsWith("Error") && !result.startsWith("Blocked")) {
|
|
15721
|
+
const currentUrl = ctx.tabManager.getActiveTab()?.view.webContents.getURL() ?? "";
|
|
15722
|
+
if (currentUrl === clickStreakUrl) {
|
|
15723
|
+
clickStreakCount++;
|
|
15724
|
+
} else {
|
|
15725
|
+
clickStreakUrl = currentUrl;
|
|
15726
|
+
clickStreakCount = 1;
|
|
15727
|
+
}
|
|
15728
|
+
if (clickStreakCount >= CLICK_STREAK_THRESHOLD) {
|
|
15729
|
+
streakWarning = `
|
|
15730
|
+
WARNING: You have clicked ${clickStreakCount} elements on this page without verifying the result. Call read_page or inspect_element to check the current page state before clicking again. If clicks are having no effect, the elements may not be interactive — try different element indices or read the page to find clickable links.`;
|
|
15731
|
+
}
|
|
15732
|
+
} else if (["read_page", "inspect_element", "screenshot", "wait_for"].includes(name)) {
|
|
15733
|
+
clickStreakCount = 0;
|
|
15734
|
+
clickStreakUrl = null;
|
|
15735
|
+
}
|
|
15736
|
+
return formattedResult + await getPostActionState$1(ctx, name) + clickNavSummary + streakWarning + flowCtx;
|
|
14143
15737
|
}
|
|
14144
15738
|
async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history) {
|
|
14145
15739
|
const lowerQuery = query.toLowerCase().trim();
|
|
@@ -14150,14 +15744,20 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
|
|
|
14150
15744
|
const pageType = detectPageType(pageContent);
|
|
14151
15745
|
const defaultReadMode = chooseAgentReadMode(pageContent);
|
|
14152
15746
|
if (provider.agentToolProfile === "compact") {
|
|
15747
|
+
const prevGoal = runtime2.getState().taskTracker?.goal?.trim();
|
|
14153
15748
|
runtime2.ensureTaskTracker(query, pageContent.url || activeWebContents.getURL());
|
|
15749
|
+
if (prevGoal !== query.trim()) {
|
|
15750
|
+
clearCartState();
|
|
15751
|
+
}
|
|
14154
15752
|
} else {
|
|
14155
15753
|
runtime2.clearTaskTracker();
|
|
15754
|
+
clearCartState();
|
|
14156
15755
|
}
|
|
14157
|
-
const structuredContext =
|
|
15756
|
+
const structuredContext = provider.agentToolProfile === "compact" ? buildCompactScopedContext(
|
|
14158
15757
|
pageContent,
|
|
14159
|
-
defaultReadMode
|
|
14160
|
-
|
|
15758
|
+
defaultReadMode,
|
|
15759
|
+
pageType
|
|
15760
|
+
) : buildScopedContext(pageContent, defaultReadMode);
|
|
14161
15761
|
const runtimeState = runtime2.getState();
|
|
14162
15762
|
const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
|
|
14163
15763
|
const taskTrackerContext = runtime2.getTaskTrackerContext();
|
|
@@ -14181,7 +15781,11 @@ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.ti
|
|
|
14181
15781
|
recentCheckpoints: recentCheckpoints || "- none",
|
|
14182
15782
|
taskTrackerContext: taskTrackerContext || "- none"
|
|
14183
15783
|
});
|
|
14184
|
-
const actionCtx = {
|
|
15784
|
+
const actionCtx = {
|
|
15785
|
+
tabManager,
|
|
15786
|
+
runtime: runtime2,
|
|
15787
|
+
toolProfile: provider.agentToolProfile
|
|
15788
|
+
};
|
|
14185
15789
|
const contextualTools = pruneToolsForContext(
|
|
14186
15790
|
AGENT_TOOLS,
|
|
14187
15791
|
pageType,
|
|
@@ -15370,7 +16974,7 @@ async function clickElement(wc, selector) {
|
|
|
15370
16974
|
|
|
15371
16975
|
const rect = el.getBoundingClientRect();
|
|
15372
16976
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
15373
|
-
return { error: "Element is not visible" };
|
|
16977
|
+
return { error: "Element is not visible. It may be inside a collapsed, lazy-loaded, or virtual-scroll section. Scroll toward it (scroll or scroll_to_element) then call read_page to refresh visible elements before clicking again." };
|
|
15374
16978
|
}
|
|
15375
16979
|
|
|
15376
16980
|
const points = samplePoints(rect);
|
|
@@ -15481,8 +17085,24 @@ async function clickResolvedSelector(wc, selector) {
|
|
|
15481
17085
|
await waitForPotentialNavigation(wc, beforeUrl2);
|
|
15482
17086
|
const afterUrl2 = wc.getURL();
|
|
15483
17087
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
15484
|
-
|
|
15485
|
-
if (!overlayHint2)
|
|
17088
|
+
let overlayHint2 = await detectPostClickOverlay(wc);
|
|
17089
|
+
if (!overlayHint2 && typeof idxLabel === "string" && isAddToCartText(idxLabel)) {
|
|
17090
|
+
await sleep(1200);
|
|
17091
|
+
overlayHint2 = await detectPostClickOverlay(wc);
|
|
17092
|
+
}
|
|
17093
|
+
if (!overlayHint2) {
|
|
17094
|
+
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
17095
|
+
if (hrefMatch) {
|
|
17096
|
+
try {
|
|
17097
|
+
await wc.loadURL(hrefMatch[1]);
|
|
17098
|
+
await waitForLoad(wc, 8e3);
|
|
17099
|
+
const hrefUrl = wc.getURL();
|
|
17100
|
+
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
17101
|
+
} catch {
|
|
17102
|
+
}
|
|
17103
|
+
}
|
|
17104
|
+
return result;
|
|
17105
|
+
}
|
|
15486
17106
|
const dialogActions = typeof idxLabel === "string" && isAddToCartText(idxLabel) ? await getCartDialogActions(wc) : null;
|
|
15487
17107
|
const actionsSuffix = dialogActions ? `
|
|
15488
17108
|
${dialogActions}
|
|
@@ -15504,9 +17124,11 @@ ${overlayHint2}${actionsSuffix}`;
|
|
|
15504
17124
|
const result = await wc.executeJavaScript(`
|
|
15505
17125
|
(function() {
|
|
15506
17126
|
var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
|
|
15507
|
-
if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
17127
|
+
if (!el || !document.contains(el)) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
|
|
15508
17128
|
if (el instanceof HTMLElement) { el.focus(); el.click(); }
|
|
15509
|
-
|
|
17129
|
+
var anchor = el instanceof HTMLAnchorElement ? el : el.closest('a[href]');
|
|
17130
|
+
var href = anchor instanceof HTMLAnchorElement ? anchor.href : null;
|
|
17131
|
+
return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase()) + (href ? "\\nhref: " + href : "");
|
|
15510
17132
|
})()
|
|
15511
17133
|
`);
|
|
15512
17134
|
if (typeof result === "string" && result.startsWith("Error")) return result;
|
|
@@ -15516,14 +17138,30 @@ ${overlayHint2}${actionsSuffix}`;
|
|
|
15516
17138
|
await waitForPotentialNavigation(wc, beforeUrl2);
|
|
15517
17139
|
const afterUrl2 = wc.getURL();
|
|
15518
17140
|
if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
|
|
15519
|
-
|
|
15520
|
-
if (!overlayHint2)
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
17141
|
+
let overlayHint2 = await detectPostClickOverlay(wc);
|
|
17142
|
+
if (!overlayHint2 && typeof shadowLabel === "string" && isAddToCartText(shadowLabel)) {
|
|
17143
|
+
await sleep(1200);
|
|
17144
|
+
overlayHint2 = await detectPostClickOverlay(wc);
|
|
17145
|
+
}
|
|
17146
|
+
if (!overlayHint2) {
|
|
17147
|
+
const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
|
|
17148
|
+
if (hrefMatch) {
|
|
17149
|
+
try {
|
|
17150
|
+
await wc.loadURL(hrefMatch[1]);
|
|
17151
|
+
await waitForLoad(wc, 8e3);
|
|
17152
|
+
const hrefUrl = wc.getURL();
|
|
17153
|
+
if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
|
|
17154
|
+
} catch {
|
|
17155
|
+
}
|
|
17156
|
+
}
|
|
17157
|
+
return result;
|
|
17158
|
+
}
|
|
17159
|
+
const dialogActions2 = typeof shadowLabel === "string" && isAddToCartText(shadowLabel) ? await getCartDialogActions(wc) : null;
|
|
17160
|
+
const actionsSuffix2 = dialogActions2 ? `
|
|
17161
|
+
${dialogActions2}
|
|
15524
17162
|
Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
15525
17163
|
return `${result}
|
|
15526
|
-
${overlayHint2}${
|
|
17164
|
+
${overlayHint2}${actionsSuffix2}`;
|
|
15527
17165
|
}
|
|
15528
17166
|
const beforeUrl = wc.getURL();
|
|
15529
17167
|
const elInfo = await describeElementForClick(wc, selector);
|
|
@@ -15567,6 +17205,16 @@ Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
|
15567
17205
|
${overlayHint}${actionsSuffix}`;
|
|
15568
17206
|
}
|
|
15569
17207
|
if (cartMatch) {
|
|
17208
|
+
await sleep(1200);
|
|
17209
|
+
const delayedOverlayHint = await detectPostClickOverlay(wc);
|
|
17210
|
+
if (delayedOverlayHint) {
|
|
17211
|
+
const dialogActions = await getCartDialogActions(wc);
|
|
17212
|
+
const actionsSuffix = dialogActions ? `
|
|
17213
|
+
${dialogActions}
|
|
17214
|
+
Click one of these dialog actions. Do NOT click any other element.` : "";
|
|
17215
|
+
return `${clickText} (${clickResult})
|
|
17216
|
+
${delayedOverlayHint}${actionsSuffix}`;
|
|
17217
|
+
}
|
|
15570
17218
|
return `${clickText} (${clickResult})`;
|
|
15571
17219
|
}
|
|
15572
17220
|
const activationResult = await activateElement(wc, selector);
|
|
@@ -16229,7 +17877,7 @@ async function hoverElement(wc, selector) {
|
|
|
16229
17877
|
el.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'center' });
|
|
16230
17878
|
}
|
|
16231
17879
|
const rect = el.getBoundingClientRect();
|
|
16232
|
-
if (rect.width <= 0 || rect.height <= 0) return { error: 'Error[hidden]: Element has no visible area' };
|
|
17880
|
+
if (rect.width <= 0 || rect.height <= 0) return { error: 'Error[hidden]: Element has no visible area. It may be inside a collapsed, lazy-loaded, or virtual-scroll section. Scroll toward it then call read_page to refresh visible elements.' };
|
|
16233
17881
|
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true }));
|
|
16234
17882
|
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false }));
|
|
16235
17883
|
const label = (el.textContent || el.tagName || 'Element').trim().slice(0, 80);
|
|
@@ -20492,8 +22140,9 @@ function registerIpcHandlers(windowState, runtime2) {
|
|
|
20492
22140
|
if (!config || typeof config !== "object" || !("id" in config)) {
|
|
20493
22141
|
return { ok: false, models: [], error: "Invalid provider configuration" };
|
|
20494
22142
|
}
|
|
20495
|
-
|
|
20496
|
-
|
|
22143
|
+
return await fetchProviderModels(
|
|
22144
|
+
config
|
|
22145
|
+
);
|
|
20497
22146
|
} catch (err) {
|
|
20498
22147
|
return { ok: false, models: [], error: err instanceof Error ? err.message : "Unknown error" };
|
|
20499
22148
|
}
|
|
@@ -20983,6 +22632,82 @@ function setNextHint(state2, nextHint) {
|
|
|
20983
22632
|
function normalizeResult(result) {
|
|
20984
22633
|
return result.toLowerCase();
|
|
20985
22634
|
}
|
|
22635
|
+
function looksLikeListingResult(result) {
|
|
22636
|
+
const lowered = normalizeResult(result);
|
|
22637
|
+
return lowered.includes("### primary results") || lowered.includes("### likely search results") || lowered.includes("[read_page mode=results_only]");
|
|
22638
|
+
}
|
|
22639
|
+
function countSurfacedResults(result) {
|
|
22640
|
+
const matches = result.match(/^\s*-\s+\[#\d+\]/gm);
|
|
22641
|
+
return matches?.length ?? 0;
|
|
22642
|
+
}
|
|
22643
|
+
function looksLikeSearchResultsPage(result) {
|
|
22644
|
+
const lowered = normalizeResult(result);
|
|
22645
|
+
return lowered.includes("/searchresults") || lowered.includes("/books/search") || lowered.includes("search results") || lowered.includes("bestsellers") || lowered.includes("best sellers");
|
|
22646
|
+
}
|
|
22647
|
+
function looksLikeProductDetailResult(result) {
|
|
22648
|
+
const lowered = normalizeResult(result);
|
|
22649
|
+
return lowered.includes("### visible purchase controls") || /\badd(?: item)? to (?:cart|bag|basket)\b/.test(lowered) || lowered.includes("buy now") || /https?:\/\/[^\s)]+\/book\//i.test(result);
|
|
22650
|
+
}
|
|
22651
|
+
function looksLikeCartConfirmation(result) {
|
|
22652
|
+
return /(added to cart|cart confirmation|view cart|continue shopping|shopping cart|checkout)/.test(
|
|
22653
|
+
normalizeResult(result)
|
|
22654
|
+
);
|
|
22655
|
+
}
|
|
22656
|
+
function looksLikeCartPage(result) {
|
|
22657
|
+
const lowered = normalizeResult(result);
|
|
22658
|
+
return /\*\*url:\*\*\s*https?:\/\/[^\s]+\/cart\b/.test(lowered) || /\b(?:navigated to|went back to|went forward to)\s+https?:\/\/[^\s]+\/cart\b/.test(
|
|
22659
|
+
lowered
|
|
22660
|
+
) || /\*\*title:\*\*.*\b(cart|checkout)\b/.test(lowered) || /\b(shopping cart|cart subtotal|cart total)\b/.test(lowered);
|
|
22661
|
+
}
|
|
22662
|
+
function looksLikeStaleElementError(result) {
|
|
22663
|
+
const lowered = normalizeResult(result);
|
|
22664
|
+
return lowered.includes("error[stale-index]") || lowered.includes("element not found — the page may have changed") || lowered.includes("shadow dom element not found") || lowered.includes("call read_page to refresh") || lowered.includes("cannot locate the elements to click") || lowered.includes("page structure is not being reliably captured");
|
|
22665
|
+
}
|
|
22666
|
+
function extractStructuredUrl(result) {
|
|
22667
|
+
return result.match(/\*\*url:\*\*\s*([^\n]+)/i)?.[1]?.trim() ?? extractNavigatedUrl(result);
|
|
22668
|
+
}
|
|
22669
|
+
function isAddToCartSuccess(actionName, result) {
|
|
22670
|
+
const lowered = normalizeResult(result);
|
|
22671
|
+
if (actionName !== "click") return false;
|
|
22672
|
+
if (lowered.startsWith("blocked:")) return false;
|
|
22673
|
+
const clickedAddToCart = /clicked:.*add(?: item)? to (?:cart|bag|basket)/.test(lowered);
|
|
22674
|
+
return clickedAddToCart && looksLikeCartConfirmation(result);
|
|
22675
|
+
}
|
|
22676
|
+
function extractNavigatedUrl(result) {
|
|
22677
|
+
return result.match(
|
|
22678
|
+
/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i
|
|
22679
|
+
)?.[1]?.trim() ?? null;
|
|
22680
|
+
}
|
|
22681
|
+
function stepIndexMatching(steps, pattern) {
|
|
22682
|
+
return steps.findIndex((step) => pattern.test(step.label.toLowerCase()));
|
|
22683
|
+
}
|
|
22684
|
+
function finalizeShoppingTracker(state2, detail) {
|
|
22685
|
+
const steps = state2.steps.map((step) => ({ ...step }));
|
|
22686
|
+
const pickIndex = stepIndexMatching(steps, /^pick the requested/);
|
|
22687
|
+
const cartIndex = stepIndexMatching(steps, /^add the chosen .* to the cart$/);
|
|
22688
|
+
const explainIndex = stepIndexMatching(steps, /^explain the recommendations$/);
|
|
22689
|
+
if (pickIndex >= 0) {
|
|
22690
|
+
steps[pickIndex] = {
|
|
22691
|
+
...steps[pickIndex],
|
|
22692
|
+
status: "done",
|
|
22693
|
+
detail
|
|
22694
|
+
};
|
|
22695
|
+
}
|
|
22696
|
+
if (cartIndex >= 0) {
|
|
22697
|
+
steps[cartIndex] = {
|
|
22698
|
+
...steps[cartIndex],
|
|
22699
|
+
status: "done",
|
|
22700
|
+
detail
|
|
22701
|
+
};
|
|
22702
|
+
}
|
|
22703
|
+
const activeIndex = explainIndex >= 0 ? explainIndex : state2.currentStepIndex;
|
|
22704
|
+
return {
|
|
22705
|
+
...state2,
|
|
22706
|
+
steps: setActiveStep(steps, activeIndex),
|
|
22707
|
+
currentStepIndex: activeIndex,
|
|
22708
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
22709
|
+
};
|
|
22710
|
+
}
|
|
20986
22711
|
function createTaskTracker(goal, startUrl) {
|
|
20987
22712
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20988
22713
|
return {
|
|
@@ -20993,23 +22718,59 @@ function createTaskTracker(goal, startUrl) {
|
|
|
20993
22718
|
currentStepIndex: 0,
|
|
20994
22719
|
steps: buildInitialSteps(goal),
|
|
20995
22720
|
lastAction: void 0,
|
|
20996
|
-
nextHint: "
|
|
22721
|
+
nextHint: "Use the site's search box or a strong curated section immediately. Avoid rereading the homepage unless search or navigation is hidden."
|
|
20997
22722
|
};
|
|
20998
22723
|
}
|
|
20999
22724
|
function updateTaskTracker(state2, actionName, result) {
|
|
21000
|
-
const
|
|
22725
|
+
const requestedCount = state2.requestedCount ?? extractRequestedCount(state2.goal) ?? null;
|
|
22726
|
+
let cartCount = state2.cartCount ?? 0;
|
|
22727
|
+
const cartVisible = state2.cartVisible || looksLikeCartPage(result);
|
|
21001
22728
|
let nextState = {
|
|
21002
22729
|
...state2,
|
|
21003
22730
|
lastAction: actionName,
|
|
22731
|
+
requestedCount,
|
|
22732
|
+
cartCount,
|
|
22733
|
+
cartVisible,
|
|
21004
22734
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21005
22735
|
};
|
|
21006
22736
|
const currentLabel = nextState.steps[nextState.currentStepIndex]?.label.toLowerCase() ?? "";
|
|
21007
22737
|
if (actionName === "navigate") {
|
|
21008
|
-
|
|
21009
|
-
|
|
21010
|
-
|
|
21011
|
-
|
|
21012
|
-
|
|
22738
|
+
if (/navigate to the requested site/.test(currentLabel)) {
|
|
22739
|
+
nextState = completeStep(nextState, "Reached the requested site.");
|
|
22740
|
+
return setNextHint(
|
|
22741
|
+
nextState,
|
|
22742
|
+
"Use the site's search box or a curated section to expose product results you can click directly. Avoid a full-page read unless the path is unclear."
|
|
22743
|
+
);
|
|
22744
|
+
}
|
|
22745
|
+
const navigatedUrl = extractNavigatedUrl(result) || "";
|
|
22746
|
+
if (/pick the requested/.test(currentLabel)) {
|
|
22747
|
+
if (/\/book\//i.test(navigatedUrl)) {
|
|
22748
|
+
return setNextHint(
|
|
22749
|
+
nextState,
|
|
22750
|
+
"You opened a chosen product detail page. Do not restart search. Click Add to Cart here, then wait for cart confirmation before moving on."
|
|
22751
|
+
);
|
|
22752
|
+
}
|
|
22753
|
+
if (looksLikeSearchResultsPage(result)) {
|
|
22754
|
+
return setNextHint(
|
|
22755
|
+
nextState,
|
|
22756
|
+
"You are back on a results page while the chosen items are already decided. Do not restart search or browse new categories. Open one of the chosen result links and continue the add-to-cart flow."
|
|
22757
|
+
);
|
|
22758
|
+
}
|
|
22759
|
+
}
|
|
22760
|
+
if (/add the chosen .* to the cart/.test(currentLabel)) {
|
|
22761
|
+
if (/\/book\//i.test(navigatedUrl)) {
|
|
22762
|
+
return setNextHint(
|
|
22763
|
+
nextState,
|
|
22764
|
+
"Stay on this product detail page and add the current chosen item to the cart. Do not go back to search unless this specific cart step fails."
|
|
22765
|
+
);
|
|
22766
|
+
}
|
|
22767
|
+
if (looksLikeSearchResultsPage(result)) {
|
|
22768
|
+
return setNextHint(
|
|
22769
|
+
nextState,
|
|
22770
|
+
"The chosen items are already decided. Do not restart the search flow here. Open the next chosen result from the current page and add it to the cart."
|
|
22771
|
+
);
|
|
22772
|
+
}
|
|
22773
|
+
}
|
|
21013
22774
|
}
|
|
21014
22775
|
const isDiscoveryAction = [
|
|
21015
22776
|
"read_page",
|
|
@@ -21019,31 +22780,101 @@ function updateTaskTracker(state2, actionName, result) {
|
|
|
21019
22780
|
"scroll"
|
|
21020
22781
|
].includes(actionName);
|
|
21021
22782
|
if (isDiscoveryAction && /browse or search/.test(currentLabel)) {
|
|
22783
|
+
const surfacedResults = countSurfacedResults(result);
|
|
21022
22784
|
nextState = completeStep(nextState, "Found a starting point on the site.");
|
|
21023
22785
|
return setNextHint(
|
|
21024
22786
|
nextState,
|
|
21025
|
-
"Inspect
|
|
22787
|
+
looksLikeListingResult(result) ? surfacedResults === 1 ? "One likely result is visible. Inspect or click that result before deciding there is no match. Do not skip to a new search yet." : "Product results are already visible. Open exactly one unseen result now, add that item before choosing another, and do not click multiple results in a row from the same listing page." : looksLikeSearchResultsPage(result) ? 'You are on a results page. Call read_page(mode="results_only") now to surface product results. Do not use visible_only or generic inspect_element to hunt result links.' : "Expose product results you can click directly, then inspect individual items until you have the full set."
|
|
21026
22788
|
);
|
|
21027
22789
|
}
|
|
21028
22790
|
if (/pick the requested/.test(currentLabel) && isDiscoveryAction) {
|
|
22791
|
+
if (isAddToCartSuccess(actionName, result)) {
|
|
22792
|
+
cartCount += 1;
|
|
22793
|
+
nextState = {
|
|
22794
|
+
...nextState,
|
|
22795
|
+
cartCount
|
|
22796
|
+
};
|
|
22797
|
+
if (requestedCount && cartCount >= requestedCount) {
|
|
22798
|
+
nextState = finalizeShoppingTracker(
|
|
22799
|
+
nextState,
|
|
22800
|
+
`Added ${cartCount} of ${requestedCount} requested items to the cart.`
|
|
22801
|
+
);
|
|
22802
|
+
return setNextHint(
|
|
22803
|
+
nextState,
|
|
22804
|
+
cartVisible ? "All requested items are now in the cart and the cart is visible. Explain your reasoning in chat now and stop using tools." : "All requested items are now in the cart. Open the cart so the user can see it, then explain your reasoning in chat and stop using tools."
|
|
22805
|
+
);
|
|
22806
|
+
}
|
|
22807
|
+
return setNextHint(
|
|
22808
|
+
nextState,
|
|
22809
|
+
requestedCount ? `${cartCount} of ${requestedCount} requested items are now in the cart. If the cart confirmation dialog is open, click Continue Shopping there. Do not click View Cart or Go to Basket until all requested items are added. Only use go_back if no dialog action is available. Then open the next unseen result.` : "This item is now in the cart. If the cart confirmation dialog is open, click Continue Shopping there. Do not click View Cart or Go to Basket yet. Only use go_back if no dialog action is available. Then open the next unseen result."
|
|
22810
|
+
);
|
|
22811
|
+
}
|
|
22812
|
+
if (looksLikeCartConfirmation(result)) {
|
|
22813
|
+
return setNextHint(
|
|
22814
|
+
nextState,
|
|
22815
|
+
"This item is already in the cart. If the cart confirmation dialog is still open, click Continue Shopping there. Do not click View Cart or Go to Basket yet. Only use go_back if no dialog action is available. Then open the next unseen result."
|
|
22816
|
+
);
|
|
22817
|
+
}
|
|
22818
|
+
const structuredUrl = extractStructuredUrl(result) || "";
|
|
22819
|
+
if (actionName === "read_page" && cartCount > 0 && /\/book\//i.test(structuredUrl) && looksLikeProductDetailResult(result)) {
|
|
22820
|
+
return setNextHint(
|
|
22821
|
+
nextState,
|
|
22822
|
+
"This detail page may already be for an item you just added. Do not click Add to Cart again on the same page. If the cart confirmation dialog is still open, click Continue Shopping there. Otherwise go back once and open the next chosen result. Do not click View Cart or Go to Basket yet."
|
|
22823
|
+
);
|
|
22824
|
+
}
|
|
22825
|
+
if (looksLikeProductDetailResult(result)) {
|
|
22826
|
+
return setNextHint(
|
|
22827
|
+
nextState,
|
|
22828
|
+
'You are on a product detail page. Opening this page did not add the item to the cart. Click Add to Cart now, then wait for cart confirmation before moving on. Use read_page(mode="visible_only") once only if you need the Add to Cart index.'
|
|
22829
|
+
);
|
|
22830
|
+
}
|
|
22831
|
+
if (looksLikeSearchResultsPage(result) && !looksLikeListingResult(result)) {
|
|
22832
|
+
return setNextHint(
|
|
22833
|
+
nextState,
|
|
22834
|
+
'This is still a results page. Call read_page(mode="results_only") now and click a surfaced result. Do not loop on visible_only or generic inspect_element here.'
|
|
22835
|
+
);
|
|
22836
|
+
}
|
|
22837
|
+
if (looksLikeListingResult(result)) {
|
|
22838
|
+
const surfacedResults = countSurfacedResults(result);
|
|
22839
|
+
return setNextHint(
|
|
22840
|
+
nextState,
|
|
22841
|
+
surfacedResults === 1 ? "There is one likely result visible. Inspect or click that result before declaring no match or moving to a different query." : "A product listing is already visible. Open exactly one unseen result now, add that item to the cart from its detail page, then return for the next result. Do not click multiple remembered results in a row from the listing page."
|
|
22842
|
+
);
|
|
22843
|
+
}
|
|
22844
|
+
if (actionName === "click" && looksLikeStaleElementError(result)) {
|
|
22845
|
+
return setNextHint(
|
|
22846
|
+
nextState,
|
|
22847
|
+
"The last remembered click target is stale. Trust the latest page state, not older labels or indexes. Call read_page once on the current page. If you are on a detail page, add the current item now. If you are still on a listing page, open exactly one visible unseen result. Do not click multiple saved results in sequence."
|
|
22848
|
+
);
|
|
22849
|
+
}
|
|
21029
22850
|
return setNextHint(
|
|
21030
22851
|
nextState,
|
|
21031
|
-
"
|
|
22852
|
+
"Pick one promising unseen item at a time. As soon as a detail page opens, add that item to the cart before selecting another result."
|
|
21032
22853
|
);
|
|
21033
22854
|
}
|
|
21034
|
-
if (/
|
|
21035
|
-
|
|
21036
|
-
|
|
21037
|
-
|
|
22855
|
+
if (/pick the requested/.test(currentLabel) && actionName === "go_back") {
|
|
22856
|
+
return setNextHint(
|
|
22857
|
+
nextState,
|
|
22858
|
+
"You are back on the listing flow. Open exactly one next chosen or unseen result now instead of rereading the whole page, restarting search, or clicking multiple results in sequence."
|
|
22859
|
+
);
|
|
22860
|
+
}
|
|
22861
|
+
if (/add the chosen .* to the cart/.test(currentLabel) && isAddToCartSuccess(actionName, result)) {
|
|
22862
|
+
cartCount += 1;
|
|
22863
|
+
nextState = {
|
|
22864
|
+
...nextState,
|
|
22865
|
+
cartCount
|
|
22866
|
+
};
|
|
22867
|
+
const detail = requestedCount ? `Added ${cartCount} of ${requestedCount} requested items to the cart.` : "Cart interaction succeeded.";
|
|
22868
|
+
nextState = completeStep(nextState, detail);
|
|
21038
22869
|
return setNextHint(
|
|
21039
22870
|
nextState,
|
|
21040
|
-
"Summarize the chosen books and explain why they were recommended."
|
|
22871
|
+
requestedCount && cartCount >= requestedCount ? cartVisible ? "All requested books are now in the cart and the cart is visible. Explain your reasoning in chat now and stop using tools." : "All requested books are now in the cart. Open the cart so the user can see it, then explain your reasoning in chat and stop using tools." : requestedCount ? `${cartCount} of ${requestedCount} requested books are now in the cart. Continue adding the remaining selected books.` : "Summarize the chosen books and explain why they were recommended."
|
|
21041
22872
|
);
|
|
21042
22873
|
}
|
|
21043
22874
|
if (/explain the recommendations/.test(currentLabel)) {
|
|
21044
22875
|
return setNextHint(
|
|
21045
22876
|
nextState,
|
|
21046
|
-
"Finish by naming the chosen books and giving concise reasons for each."
|
|
22877
|
+
cartVisible ? "The cart is visible. Explain your reasoning in chat now, mention the chosen books, and stop using tools." : "Finish by naming the chosen books and giving concise reasons for each. If the cart is not visible yet, show it first."
|
|
21047
22878
|
);
|
|
21048
22879
|
}
|
|
21049
22880
|
return nextState;
|