@quanta-intellect/vessel-browser 0.1.35 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/main/index.js CHANGED
@@ -2067,8 +2067,8 @@ class TabManager {
2067
2067
  window;
2068
2068
  onStateChange;
2069
2069
  highlightCaptureCallback = null;
2070
- constructor(window, onStateChange) {
2071
- this.window = 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: "system",
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/.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,23 +5526,114 @@ 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
  }
5651
- function canonicalizeUrlLike(value) {
5543
+ function canonicalizeUrlLike(value) {
5544
+ try {
5545
+ const url = new URL(value.trim());
5546
+ if (url.protocol === "http:" || url.protocol === "https:") {
5547
+ url.hostname = url.hostname.replace(/^www\./, "");
5548
+ url.hash = "";
5549
+ if (url.pathname.endsWith("/") && url.pathname !== "/") {
5550
+ url.pathname = url.pathname.replace(/\/+$/, "");
5551
+ }
5552
+ return url.toString();
5553
+ }
5554
+ } catch {
5555
+ }
5556
+ return value.trim();
5557
+ }
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 };
5652
5612
  try {
5653
- const url = new URL(value.trim());
5654
- if (url.protocol === "http:" || url.protocol === "https:") {
5655
- url.hostname = url.hostname.replace(/^www\./, "");
5656
- url.hash = "";
5657
- if (url.pathname.endsWith("/") && url.pathname !== "/") {
5658
- url.pathname = url.pathname.replace(/\/+$/, "");
5659
- }
5660
- return url.toString();
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;
5661
5620
  }
5621
+ return null;
5662
5622
  } catch {
5663
5623
  }
5664
- return value.trim();
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;
5665
5637
  }
5666
5638
  function coerceToolArgsForExecution(name, args) {
5667
5639
  const coerced = { ...args };
@@ -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 delta = chunk.choices[0]?.delta?.content;
5851
- if (delta) {
5852
- onChunk(delta);
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
- try {
5929
- parsedArgs = JSON.parse(tc.argsJson || "{}");
5930
- } catch {
5931
- parsedArgs = {};
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
- try {
5971
- JSON.parse(tc.argsJson || "{}");
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: "system",
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
- try {
6025
- args = JSON.parse(tc.argsJson || "{}");
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: "system",
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
- if (this.agentToolProfile === "compact" && hasRecentDuplicateToolCall(
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: "system",
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) {
@@ -6166,6 +6437,55 @@ function validateProviderConnection(config, options = { requireModel: true }) {
6166
6437
  }
6167
6438
  return null;
6168
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).`;
6467
+ }
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.`;
6470
+ }
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);
6487
+ }
6488
+ }
6169
6489
  async function fetchProviderModels(config) {
6170
6490
  const normalized = sanitizeProviderConfig(config);
6171
6491
  const error = validateProviderConnection(normalized, { requireModel: false });
@@ -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
- return page.data.map((model) => model.id);
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,10 @@ 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.",
7802
8215
  "Keep your reasoning short. Prefer taking the next tool action over writing a long plan."
7803
8216
  ];
7804
8217
  function buildInstructionBlock(instructions) {
@@ -7849,6 +8262,342 @@ function buildAgentSystemPrompt(input) {
7849
8262
  ...instructionBlocks
7850
8263
  ].join("\n\n");
7851
8264
  }
8265
+ function normalizeComparable(value) {
8266
+ return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
8267
+ }
8268
+ function normalizeUrlForMatch(value) {
8269
+ if (!value) return null;
8270
+ try {
8271
+ const url = new URL(value);
8272
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
8273
+ return `${url.origin}${pathname}`.toLowerCase();
8274
+ } catch {
8275
+ return value.trim().replace(/\/+$/, "").toLowerCase() || null;
8276
+ }
8277
+ }
8278
+ function getUrlPathSegments(value) {
8279
+ if (!value) return [];
8280
+ try {
8281
+ return new URL(value).pathname.split("/").filter(Boolean);
8282
+ } catch {
8283
+ return value.split("?")[0].split("#")[0].split("/").filter(Boolean);
8284
+ }
8285
+ }
8286
+ function isVisible(element) {
8287
+ return element.visible !== false && element.obscured !== true && element.blockedByOverlay !== true;
8288
+ }
8289
+ function isInViewport(element) {
8290
+ return element.inViewport !== false;
8291
+ }
8292
+ function looksLikeListingPage(page) {
8293
+ const haystack = normalizeComparable(
8294
+ [
8295
+ page.url,
8296
+ page.title,
8297
+ page.excerpt,
8298
+ page.content.slice(0, 2e3),
8299
+ page.headings.map((heading) => heading.text).join(" ")
8300
+ ].filter(Boolean).join(" ")
8301
+ );
8302
+ 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(
8303
+ haystack
8304
+ );
8305
+ }
8306
+ function isBlockedLabel(label) {
8307
+ 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(
8308
+ label
8309
+ );
8310
+ }
8311
+ function resultScore(page, element, listingLike) {
8312
+ if (element.type !== "link" || !element.href || !element.text?.trim()) return -1;
8313
+ const text = element.text.trim();
8314
+ const comparableText = normalizeComparable(text);
8315
+ const href = normalizeUrlForMatch(element.href);
8316
+ const pageUrl = normalizeUrlForMatch(page.url);
8317
+ const hrefSegments = getUrlPathSegments(element.href);
8318
+ const haystack = normalizeComparable(
8319
+ [text, element.description, element.selector, element.href, element.context].filter(Boolean).join(" ")
8320
+ );
8321
+ if (!comparableText || isBlockedLabel(comparableText)) return -1;
8322
+ let score = 0;
8323
+ if (element.context === "article") score += 5;
8324
+ if (element.context === "main" || element.context === "content") score += 4;
8325
+ if (!element.context) score += 1;
8326
+ if (isVisible(element)) score += 2;
8327
+ if (isInViewport(element)) score += 3;
8328
+ if (text.length >= 8 && text.length <= 140) score += 2;
8329
+ if (text.split(/\s+/).length >= 2) score += 2;
8330
+ if (text.split(/\s+/).length >= 4) score += 1;
8331
+ if (hrefSegments.length >= 2) score += 1;
8332
+ if (hrefSegments.some((segment) => /book|books|item|product|title|catalog/.test(segment))) {
8333
+ score += 3;
8334
+ }
8335
+ if (href && pageUrl) {
8336
+ try {
8337
+ if (new URL(href).origin === new URL(pageUrl).origin) score += 2;
8338
+ } catch {
8339
+ }
8340
+ }
8341
+ if (listingLike) score += 2;
8342
+ if (/\b(book|novel|story|poems|poetry|essays|memoir|history|science|fiction)\b/.test(haystack)) {
8343
+ score += 2;
8344
+ }
8345
+ if (/\b(author|hardcover|paperback|preorder|pre-order|signed edition)\b/.test(haystack)) {
8346
+ score += 1;
8347
+ }
8348
+ if (element.context === "nav" || element.context === "header" || element.context === "footer" || element.context === "sidebar" || element.context === "dialog") {
8349
+ score -= 6;
8350
+ }
8351
+ if (/\b(filter|sort|format|price|signed|staff picks|more results|view all)\b/.test(comparableText)) {
8352
+ score -= 3;
8353
+ }
8354
+ try {
8355
+ const linkUrl = new URL(element.href);
8356
+ const filterParams = ["condition", "binding", "format", "availability", "sort", "filter", "price", "category_id", "view"];
8357
+ if (filterParams.some((p) => linkUrl.searchParams.has(p))) {
8358
+ score -= 10;
8359
+ }
8360
+ const filterPathSegments = ["format", "condition", "binding", "availability", "sort"];
8361
+ const hasFilterPath = filterPathSegments.some(
8362
+ (seg) => hrefSegments.some((s) => s.toLowerCase() === seg)
8363
+ );
8364
+ if (hasFilterPath) {
8365
+ score -= 10;
8366
+ }
8367
+ } catch {
8368
+ }
8369
+ return score;
8370
+ }
8371
+ function getCompactPrimaryResultLinks(page, options) {
8372
+ const listingLike = looksLikeListingPage(page);
8373
+ const max = options?.max ?? 8;
8374
+ const visibleOnly = options?.visibleOnly === true;
8375
+ const seen = /* @__PURE__ */ new Set();
8376
+ return page.interactiveElements.filter((element) => !visibleOnly || isVisible(element)).map((element) => ({
8377
+ element,
8378
+ score: resultScore(page, element, listingLike)
8379
+ })).filter(({ score }) => score >= (listingLike ? 5 : 7)).sort(
8380
+ (a, b) => b.score - a.score || (a.element.index ?? Number.MAX_SAFE_INTEGER) - (b.element.index ?? Number.MAX_SAFE_INTEGER)
8381
+ ).map(({ element }) => element).filter((element) => {
8382
+ const key = `${normalizeComparable(element.text)}|${normalizeUrlForMatch(element.href) || ""}`;
8383
+ if (seen.has(key)) return false;
8384
+ seen.add(key);
8385
+ return true;
8386
+ }).slice(0, max);
8387
+ }
8388
+ function looksLikeProductPage(page) {
8389
+ const url = (page.url || "").toLowerCase();
8390
+ 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(
8391
+ page.content.slice(0, 3e3)
8392
+ );
8393
+ }
8394
+ function chooseCompactReadMode(page, fallbackMode) {
8395
+ if (fallbackMode === "results_only") return fallbackMode;
8396
+ if (looksLikeProductPage(page)) return fallbackMode;
8397
+ const candidates = getCompactPrimaryResultLinks(page, { max: 6 });
8398
+ if (candidates.length >= 4) return "results_only";
8399
+ if (candidates.length >= 2 && looksLikeListingPage(page)) return "results_only";
8400
+ return fallbackMode;
8401
+ }
8402
+ const MAX_RESULTS = 6;
8403
+ const MAX_CONTROLS = 8;
8404
+ const MAX_FIELDS = 8;
8405
+ const MAX_HEADINGS = 5;
8406
+ const MAX_TEXT_CHARS = 420;
8407
+ function compactText(value, max = MAX_TEXT_CHARS) {
8408
+ const text = (value || "").replace(/\s+/g, " ").trim();
8409
+ if (!text) return "";
8410
+ return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
8411
+ }
8412
+ function isVisibleElement(element) {
8413
+ return element.visible !== false && element.inViewport !== false && element.blockedByOverlay !== true && element.obscured !== true;
8414
+ }
8415
+ function elementLabel(element) {
8416
+ return compactText(
8417
+ element.text || element.label || element.placeholder || element.name || element.href || element.description,
8418
+ 96
8419
+ ) || "Element";
8420
+ }
8421
+ function formatElement(element) {
8422
+ const prefix = element.index != null ? `[#${element.index}] ` : "";
8423
+ const kind = element.type === "input" ? `${element.inputType || "text"} input` : element.type === "select" ? "select" : element.type;
8424
+ const href = element.type === "link" && element.href ? ` -> ${element.href}` : "";
8425
+ return `${prefix}${elementLabel(element)} (${kind})${href}`;
8426
+ }
8427
+ function uniqueElements(elements) {
8428
+ const seen = /* @__PURE__ */ new Set();
8429
+ return elements.filter((element) => {
8430
+ const key = `${element.index ?? ""}|${element.type}|${elementLabel(element)}|${element.href ?? ""}`;
8431
+ if (seen.has(key)) return false;
8432
+ seen.add(key);
8433
+ return true;
8434
+ });
8435
+ }
8436
+ function isPaginationLike(element) {
8437
+ const text = `${element.text || ""} ${element.label || ""}`.toLowerCase();
8438
+ return /\b(next|prev|previous|load more|more results)\b/.test(text);
8439
+ }
8440
+ function getPrimaryResultLinks(page) {
8441
+ return uniqueElements(
8442
+ getCompactPrimaryResultLinks(page, {
8443
+ visibleOnly: true,
8444
+ max: MAX_RESULTS
8445
+ }).filter((element) => !isPaginationLike(element))
8446
+ );
8447
+ }
8448
+ function isPurchaseControl(element) {
8449
+ const text = `${element.text || ""} ${element.label || ""}`.toLowerCase();
8450
+ return /\b(add to cart|add to bag|add to basket|buy now|checkout|view cart)\b/.test(
8451
+ text
8452
+ );
8453
+ }
8454
+ function isAddToCartControl(element) {
8455
+ const text = `${element.text || ""} ${element.label || ""}`.toLowerCase();
8456
+ return /\badd to cart|add to bag|add to basket\b/.test(text);
8457
+ }
8458
+ function looksLikeProductDetailPage(page) {
8459
+ return /\/book\//i.test(page.url) || /\bbook\b/i.test(page.title);
8460
+ }
8461
+ function hasCartConfirmationState(page) {
8462
+ const haystack = compactText(
8463
+ [
8464
+ page.url,
8465
+ page.title,
8466
+ page.excerpt,
8467
+ page.content.slice(0, 1200),
8468
+ page.overlays.map((overlay) => overlay.label || overlay.message || overlay.text || overlay.kind || "").join(" ")
8469
+ ].filter(Boolean).join(" "),
8470
+ 1600
8471
+ ).toLowerCase();
8472
+ 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(
8473
+ haystack
8474
+ );
8475
+ }
8476
+ function getVisibleControls(page) {
8477
+ return uniqueElements(page.interactiveElements.filter(isVisibleElement)).slice(
8478
+ 0,
8479
+ MAX_CONTROLS
8480
+ );
8481
+ }
8482
+ function getVisiblePurchaseControls(page) {
8483
+ return uniqueElements(
8484
+ page.interactiveElements.filter(isVisibleElement).filter(isPurchaseControl).sort((a, b) => (a.index ?? Number.MAX_SAFE_INTEGER) - (b.index ?? Number.MAX_SAFE_INTEGER))
8485
+ ).slice(0, 6);
8486
+ }
8487
+ function getOffscreenPurchaseControls(page) {
8488
+ const visibleKeys = new Set(getVisiblePurchaseControls(page).map(controlKey));
8489
+ return uniqueElements(
8490
+ 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))
8491
+ ).slice(0, 4);
8492
+ }
8493
+ function controlKey(element) {
8494
+ return `${element.index ?? ""}|${element.type}|${elementLabel(element)}|${element.href ?? ""}`;
8495
+ }
8496
+ function isLowValueListingControl(element) {
8497
+ const label = elementLabel(element).toLowerCase();
8498
+ return /\b(filter|sort|format|price|availability|signed edition|binding|language|refine|clear all|remove filter)\b/.test(
8499
+ label
8500
+ );
8501
+ }
8502
+ function getVisibleFormFields(page) {
8503
+ return uniqueElements(
8504
+ page.forms.flatMap((form) => form.fields).filter(isVisibleElement)
8505
+ ).slice(0, MAX_FIELDS);
8506
+ }
8507
+ function pushSection(lines, title, items) {
8508
+ if (items.length === 0) return;
8509
+ lines.push("");
8510
+ lines.push(title);
8511
+ lines.push(...items.map((item) => `- ${item}`));
8512
+ }
8513
+ function buildTextSnapshot(page) {
8514
+ const excerpt = compactText(page.excerpt);
8515
+ if (excerpt) return [excerpt];
8516
+ const content = compactText(page.content);
8517
+ return content ? [content] : [];
8518
+ }
8519
+ function buildCompactScopedContext(page, mode, pageType = detectPageType(page)) {
8520
+ const lines = [
8521
+ `**URL:** ${page.url}`,
8522
+ `**Title:** ${page.title}`,
8523
+ `**Page Type:** ${pageType}`,
8524
+ `**Mode:** ${mode}`
8525
+ ];
8526
+ if (page.byline) {
8527
+ lines.push(`**Author:** ${compactText(page.byline, 120)}`);
8528
+ }
8529
+ const warnings = (page.pageIssues || []).slice(0, 3).map((issue) => compactText(issue.summary, 140));
8530
+ pushSection(lines, "### Access Warnings", warnings);
8531
+ const blockingOverlays = page.overlays.filter((overlay) => overlay.blocksInteraction).slice(0, 3).map(
8532
+ (overlay) => compactText(
8533
+ overlay.label || overlay.message || overlay.text || overlay.kind || overlay.type,
8534
+ 140
8535
+ )
8536
+ );
8537
+ pushSection(lines, "### Immediate Blockers", blockingOverlays);
8538
+ const visiblePurchaseControls = getVisiblePurchaseControls(page);
8539
+ const offscreenPurchaseControls = getOffscreenPurchaseControls(page);
8540
+ const purchaseControls = visiblePurchaseControls.map(formatElement);
8541
+ const addToCartVisible = visiblePurchaseControls.some(isAddToCartControl);
8542
+ const addToCartOffscreen = offscreenPurchaseControls.some(isAddToCartControl);
8543
+ if (looksLikeProductDetailPage(page) && !hasCartConfirmationState(page)) {
8544
+ if (addToCartVisible) {
8545
+ pushSection(lines, "### Action Status", [
8546
+ "Product detail page open. This item is not in the cart yet.",
8547
+ "Click Add to Cart and wait for cart confirmation before moving on."
8548
+ ]);
8549
+ } else if (addToCartOffscreen) {
8550
+ pushSection(lines, "### Action Status", [
8551
+ "Product detail page open. This item is not in the cart yet.",
8552
+ "Add to Cart is present but outside the current viewport.",
8553
+ "Scroll once or use the offscreen purchase control below, then wait for cart confirmation."
8554
+ ]);
8555
+ }
8556
+ }
8557
+ pushSection(lines, "### Visible Purchase Controls", purchaseControls);
8558
+ pushSection(
8559
+ lines,
8560
+ "### Offscreen Purchase Actions",
8561
+ offscreenPurchaseControls.map(formatElement)
8562
+ );
8563
+ const primaryResultElements = getPrimaryResultLinks(page);
8564
+ const primaryResults = primaryResultElements.map(formatElement);
8565
+ if (primaryResults.length > 0) {
8566
+ pushSection(
8567
+ lines,
8568
+ "### Primary Results",
8569
+ primaryResults
8570
+ );
8571
+ }
8572
+ if (pageType === "FORM" || pageType === "LOGIN" || mode === "forms_only") {
8573
+ pushSection(
8574
+ lines,
8575
+ "### Form Fields",
8576
+ getVisibleFormFields(page).map(formatElement)
8577
+ );
8578
+ }
8579
+ if (mode === "visible_only" || mode === "interactives_only" || pageType === "SEARCH_READY" || pageType === "GENERAL") {
8580
+ const primaryResultKeys = new Set(primaryResultElements.map(controlKey));
8581
+ const visibleControls = getVisibleControls(page).filter((element) => !primaryResultKeys.has(controlKey(element))).filter(
8582
+ (element) => primaryResultElements.length > 0 ? !isLowValueListingControl(element) : true
8583
+ ).map(formatElement);
8584
+ pushSection(
8585
+ lines,
8586
+ "### Visible Controls",
8587
+ visibleControls
8588
+ );
8589
+ }
8590
+ const headingItems = page.headings.slice(0, MAX_HEADINGS).map((heading) => `H${heading.level}: ${compactText(heading.text, 100)}`);
8591
+ pushSection(lines, "### Top Headings", headingItems);
8592
+ if (mode === "summary" || mode === "text_only" || lines.length <= 6) {
8593
+ pushSection(lines, "### Text Snapshot", buildTextSnapshot(page));
8594
+ }
8595
+ lines.push("");
8596
+ lines.push(
8597
+ `Stats: ${page.interactiveElements.length} interactives, ${page.forms.length} forms, ${page.navigation.length} nav links, ${page.headings.length} headings`
8598
+ );
8599
+ return lines.join("\n");
8600
+ }
7852
8601
  const WRAPPING_QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
7853
8602
  function stripWrappingQuotes(value) {
7854
8603
  const trimmed = value.trim();
@@ -7994,10 +8743,11 @@ const TOOL_DEFINITIONS = [
7994
8743
  {
7995
8744
  name: "click",
7996
8745
  title: "Click Element",
7997
- description: "Click an element on the page by its index number or CSS selector. Use this to check or uncheck checkboxes and to select radio buttons do NOT use select_option for those.",
8746
+ 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
8747
  inputSchema: {
7999
8748
  index: zod.z.number().optional().describe("Element index from the page content listing"),
8000
- selector: zod.z.string().optional().describe("CSS selector as fallback")
8749
+ selector: zod.z.string().optional().describe("CSS selector as fallback"),
8750
+ text: zod.z.string().optional().describe("Visible label, link text, button text, or section name to match")
8001
8751
  },
8002
8752
  tier: 0
8003
8753
  },
@@ -8118,10 +8868,11 @@ const TOOL_DEFINITIONS = [
8118
8868
  {
8119
8869
  name: "inspect_element",
8120
8870
  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. Use this instead of reading the whole page when you only need local context.",
8871
+ 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
8872
  inputSchema: {
8123
8873
  index: zod.z.number().optional().describe("Element index to inspect"),
8124
8874
  selector: zod.z.string().optional().describe("CSS selector to inspect"),
8875
+ text: zod.z.string().optional().describe("Visible label or section text to locate before inspecting"),
8125
8876
  limit: zod.z.number().optional().describe("Maximum nearby controls to include (default 8)")
8126
8877
  },
8127
8878
  tier: 1,
@@ -8473,10 +9224,11 @@ const TOOL_DEFINITIONS = [
8473
9224
  {
8474
9225
  name: "scroll_to_element",
8475
9226
  title: "Scroll To Element",
8476
- description: "Scroll a specific element into view by index or selector. Useful for navigating to off-screen content.",
9227
+ 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
9228
  inputSchema: {
8478
9229
  index: zod.z.number().optional().describe("Element index to scroll to"),
8479
9230
  selector: zod.z.string().optional().describe("CSS selector to scroll to"),
9231
+ text: zod.z.string().optional().describe("Visible label or section text to scroll into view"),
8480
9232
  position: zod.z.enum(["center", "top", "bottom"]).optional().describe(
8481
9233
  "Where to position the element in the viewport (default center)"
8482
9234
  )
@@ -9776,6 +10528,340 @@ function deleteNamedSession(name) {
9776
10528
  fs$1.unlinkSync(filePath);
9777
10529
  return true;
9778
10530
  }
10531
+ function isInvalidTextTargetQuery(rawQuery) {
10532
+ const trimmed = String(rawQuery || "").trim();
10533
+ if (!trimmed) return true;
10534
+ if (/<\/?[a-z][^>]*>/i.test(trimmed)) return true;
10535
+ if (/^&lt;\/?[a-z][^&]*&gt;$/i.test(trimmed)) return true;
10536
+ if (/^<\/?[a-z][a-z0-9:-]*>$/i.test(trimmed)) return true;
10537
+ return false;
10538
+ }
10539
+ function resolveTextTargetInDocument(doc, rawQuery, mode) {
10540
+ function normalize(value) {
10541
+ return String(value || "").toLowerCase().replace(/\s+/g, " ").trim();
10542
+ }
10543
+ function text(value) {
10544
+ return String(value || "").replace(/\s+/g, " ").trim();
10545
+ }
10546
+ function escapeSelectorValue(value) {
10547
+ const cssObject = typeof CSS !== "undefined" ? CSS : void 0;
10548
+ if (cssObject && typeof cssObject.escape === "function") {
10549
+ return cssObject.escape(value);
10550
+ }
10551
+ return String(value).replace(/["\\]/g, "\\$&");
10552
+ }
10553
+ function uniqueSelector(candidate) {
10554
+ if (!candidate) return null;
10555
+ try {
10556
+ return doc.querySelectorAll(candidate).length === 1 ? candidate : null;
10557
+ } catch {
10558
+ return null;
10559
+ }
10560
+ }
10561
+ function uniqueAttributeSelector(el, attribute) {
10562
+ const value = text(el.getAttribute(attribute));
10563
+ if (!value) return null;
10564
+ const candidate = `${el.tagName.toLowerCase()}[${attribute}="${escapeSelectorValue(value)}"]`;
10565
+ return uniqueSelector(candidate);
10566
+ }
10567
+ function selectorFor(el) {
10568
+ if (!el) return null;
10569
+ const htmlEl = el;
10570
+ if (htmlEl.id) return `#${escapeSelectorValue(htmlEl.id)}`;
10571
+ for (const attribute of [
10572
+ "data-testid",
10573
+ "name",
10574
+ "aria-label",
10575
+ "title",
10576
+ "href"
10577
+ ]) {
10578
+ const candidate2 = uniqueAttributeSelector(el, attribute);
10579
+ if (candidate2) return candidate2;
10580
+ }
10581
+ const parts = [];
10582
+ let current = el;
10583
+ while (current) {
10584
+ const currentEl = current;
10585
+ if (currentEl.id) {
10586
+ parts.unshift(`#${escapeSelectorValue(currentEl.id)}`);
10587
+ break;
10588
+ }
10589
+ const parent = current.parentElement;
10590
+ const tag = current.tagName.toLowerCase();
10591
+ if (!parent) {
10592
+ parts.unshift(tag);
10593
+ break;
10594
+ }
10595
+ const siblings = Array.from(parent.children).filter(
10596
+ (child) => child.tagName === current.tagName
10597
+ );
10598
+ const index = siblings.indexOf(current) + 1;
10599
+ parts.unshift(
10600
+ siblings.length > 1 ? `${tag}:nth-of-type(${index})` : tag
10601
+ );
10602
+ current = parent;
10603
+ }
10604
+ const candidate = parts.join(" > ");
10605
+ return uniqueSelector(candidate) || candidate;
10606
+ }
10607
+ function isVisible2(el) {
10608
+ if (!(el instanceof HTMLElement)) return false;
10609
+ if (el.hidden || el.getAttribute("aria-hidden") === "true") return false;
10610
+ const style = typeof getComputedStyle === "function" ? getComputedStyle(el) : null;
10611
+ if (style && (style.display === "none" || style.visibility === "hidden" || style.opacity === "0")) {
10612
+ return false;
10613
+ }
10614
+ return true;
10615
+ }
10616
+ function inViewport(el) {
10617
+ if (!(el instanceof HTMLElement)) return false;
10618
+ if (typeof el.getBoundingClientRect !== "function") return true;
10619
+ const rect = el.getBoundingClientRect();
10620
+ if (rect.width === 0 && rect.height === 0) return true;
10621
+ const vw = window.innerWidth || doc.documentElement?.clientWidth || 0;
10622
+ const vh = window.innerHeight || doc.documentElement?.clientHeight || 0;
10623
+ return rect.bottom > 0 && rect.right > 0 && rect.top < vh && rect.left < vw;
10624
+ }
10625
+ function labelFor(el) {
10626
+ const htmlEl = el;
10627
+ return text(
10628
+ htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || htmlEl.getAttribute("name") || htmlEl.getAttribute("placeholder") || ("value" in htmlEl ? String(htmlEl.value || "") : "") || htmlEl.textContent
10629
+ ) || el.tagName.toLowerCase();
10630
+ }
10631
+ function contentFor(el) {
10632
+ const ariaLabel = text(el.getAttribute?.("aria-label"));
10633
+ const title = text(el.getAttribute?.("title"));
10634
+ const ownText = text(el.textContent).slice(0, 300);
10635
+ return [ariaLabel, title, ownText].filter(Boolean).join(" ");
10636
+ }
10637
+ function scoreText(query2, candidate) {
10638
+ const normalizedCandidate = normalize(candidate);
10639
+ if (!normalizedCandidate) return -1;
10640
+ if (normalizedCandidate === query2) return 180;
10641
+ if (normalizedCandidate.startsWith(query2)) return 150;
10642
+ if (normalizedCandidate.includes(query2)) {
10643
+ return 130 - Math.min(30, normalizedCandidate.length - query2.length);
10644
+ }
10645
+ const words = query2.split(" ").filter((word) => word.length >= 3);
10646
+ if (words.length === 0) return -1;
10647
+ const overlap = words.filter((word) => normalizedCandidate.includes(word));
10648
+ if (overlap.length === 0) return -1;
10649
+ return overlap.length * 18;
10650
+ }
10651
+ function interactiveBonus(el) {
10652
+ const htmlEl = el;
10653
+ const tag = el.tagName.toLowerCase();
10654
+ const label = normalize(labelFor(el));
10655
+ let score = 0;
10656
+ if (tag === "button") score += 40;
10657
+ if (tag === "a") score += 35;
10658
+ if (tag === "input") score += 20;
10659
+ if (htmlEl.getAttribute("role") === "button") score += 25;
10660
+ if (/\b(shop|view|see|explore|browse|open|details|collection|discover)\b/.test(label)) {
10661
+ score += 30;
10662
+ }
10663
+ if (inViewport(el)) score += 25;
10664
+ return score;
10665
+ }
10666
+ function regionBonus(el) {
10667
+ const tag = el.tagName.toLowerCase();
10668
+ let score = 0;
10669
+ if (/^h[1-4]$/.test(tag)) score += 45;
10670
+ if (tag === "section" || tag === "article" || tag === "main") score += 30;
10671
+ if (el.getAttribute("role") === "heading") score += 35;
10672
+ if (inViewport(el)) score += 20;
10673
+ return score;
10674
+ }
10675
+ function consider(best, el, score, matchedText) {
10676
+ if (!Number.isFinite(score) || score < 0) return best;
10677
+ if (!best || score > best.score) return { el, score, matchedText };
10678
+ return best;
10679
+ }
10680
+ if (isInvalidTextTargetQuery(rawQuery)) return null;
10681
+ const query = normalize(rawQuery);
10682
+ if (!query) return null;
10683
+ let bestInteractive = null;
10684
+ const interactiveSelector = "a[href], button, [role='button'], input[type='submit'], input[type='button'], input[type='radio'], input[type='checkbox'], select, textarea";
10685
+ doc.querySelectorAll(interactiveSelector).forEach((el) => {
10686
+ if (!isVisible2(el)) return;
10687
+ const matchedText = labelFor(el);
10688
+ const score = scoreText(query, matchedText) + scoreText(query, contentFor(el)) + interactiveBonus(el);
10689
+ bestInteractive = consider(bestInteractive, el, score, matchedText);
10690
+ });
10691
+ if (mode === "interactive" && bestInteractive && bestInteractive.score >= 120) {
10692
+ const selector2 = selectorFor(bestInteractive.el);
10693
+ if (selector2) {
10694
+ return {
10695
+ selector: selector2,
10696
+ label: labelFor(bestInteractive.el),
10697
+ kind: bestInteractive.el.tagName.toLowerCase(),
10698
+ matchedText: bestInteractive.matchedText
10699
+ };
10700
+ }
10701
+ }
10702
+ let bestRegion = null;
10703
+ const regionSelector = "h1, h2, h3, h4, [role='heading'], section, article, main, aside, li, [data-testid], div";
10704
+ let seenRegions = 0;
10705
+ doc.querySelectorAll(regionSelector).forEach((el) => {
10706
+ if (seenRegions >= 400 || !isVisible2(el)) return;
10707
+ seenRegions += 1;
10708
+ const matchedText = contentFor(el);
10709
+ const score = scoreText(query, matchedText) + regionBonus(el);
10710
+ bestRegion = consider(bestRegion, el, score, matchedText);
10711
+ });
10712
+ if (!bestRegion || bestRegion.score < 80) {
10713
+ if (mode === "interactive" && bestInteractive) {
10714
+ const selector2 = selectorFor(bestInteractive.el);
10715
+ if (!selector2) return null;
10716
+ return {
10717
+ selector: selector2,
10718
+ label: labelFor(bestInteractive.el),
10719
+ kind: bestInteractive.el.tagName.toLowerCase(),
10720
+ matchedText: bestInteractive.matchedText
10721
+ };
10722
+ }
10723
+ return null;
10724
+ }
10725
+ if (mode === "context") {
10726
+ const selector2 = selectorFor(bestRegion.el);
10727
+ if (!selector2) return null;
10728
+ return {
10729
+ selector: selector2,
10730
+ label: labelFor(bestRegion.el),
10731
+ kind: bestRegion.el.tagName.toLowerCase(),
10732
+ matchedText: bestRegion.matchedText
10733
+ };
10734
+ }
10735
+ let regionAction = null;
10736
+ bestRegion.el.querySelectorAll(interactiveSelector).forEach((el) => {
10737
+ if (!isVisible2(el)) return;
10738
+ const matchedText = labelFor(el);
10739
+ const score = scoreText(query, matchedText) + interactiveBonus(el) + (bestRegion ? Math.floor(bestRegion.score / 4) : 0);
10740
+ regionAction = consider(regionAction, el, score, matchedText);
10741
+ });
10742
+ const chosen = regionAction || bestInteractive;
10743
+ if (!chosen) return null;
10744
+ const selector = selectorFor(chosen.el);
10745
+ if (!selector) return null;
10746
+ return {
10747
+ selector,
10748
+ label: labelFor(chosen.el),
10749
+ kind: chosen.el.tagName.toLowerCase(),
10750
+ matchedText: chosen.matchedText
10751
+ };
10752
+ }
10753
+ function normalizeWhitespace(text) {
10754
+ return text.replace(/\r/g, "").replace(/\n{3,}/g, "\n\n").trim();
10755
+ }
10756
+ function limitText(text, maxLines, maxChars) {
10757
+ const normalized = normalizeWhitespace(text);
10758
+ const lines = normalized.split("\n");
10759
+ const trimmedLines = lines.slice(0, maxLines);
10760
+ let limited = trimmedLines.join("\n");
10761
+ if (limited.length > maxChars) {
10762
+ limited = `${limited.slice(0, maxChars - 1)}…`;
10763
+ } else if (lines.length > maxLines) {
10764
+ limited += "\n…";
10765
+ }
10766
+ return limited;
10767
+ }
10768
+ function extractSection(text, heading) {
10769
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10770
+ const match = text.match(
10771
+ new RegExp(`${escaped}\\n([\\s\\S]*?)(?=\\n### |\\n## |$)`)
10772
+ );
10773
+ if (!match) return null;
10774
+ return `${heading}
10775
+ ${match[1].trim()}`;
10776
+ }
10777
+ function compactReadPageResult(text) {
10778
+ const cleaned = normalizeWhitespace(
10779
+ text.replace(
10780
+ /\n+Need more detail\? Escalate with read_page\(mode="debug"\) only if the narrow modes are insufficient\.\s*$/s,
10781
+ ""
10782
+ )
10783
+ );
10784
+ const readHeader = cleaned.match(/^\[read_page mode=[^\]]+\]/m)?.[0];
10785
+ const mandatoryHeadings = [
10786
+ "### Action Status",
10787
+ "### Visible Purchase Controls",
10788
+ "### Offscreen Purchase Actions"
10789
+ ];
10790
+ const optionalHeadings = [
10791
+ "### Access Warnings",
10792
+ "### Immediate Blockers",
10793
+ "### Likely Search Results",
10794
+ "### Primary Results",
10795
+ "### Form Fields",
10796
+ "### Visible Controls",
10797
+ "### Top Headings",
10798
+ "### Text Snapshot"
10799
+ ];
10800
+ const mandatorySections = mandatoryHeadings.map((heading) => extractSection(cleaned, heading)).filter((value) => Boolean(value));
10801
+ const optionalSections = optionalHeadings.map((heading) => extractSection(cleaned, heading)).filter((value) => Boolean(value));
10802
+ const allSections = [...mandatorySections, ...optionalSections];
10803
+ if (allSections.length === 0) {
10804
+ return limitText(cleaned, 22, 1800);
10805
+ }
10806
+ const metaLines = cleaned.split("\n").filter((line) => /^\*\*(URL|Title|Page Type|Mode|Author):\*\*/.test(line)).slice(0, 5);
10807
+ const maxOptional = Math.max(0, 5 - mandatorySections.length);
10808
+ const keptSections = [
10809
+ ...mandatorySections,
10810
+ ...optionalSections.slice(0, maxOptional)
10811
+ ];
10812
+ return [
10813
+ readHeader,
10814
+ metaLines.join("\n"),
10815
+ keptSections.join("\n\n")
10816
+ ].filter(Boolean).join("\n\n");
10817
+ }
10818
+ function compactSearchLikeResult(text) {
10819
+ const cleaned = normalizeWhitespace(text);
10820
+ const marker = "\nSearch results snapshot:\n";
10821
+ const markerIndex = cleaned.indexOf(marker);
10822
+ if (markerIndex === -1) {
10823
+ return limitText(cleaned, 16, 1400);
10824
+ }
10825
+ const summary = cleaned.slice(0, markerIndex).trim();
10826
+ const snapshot = cleaned.slice(markerIndex + marker.length).trim();
10827
+ return [summary, compactReadPageResult(snapshot)].filter(Boolean).join("\n\n");
10828
+ }
10829
+ function compactCurrentTabResult(text) {
10830
+ try {
10831
+ const parsed = JSON.parse(text);
10832
+ if (!parsed || typeof parsed !== "object") return limitText(text, 8, 500);
10833
+ return [
10834
+ `Current tab: ${parsed.title || "(untitled)"}`,
10835
+ parsed.url ? `URL: ${parsed.url}` : "",
10836
+ `State: loading=${parsed.isLoading ? "yes" : "no"}, back=${parsed.canGoBack ? "yes" : "no"}, forward=${parsed.canGoForward ? "yes" : "no"}`
10837
+ ].filter(Boolean).join("\n");
10838
+ } catch {
10839
+ return limitText(text, 8, 500);
10840
+ }
10841
+ }
10842
+ function looksLikeRichToolResult(text) {
10843
+ return text.startsWith("{") && text.includes('"__richResult":true');
10844
+ }
10845
+ function formatCompactToolResult(name, result) {
10846
+ if (!result || looksLikeRichToolResult(result)) return result;
10847
+ switch (name) {
10848
+ case "current_tab":
10849
+ return compactCurrentTabResult(result);
10850
+ case "read_page":
10851
+ return compactReadPageResult(result);
10852
+ case "search":
10853
+ case "navigate":
10854
+ case "go_back":
10855
+ case "go_forward":
10856
+ case "paginate":
10857
+ case "wait_for_navigation":
10858
+ return compactSearchLikeResult(result);
10859
+ case "list_tabs":
10860
+ return limitText(result, 10, 900);
10861
+ default:
10862
+ return limitText(result, 18, 1400);
10863
+ }
10864
+ }
9779
10865
  const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS = 1500;
9780
10866
  const QUIET_NAVIGATION_WINDOW_MS = 1200;
9781
10867
  const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
@@ -10132,6 +11218,25 @@ ${truncated}`;
10132
11218
  Search results snapshot unavailable. Use read_page(mode="results_only") if needed.` : `
10133
11219
  Search results snapshot unavailable. Use read_page(mode="results_only") if needed.`;
10134
11220
  }
11221
+ async function getPostClickNavSummary(wc, toolProfile) {
11222
+ try {
11223
+ const content = await Promise.race([
11224
+ extractContent(wc),
11225
+ new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
11226
+ ]);
11227
+ if (content && content.content.length > 0) {
11228
+ const scoped = toolProfile === "compact" ? buildCompactScopedContext(content, "visible_only") : buildScopedContext(content, "visible_only");
11229
+ const maxLen = toolProfile === "compact" ? 1800 : 3e3;
11230
+ const truncated = scoped.length > maxLen ? `${scoped.slice(0, maxLen)}
11231
+ [Page snapshot truncated. Use read_page for full details.]` : scoped;
11232
+ return `
11233
+ Page snapshot after navigation:
11234
+ ${truncated}`;
11235
+ }
11236
+ } catch {
11237
+ }
11238
+ return "";
11239
+ }
10135
11240
  async function scrollPage$1(wc, deltaY) {
10136
11241
  const getScrollY = async () => {
10137
11242
  const scrollY = await executePageScript(
@@ -10324,9 +11429,17 @@ async function describeElementForClick$1(wc, selector) {
10324
11429
  if (!el) return { error: "Element not found" };
10325
11430
  const anchor = el instanceof HTMLAnchorElement ? el : el.closest("a[href]");
10326
11431
  const text = (el.textContent || el.tagName || "Element").trim().slice(0, 100);
11432
+ const tag = el.tagName.toLowerCase();
11433
+ const interactiveTags = new Set(["a","button","input","select","textarea","summary","details","option"]);
11434
+ const hasRole = el.getAttribute("role") === "button" || el.getAttribute("role") === "link" || el.getAttribute("role") === "tab";
11435
+ const hasClickListener = el.onclick != null || el.getAttribute("onclick") != null;
11436
+ const isInteractive = interactiveTags.has(tag) || hasRole || hasClickListener || !!anchor;
10327
11437
  return {
10328
11438
  text: text || "Element",
10329
11439
  href: anchor instanceof HTMLAnchorElement ? anchor.href : undefined,
11440
+ target: anchor instanceof HTMLAnchorElement ? (anchor.getAttribute("target") || "") : undefined,
11441
+ tag,
11442
+ isInteractive,
10330
11443
  };
10331
11444
  })()
10332
11445
  `,
@@ -10345,7 +11458,10 @@ async function describeElementForClick$1(wc, selector) {
10345
11458
  }
10346
11459
  return {
10347
11460
  text: "text" in result && typeof result.text === "string" ? result.text : "Element",
10348
- href: "href" in result && typeof result.href === "string" ? result.href : void 0
11461
+ href: "href" in result && typeof result.href === "string" ? result.href : void 0,
11462
+ target: "target" in result && typeof result.target === "string" ? result.target : void 0,
11463
+ tag: "tag" in result && typeof result.tag === "string" ? result.tag : void 0,
11464
+ isInteractive: "isInteractive" in result && typeof result.isInteractive === "boolean" ? result.isInteractive : void 0
10349
11465
  };
10350
11466
  }
10351
11467
  async function inspectElement(wc, selector, limit = 8) {
@@ -10429,6 +11545,18 @@ async function inspectElement(wc, selector, limit = 8) {
10429
11545
  ) || "element";
10430
11546
  }
10431
11547
 
11548
+ function purchasePriority(label, href) {
11549
+ const haystack = ((label || "") + " " + (href || ""))
11550
+ .toLowerCase()
11551
+ .replace(/s+/g, " ")
11552
+ .trim();
11553
+ if (!haystack) return null;
11554
+ if (/\badd(?: item)? to (?:cart|bag|basket)\b/.test(haystack)) return 0;
11555
+ if (/\b(?:buy now|preorder|pre-order|reserve now|shop now)\b/.test(haystack)) return 1;
11556
+ if (/\b(?:checkout|view cart|view basket|go to cart|view bag)\b/.test(haystack)) return 2;
11557
+ return null;
11558
+ }
11559
+
10432
11560
  function chooseRegion(target) {
10433
11561
  const preferred = target.closest(
10434
11562
  "[data-testid], article, [role='article'], [role='listitem'], li, tr, form, section, aside, dialog, [role='dialog']"
@@ -10454,19 +11582,69 @@ async function inspectElement(wc, selector, limit = 8) {
10454
11582
  const region = chooseRegion(target);
10455
11583
  const nearby = [];
10456
11584
  const seen = new Set();
11585
+ const purchaseActions = [];
11586
+ const purchaseSeen = new Set();
10457
11587
  region.querySelectorAll("a[href], button, input:not([type='hidden']), select, textarea").forEach((el) => {
10458
11588
  if (!(el instanceof HTMLElement) || !isVisible(el)) return;
10459
11589
  const candidateSelector = selectorFor(el);
10460
11590
  if (!candidateSelector || seen.has(candidateSelector)) return;
10461
11591
  seen.add(candidateSelector);
11592
+ const candidateLabel = labelFor(el).slice(0, 100);
11593
+ const candidateHref = el instanceof HTMLAnchorElement ? text(el.href) : undefined;
10462
11594
  nearby.push({
10463
- label: labelFor(el).slice(0, 100),
11595
+ index: typeof window.__vessel?.getElementIndexBySelector === "function"
11596
+ ? window.__vessel.getElementIndexBySelector(candidateSelector) ?? undefined
11597
+ : undefined,
11598
+ label: candidateLabel,
11599
+ type: el.tagName.toLowerCase(),
11600
+ selector: candidateSelector,
11601
+ href: candidateHref,
11602
+ });
11603
+ const purchaseRank = purchasePriority(candidateLabel, candidateHref);
11604
+ if (purchaseRank !== null && !purchaseSeen.has(candidateSelector)) {
11605
+ purchaseSeen.add(candidateSelector);
11606
+ purchaseActions.push({
11607
+ index: typeof window.__vessel?.getElementIndexBySelector === "function"
11608
+ ? window.__vessel.getElementIndexBySelector(candidateSelector) ?? undefined
11609
+ : undefined,
11610
+ label: candidateLabel,
11611
+ type: el.tagName.toLowerCase(),
11612
+ selector: candidateSelector,
11613
+ href: candidateHref,
11614
+ source: "nearby",
11615
+ rank: purchaseRank,
11616
+ });
11617
+ }
11618
+ });
11619
+
11620
+ document.querySelectorAll("button, a[href], input[type='submit'], input[type='button']").forEach((el) => {
11621
+ if (!(el instanceof HTMLElement) || !isVisible(el)) return;
11622
+ const candidateSelector = selectorFor(el);
11623
+ if (!candidateSelector || purchaseSeen.has(candidateSelector)) return;
11624
+ const candidateLabel = labelFor(el).slice(0, 100);
11625
+ const candidateHref = el instanceof HTMLAnchorElement ? text(el.href) : undefined;
11626
+ const purchaseRank = purchasePriority(candidateLabel, candidateHref);
11627
+ if (purchaseRank === null) return;
11628
+ purchaseSeen.add(candidateSelector);
11629
+ purchaseActions.push({
11630
+ index: typeof window.__vessel?.getElementIndexBySelector === "function"
11631
+ ? window.__vessel.getElementIndexBySelector(candidateSelector) ?? undefined
11632
+ : undefined,
11633
+ label: candidateLabel,
10464
11634
  type: el.tagName.toLowerCase(),
10465
11635
  selector: candidateSelector,
10466
- href: el instanceof HTMLAnchorElement ? text(el.href) : undefined,
11636
+ href: candidateHref,
11637
+ source: "page",
11638
+ rank: purchaseRank,
10467
11639
  });
10468
11640
  });
10469
11641
 
11642
+ purchaseActions.sort((a, b) => {
11643
+ if (a.rank !== b.rank) return a.rank - b.rank;
11644
+ if (a.source !== b.source) return a.source === "nearby" ? -1 : 1;
11645
+ return a.label.localeCompare(b.label);
11646
+ });
11647
+
10470
11648
  return {
10471
11649
  target: {
10472
11650
  label: labelFor(target).slice(0, 120),
@@ -10483,6 +11661,14 @@ async function inspectElement(wc, selector, limit = 8) {
10483
11661
  text: text(region.textContent)?.slice(0, 400),
10484
11662
  },
10485
11663
  nearby: nearby.slice(0, ${Math.max(1, Math.min(20, limit))}),
11664
+ purchaseActions: purchaseActions.slice(0, 8).map((item) => ({
11665
+ index: item.index,
11666
+ label: item.label,
11667
+ type: item.type,
11668
+ selector: item.selector,
11669
+ href: item.href,
11670
+ source: item.source,
11671
+ })),
10486
11672
  };
10487
11673
  })()
10488
11674
  `,
@@ -10515,11 +11701,26 @@ async function inspectElement(wc, selector, limit = 8) {
10515
11701
  lines.push("Nearby controls:");
10516
11702
  for (const item of result.nearby) {
10517
11703
  const hrefSuffix = item.href ? ` -> ${item.href}` : "";
11704
+ const indexPrefix = typeof item.index === "number" ? `[#${item.index}] ` : "";
10518
11705
  lines.push(
10519
- `- ${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
11706
+ `- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
10520
11707
  );
10521
11708
  }
10522
11709
  }
11710
+ if (Array.isArray(result.purchaseActions) && result.purchaseActions.length > 0) {
11711
+ lines.push("Likely purchase actions:");
11712
+ for (const item of result.purchaseActions) {
11713
+ const hrefSuffix = item.href ? ` -> ${item.href}` : "";
11714
+ const sourceSuffix = item.source === "nearby" ? " (same region)" : " (elsewhere on page)";
11715
+ const indexPrefix = typeof item.index === "number" ? `[#${item.index}] ` : "";
11716
+ lines.push(
11717
+ `- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}${sourceSuffix}`
11718
+ );
11719
+ }
11720
+ lines.push(
11721
+ "When an index is available, prefer click(index=N) over selector-based clicks because it is more stable."
11722
+ );
11723
+ }
10523
11724
  return lines.join("\n");
10524
11725
  }
10525
11726
  async function getLocaleSnapshot(wc) {
@@ -10607,6 +11808,11 @@ const ADD_TO_CART_PATTERNS = [
10607
11808
  ];
10608
11809
  const recentCartClicks = /* @__PURE__ */ new Map();
10609
11810
  const CART_CLICK_COOLDOWN_MS = 15e3;
11811
+ const CART_ADDED_TTL_MS = 30 * 6e4;
11812
+ const cartAddedProducts = /* @__PURE__ */ new Map();
11813
+ let clickStreakUrl = null;
11814
+ let clickStreakCount = 0;
11815
+ const CLICK_STREAK_THRESHOLD = 3;
10610
11816
  function isAddToCartText(text) {
10611
11817
  const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
10612
11818
  return ADD_TO_CART_PATTERNS.some((p) => normalized.includes(p));
@@ -10628,18 +11834,113 @@ function isDuplicateCartClick(url, text) {
10628
11834
  }
10629
11835
  return isAddToCartText(text);
10630
11836
  }
11837
+ async function getProductPageTitle(wc) {
11838
+ try {
11839
+ const heading = await executePageScript(
11840
+ wc,
11841
+ `(function() {
11842
+ var h1 = document.querySelector('h1');
11843
+ if (h1 && h1.textContent.trim().length > 3 && h1.textContent.trim().length < 200) {
11844
+ return h1.textContent.trim();
11845
+ }
11846
+ var meta = document.querySelector('meta[property="og:title"]');
11847
+ if (meta && meta.content && meta.content.trim().length > 3) {
11848
+ return meta.content.trim();
11849
+ }
11850
+ return '';
11851
+ })()`,
11852
+ { timeoutMs: 800, label: "get product title" }
11853
+ );
11854
+ if (heading && heading !== PAGE_SCRIPT_TIMEOUT && typeof heading === "string" && heading.length > 0) {
11855
+ return heading;
11856
+ }
11857
+ } catch {
11858
+ }
11859
+ return wc.getTitle() || "";
11860
+ }
11861
+ function normalizeCartProductKey(url) {
11862
+ try {
11863
+ const parsed = new URL(url);
11864
+ const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
11865
+ return `${parsed.origin}${pathname}`;
11866
+ } catch {
11867
+ return url;
11868
+ }
11869
+ }
11870
+ function pruneCartAddedProducts(now = Date.now()) {
11871
+ for (const [key, entry] of cartAddedProducts) {
11872
+ if (now - entry.ts > CART_ADDED_TTL_MS) {
11873
+ cartAddedProducts.delete(key);
11874
+ }
11875
+ }
11876
+ }
11877
+ function cartOrigin(url) {
11878
+ if (!url) return null;
11879
+ try {
11880
+ return new URL(url).origin;
11881
+ } catch {
11882
+ return null;
11883
+ }
11884
+ }
11885
+ function recordProductAddedToCart(url, productName) {
11886
+ pruneCartAddedProducts();
11887
+ cartAddedProducts.set(normalizeCartProductKey(url), {
11888
+ title: productName || url,
11889
+ ts: Date.now()
11890
+ });
11891
+ }
11892
+ function isProductAlreadyInCart(url) {
11893
+ pruneCartAddedProducts();
11894
+ return cartAddedProducts.has(normalizeCartProductKey(url));
11895
+ }
11896
+ function getCartAddedSummary(url) {
11897
+ pruneCartAddedProducts();
11898
+ const origin = cartOrigin(url);
11899
+ const items = Array.from(cartAddedProducts.entries()).filter(([key]) => !origin || key.startsWith(`${origin}/`)).map(([_path, info]) => `- ${info.title}`).join("\n");
11900
+ if (!items) return "";
11901
+ const count = items.split("\n").length;
11902
+ return `
11903
+ Already in cart (${count} items):
11904
+ ${items}`;
11905
+ }
11906
+ async function buildCartSuccessSuffix(wc, productUrl, overlayHint) {
11907
+ const productTitle = await getProductPageTitle(wc);
11908
+ recordProductAddedToCart(productUrl, productTitle);
11909
+ const cartSummary = getCartAddedSummary(productUrl);
11910
+ const dismissResult = await tryAutoDismissCartDialog(wc);
11911
+ if (dismissResult) {
11912
+ return `
11913
+ Item added to cart. ${dismissResult}${cartSummary}
11914
+ Go back to search results to select the next product.`;
11915
+ }
11916
+ if (!overlayHint) {
11917
+ return cartSummary;
11918
+ }
11919
+ const dialogActions = await getCartDialogActions$1(wc);
11920
+ const actionsSuffix = dialogActions ? `
11921
+ ${dialogActions}
11922
+ Click one of these dialog actions. Do NOT click any other element.` : "";
11923
+ return `
11924
+ ${overlayHint}${actionsSuffix}${cartSummary}`;
11925
+ }
10631
11926
  async function clickResolvedSelector$1(wc, selector) {
10632
11927
  if (selector.startsWith("__vessel_idx:")) {
10633
11928
  const idx = Number(selector.slice("__vessel_idx:".length));
10634
11929
  const beforeUrl2 = wc.getURL();
11930
+ let idxCartMatch = false;
10635
11931
  const idxLabel = await executePageScript(
10636
11932
  wc,
10637
11933
  `window.__vessel?.getElementText?.(${idx}) || ""`,
10638
11934
  { label: "shadow element text" }
10639
11935
  );
10640
- if (typeof idxLabel === "string" && isAddToCartText(idxLabel) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
11936
+ if (typeof idxLabel === "string" && (idxCartMatch = isAddToCartText(idxLabel)) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
10641
11937
  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
11938
  }
11939
+ if (idxCartMatch && isProductAlreadyInCart(beforeUrl2)) {
11940
+ const summary = getCartAddedSummary(beforeUrl2);
11941
+ return `Blocked: This product was already added to the cart.${summary}
11942
+ Go back and select a different product.`;
11943
+ }
10643
11944
  const result = await executePageScript(
10644
11945
  wc,
10645
11946
  `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`,
@@ -10649,18 +11950,40 @@ async function clickResolvedSelector$1(wc, selector) {
10649
11950
  );
10650
11951
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
10651
11952
  if (typeof result === "string" && result.startsWith("Error")) return result;
10652
- if (typeof idxLabel === "string" && isAddToCartText(idxLabel)) {
11953
+ if (idxCartMatch) {
10653
11954
  recordCartClick(beforeUrl2, idxLabel);
10654
11955
  }
10655
11956
  await waitForPotentialNavigation$1(wc, beforeUrl2);
10656
11957
  const afterUrl2 = wc.getURL();
10657
11958
  if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
10658
- const idxOverlay = await detectPostClickOverlay$1(wc);
11959
+ let idxOverlay = await detectPostClickOverlay$1(wc);
11960
+ if (!idxOverlay && idxCartMatch) {
11961
+ await sleep$1(1200);
11962
+ idxOverlay = await detectPostClickOverlay$1(wc);
11963
+ }
11964
+ if (idxCartMatch) {
11965
+ return `${result}${await buildCartSuccessSuffix(wc, beforeUrl2, idxOverlay)}`;
11966
+ }
11967
+ if (!idxOverlay) {
11968
+ const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
11969
+ if (hrefMatch) {
11970
+ try {
11971
+ assertSafeURL(hrefMatch[1]);
11972
+ await wc.loadURL(hrefMatch[1]);
11973
+ await waitForLoad$1(wc, 8e3);
11974
+ const hrefUrl = wc.getURL();
11975
+ if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
11976
+ } catch {
11977
+ }
11978
+ }
11979
+ }
10659
11980
  return idxOverlay ? `${result}
10660
- ${idxOverlay}` : result;
11981
+ ${idxOverlay}` : `${result}
11982
+ Note: Page did not change after click.`;
10661
11983
  }
10662
11984
  if (selector.includes(" >>> ")) {
10663
11985
  const beforeUrl2 = wc.getURL();
11986
+ let shadowCartMatch = false;
10664
11987
  const shadowLabel = await executePageScript(
10665
11988
  wc,
10666
11989
  `(function() {
@@ -10669,17 +11992,24 @@ ${idxOverlay}` : result;
10669
11992
  })()`,
10670
11993
  { label: "shadow element text" }
10671
11994
  );
10672
- if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
11995
+ if (typeof shadowLabel === "string" && (shadowCartMatch = isAddToCartText(shadowLabel)) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
10673
11996
  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
11997
  }
11998
+ if (shadowCartMatch && isProductAlreadyInCart(beforeUrl2)) {
11999
+ const summary = getCartAddedSummary(beforeUrl2);
12000
+ return `Blocked: This product was already added to the cart.${summary}
12001
+ Go back and select a different product.`;
12002
+ }
10675
12003
  const result = await executePageScript(
10676
12004
  wc,
10677
12005
  `
10678
12006
  (function() {
10679
12007
  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.";
12008
+ if (!el || !document.contains(el)) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
10681
12009
  if (el instanceof HTMLElement) { el.focus(); el.click(); }
10682
- return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase());
12010
+ var anchor = el instanceof HTMLAnchorElement ? el : el.closest('a[href]');
12011
+ var href = anchor instanceof HTMLAnchorElement ? anchor.href : null;
12012
+ return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase()) + (href ? "\\nhref: " + href : "");
10683
12013
  })()
10684
12014
  `,
10685
12015
  {
@@ -10688,15 +12018,36 @@ ${idxOverlay}` : result;
10688
12018
  );
10689
12019
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
10690
12020
  if (typeof result === "string" && result.startsWith("Error")) return result;
10691
- if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel)) {
12021
+ if (shadowCartMatch) {
10692
12022
  recordCartClick(beforeUrl2, shadowLabel);
10693
12023
  }
10694
12024
  await waitForPotentialNavigation$1(wc, beforeUrl2);
10695
12025
  const afterUrl2 = wc.getURL();
10696
12026
  if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
10697
- const shadowOverlay = await detectPostClickOverlay$1(wc);
12027
+ let shadowOverlay = await detectPostClickOverlay$1(wc);
12028
+ if (!shadowOverlay && shadowCartMatch) {
12029
+ await sleep$1(1200);
12030
+ shadowOverlay = await detectPostClickOverlay$1(wc);
12031
+ }
12032
+ if (shadowCartMatch) {
12033
+ return `${result}${await buildCartSuccessSuffix(wc, beforeUrl2, shadowOverlay)}`;
12034
+ }
12035
+ if (!shadowOverlay) {
12036
+ const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
12037
+ if (hrefMatch) {
12038
+ try {
12039
+ assertSafeURL(hrefMatch[1]);
12040
+ await wc.loadURL(hrefMatch[1]);
12041
+ await waitForLoad$1(wc, 8e3);
12042
+ const hrefUrl = wc.getURL();
12043
+ if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
12044
+ } catch {
12045
+ }
12046
+ }
12047
+ }
10698
12048
  return shadowOverlay ? `${result}
10699
- ${shadowOverlay}` : result;
12049
+ ${shadowOverlay}` : `${result}
12050
+ Note: Page did not change after click.`;
10700
12051
  }
10701
12052
  const beforeUrl = wc.getURL();
10702
12053
  const elInfo = await describeElementForClick$1(wc, selector);
@@ -10719,10 +12070,16 @@ Click one of these dialog actions instead.`;
10719
12070
  return formatDeadLinkMessage(elInfo.text, validation);
10720
12071
  }
10721
12072
  }
12073
+ if (cartMatch && isProductAlreadyInCart(beforeUrl)) {
12074
+ const summary = getCartAddedSummary(beforeUrl);
12075
+ return `Blocked: This product was already added to the cart.${summary}
12076
+ Go back and select a different product.`;
12077
+ }
10722
12078
  if (cartMatch) {
10723
12079
  recordCartClick(beforeUrl, elInfo.text);
10724
12080
  }
10725
- const clickText = `Clicked: ${elInfo.text}`;
12081
+ const tagLabel = elInfo.tag && elInfo.tag !== "a" && elInfo.tag !== "button" ? ` <${elInfo.tag}>` : "";
12082
+ const clickText = `Clicked: ${elInfo.text}${tagLabel}`;
10726
12083
  const clickResult = await clickElement$1(wc, selector);
10727
12084
  if (clickResult.startsWith("Error:")) return clickResult;
10728
12085
  await waitForPotentialNavigation$1(wc, beforeUrl);
@@ -10732,15 +12089,30 @@ Click one of these dialog actions instead.`;
10732
12089
  }
10733
12090
  const overlayHint = await detectPostClickOverlay$1(wc);
10734
12091
  if (overlayHint) {
10735
- const dialogActions = cartMatch ? await getCartDialogActions$1(wc) : null;
10736
- const actionsSuffix = dialogActions ? `
10737
- ${dialogActions}
10738
- Click one of these dialog actions. Do NOT click any other element.` : "";
12092
+ if (cartMatch) {
12093
+ return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
12094
+ wc,
12095
+ beforeUrl,
12096
+ overlayHint
12097
+ )}`;
12098
+ }
10739
12099
  return `${clickText} (${clickResult})
10740
- ${overlayHint}${actionsSuffix}`;
12100
+ ${overlayHint}`;
10741
12101
  }
10742
12102
  if (cartMatch) {
10743
- return `${clickText} (${clickResult})`;
12103
+ await sleep$1(1200);
12104
+ const delayedOverlayHint = await detectPostClickOverlay$1(wc);
12105
+ if (delayedOverlayHint) {
12106
+ return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
12107
+ wc,
12108
+ beforeUrl,
12109
+ delayedOverlayHint
12110
+ )}`;
12111
+ }
12112
+ return `${clickText} (${clickResult})${await buildCartSuccessSuffix(
12113
+ wc,
12114
+ beforeUrl
12115
+ )}`;
10744
12116
  }
10745
12117
  const activationResult = await activateElement$1(wc, selector);
10746
12118
  if (!activationResult.startsWith("Error:")) {
@@ -10755,7 +12127,65 @@ ${overlayHint}${actionsSuffix}`;
10755
12127
  return `${clickText} (${clickResult})
10756
12128
  ${postActivationOverlayHint}`;
10757
12129
  }
10758
- return `${clickText} (${clickResult})`;
12130
+ const sameTabLinkTarget = typeof elInfo.href === "string" && elInfo.href.trim().length > 0 && (!elInfo.target || !/^_blank$/i.test(elInfo.target.trim()));
12131
+ if (sameTabLinkTarget) {
12132
+ const validation = await validateLinkDestination(elInfo.href);
12133
+ if (validation.status !== "dead") {
12134
+ try {
12135
+ assertSafeURL(elInfo.href);
12136
+ await wc.loadURL(elInfo.href);
12137
+ await waitForLoad$1(wc, 8e3);
12138
+ const hrefFallbackUrl = wc.getURL();
12139
+ if (hrefFallbackUrl !== beforeUrl) {
12140
+ return `${clickText} -> ${hrefFallbackUrl} (recovered via href fallback)`;
12141
+ }
12142
+ } catch {
12143
+ }
12144
+ }
12145
+ }
12146
+ const nonInteractiveWarning = elInfo.isInteractive === false && !elInfo.href ? `
12147
+ 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.` : `
12148
+ Note: Page did not change after click. The element may need a different interaction method. Consider read_page or inspect_element.`;
12149
+ return `${clickText} (${clickResult})${nonInteractiveWarning}`;
12150
+ }
12151
+ async function tryAutoDismissCartDialog(wc) {
12152
+ try {
12153
+ const result = await executePageScript(
12154
+ wc,
12155
+ `
12156
+ (function() {
12157
+ var dialog = document.querySelector('[role="dialog"], dialog[open], [role="alertdialog"], [aria-modal="true"]');
12158
+ if (!dialog) return null;
12159
+ var cs = getComputedStyle(dialog);
12160
+ if (cs.display === 'none' || cs.visibility === 'hidden') return null;
12161
+ var buttons = dialog.querySelectorAll('button, a[href], [role="button"]');
12162
+ var continueBtn = null;
12163
+ var closeBtn = null;
12164
+ for (var i = 0; i < buttons.length; i++) {
12165
+ var label = (buttons[i].getAttribute('aria-label') || buttons[i].textContent || '').trim().toLowerCase();
12166
+ if (/continue shopping|keep shopping|back to shopping/.test(label)) { continueBtn = buttons[i]; break; }
12167
+ if (/close|dismiss|×/.test(label) && !closeBtn) { closeBtn = buttons[i]; }
12168
+ }
12169
+ var target = continueBtn || closeBtn;
12170
+ if (!target) return null;
12171
+ var actionLabel = (target.getAttribute('aria-label') || target.textContent || '').trim();
12172
+ if (target.tagName === 'A' && target.href) {
12173
+ window.location.href = target.href;
12174
+ return "Navigated via: " + actionLabel;
12175
+ }
12176
+ target.click();
12177
+ return "Dismissed dialog via: " + actionLabel;
12178
+ })()
12179
+ `,
12180
+ { timeoutMs: 1500, label: "auto dismiss cart dialog" }
12181
+ );
12182
+ if (result && result !== PAGE_SCRIPT_TIMEOUT && typeof result === "string") {
12183
+ await sleep$1(500);
12184
+ return result;
12185
+ }
12186
+ } catch {
12187
+ }
12188
+ return null;
10759
12189
  }
10760
12190
  async function getCartDialogActions$1(wc) {
10761
12191
  const result = await executePageScript(
@@ -11561,6 +12991,24 @@ async function resolveSelector$1(wc, index, selector) {
11561
12991
  if (extractedSelector) return extractedSelector;
11562
12992
  return null;
11563
12993
  }
12994
+ async function resolveTargetByText(wc, query, mode) {
12995
+ const trimmed = query.trim();
12996
+ if (!trimmed) return null;
12997
+ if (isInvalidTextTargetQuery(trimmed)) return null;
12998
+ const result = await executePageScript(
12999
+ wc,
13000
+ `(${resolveTextTargetInDocument.toString()})(document, ${JSON.stringify(trimmed)}, ${JSON.stringify(mode)})`,
13001
+ {
13002
+ timeoutMs: 2200,
13003
+ label: `resolve ${mode} target by text`
13004
+ }
13005
+ );
13006
+ if (result === PAGE_SCRIPT_TIMEOUT) return PAGE_SCRIPT_TIMEOUT;
13007
+ if (!result || typeof result.selector !== "string" || !result.selector) {
13008
+ return null;
13009
+ }
13010
+ return result.selector;
13011
+ }
11564
13012
  function normalizeFieldToken(value) {
11565
13013
  return typeof value === "string" ? value.trim() : "";
11566
13014
  }
@@ -13011,8 +14459,49 @@ async function getPostActionState$1(ctx, name) {
13011
14459
  if (wc.isLoading()) {
13012
14460
  await waitForLoad$1(wc);
13013
14461
  }
14462
+ const currentUrl = wc.getURL();
14463
+ let warnings = "";
14464
+ if (isProductAlreadyInCart(currentUrl)) {
14465
+ warnings += `
14466
+ WARNING: This product is already in your cart.${getCartAddedSummary(currentUrl)}
14467
+ Go back and select a different product.`;
14468
+ }
14469
+ const taskGoal = ctx.runtime.getState().taskTracker?.goal;
14470
+ if (taskGoal && name === "click") {
14471
+ const drift = shouldBlockOffGoalDomainNavigation(taskGoal, currentUrl);
14472
+ if (drift) {
14473
+ warnings += `
14474
+ WARNING: You drifted to ${drift.targetDomain} but the task requires staying on ${drift.requestedDomain}. Call go_back immediately to return to the previous page.`;
14475
+ }
14476
+ }
14477
+ if (name === "go_back" || name === "search") {
14478
+ const cartSummary = getCartAddedSummary(currentUrl);
14479
+ if (cartSummary) {
14480
+ warnings += `${cartSummary}
14481
+ Select a DIFFERENT product that is not in the cart. Call read_page if needed to see available results.`;
14482
+ }
14483
+ }
13014
14484
  return `
13015
- [state: url=${wc.getURL()}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]`;
14485
+ [state: url=${currentUrl}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]${warnings}`;
14486
+ }
14487
+ if (name === "click" && !wc.isLoading()) {
14488
+ try {
14489
+ const emptyPage = await executePageScript(
14490
+ wc,
14491
+ `(function() {
14492
+ var body = (document.body.textContent || '').toLowerCase();
14493
+ 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)
14494
+ && body.length < 8000;
14495
+ })()`,
14496
+ { timeoutMs: 1e3, label: "empty page check" }
14497
+ );
14498
+ if (emptyPage && emptyPage !== PAGE_SCRIPT_TIMEOUT) {
14499
+ return `
14500
+ [state: url=${wc.getURL()}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=false]
14501
+ 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.`;
14502
+ }
14503
+ } catch {
14504
+ }
13016
14505
  }
13017
14506
  if (interactActions.includes(name)) {
13018
14507
  return `
@@ -13253,14 +14742,48 @@ async function executeAction(name, args, ctx) {
13253
14742
  }
13254
14743
  case "click": {
13255
14744
  if (!wc) return "Error: No active tab";
13256
- const selector = typeof args.selector === "string" && args.selector.trim() ? await resolveSelector$1(wc, void 0, args.selector) : typeof args.index === "number" ? `__vessel_idx:${args.index}` : await resolveSelector$1(wc, args.index, args.selector);
13257
- if (!selector) return "Error: No element index or selector provided";
14745
+ let selector = null;
14746
+ const textTarget = typeof args.text === "string" && args.text.trim() ? args.text.trim() : "";
14747
+ if (typeof args.selector === "string" && args.selector.trim()) {
14748
+ selector = await resolveSelector$1(wc, void 0, args.selector);
14749
+ } else if (textTarget) {
14750
+ if (isInvalidTextTargetQuery(textTarget)) {
14751
+ return `Error: "${textTarget}" looks like HTML or markup, not a visible page label. Use a book title, button text, or element index instead.`;
14752
+ }
14753
+ selector = await resolveTargetByText(wc, textTarget, "interactive");
14754
+ if (!selector && typeof args.index === "number") {
14755
+ selector = `__vessel_idx:${args.index}`;
14756
+ }
14757
+ } else if (typeof args.index === "number") {
14758
+ selector = await resolveSelector$1(wc, args.index);
14759
+ if (!selector) selector = `__vessel_idx:${args.index}`;
14760
+ } else {
14761
+ selector = await resolveSelector$1(wc, args.index, args.selector);
14762
+ }
14763
+ if (selector === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
14764
+ if (!selector) {
14765
+ return "Error: No element index, selector, or visible text provided";
14766
+ }
13258
14767
  return clickResolvedSelector$1(wc, selector);
13259
14768
  }
13260
14769
  case "inspect_element": {
13261
14770
  if (!wc) return "Error: No active tab";
13262
- const selector = await resolveSelector$1(wc, args.index, args.selector);
13263
- if (!selector) return "Error: No element index or selector provided";
14771
+ let selector = null;
14772
+ const textTarget = typeof args.text === "string" && args.text.trim() ? args.text.trim() : "";
14773
+ if (textTarget) {
14774
+ if (isInvalidTextTargetQuery(textTarget)) {
14775
+ return `Error: "${textTarget}" looks like HTML or markup, not visible page text. Use a section title, book title, or element index instead.`;
14776
+ }
14777
+ selector = await resolveTargetByText(wc, textTarget, "context");
14778
+ } else {
14779
+ selector = await resolveSelector$1(wc, args.index, args.selector);
14780
+ }
14781
+ if (selector === PAGE_SCRIPT_TIMEOUT) {
14782
+ return pageBusyError("inspect_element");
14783
+ }
14784
+ if (!selector) {
14785
+ return "Error: No element index, selector, or visible text provided";
14786
+ }
13264
14787
  return inspectElement(
13265
14788
  wc,
13266
14789
  selector,
@@ -13409,7 +14932,8 @@ async function executeAction(name, args, ctx) {
13409
14932
  const livePrefix = liveSelectionSection ? `${liveSelectionSection}
13410
14933
 
13411
14934
  ` : "";
13412
- const requestedMode = normalizeReadPageMode(args.mode, content);
14935
+ const baseMode = normalizeReadPageMode(args.mode, content);
14936
+ const requestedMode = ctx.toolProfile === "compact" && (args.mode == null || typeof args.mode === "string" && !args.mode.trim()) ? chooseCompactReadMode(content, baseMode) : baseMode;
13413
14937
  if (requestedMode === "debug" || requestedMode === "full") {
13414
14938
  const structured = buildStructuredContext(content);
13415
14939
  const truncated = content.content.length > 2e4 ? content.content.slice(0, 2e4) + "\n[Content truncated...]" : content.content;
@@ -13421,7 +14945,7 @@ ${structured}
13421
14945
 
13422
14946
  ${truncated}`;
13423
14947
  }
13424
- const scoped = buildScopedContext(content, requestedMode);
14948
+ const scoped = ctx.toolProfile === "compact" ? buildCompactScopedContext(content, requestedMode) : buildScopedContext(content, requestedMode);
13425
14949
  return [
13426
14950
  livePrefix ? livePrefix.trimEnd() : "",
13427
14951
  `[read_page mode=${requestedMode}]`,
@@ -14098,9 +15622,20 @@ ${JSON.stringify(tableJson, null, 2)}`;
14098
15622
  }
14099
15623
  case "scroll_to_element": {
14100
15624
  if (!wc) return "Error: No active tab";
14101
- const sel = await resolveSelector$1(wc, args.index, args.selector);
14102
- if (!sel)
14103
- return "Error: Provide an index or selector for the element to scroll to.";
15625
+ let sel = null;
15626
+ const textTarget = typeof args.text === "string" && args.text.trim() ? args.text.trim() : "";
15627
+ if (textTarget) {
15628
+ if (isInvalidTextTargetQuery(textTarget)) {
15629
+ return `Error: "${textTarget}" looks like HTML or markup, not visible page text. Use a section title or element index instead.`;
15630
+ }
15631
+ sel = await resolveTargetByText(wc, textTarget, "context");
15632
+ } else {
15633
+ sel = await resolveSelector$1(wc, args.index, args.selector);
15634
+ }
15635
+ if (sel === PAGE_SCRIPT_TIMEOUT) return pageBusyError("scroll_to_element");
15636
+ if (!sel) {
15637
+ return "Error: Provide an index, selector, or visible text for the element to scroll to.";
15638
+ }
14104
15639
  const block = args.position === "top" ? "start" : args.position === "bottom" ? "end" : "center";
14105
15640
  if (sel.startsWith("__vessel_idx:")) {
14106
15641
  const idx = Number(sel.slice("__vessel_idx:".length));
@@ -14138,8 +15673,36 @@ ${JSON.stringify(tableJson, null, 2)}`;
14138
15673
  }
14139
15674
  }
14140
15675
  });
15676
+ const formattedResult = ctx.toolProfile === "compact" ? formatCompactToolResult(name, result) : result;
14141
15677
  const flowCtx = ctx.runtime.getFlowContext();
14142
- return result + await getPostActionState$1(ctx, name) + flowCtx;
15678
+ let clickNavSummary = "";
15679
+ if (name === "click" && !result.startsWith("Error") && !result.startsWith("Blocked") && result.includes(" -> ")) {
15680
+ const summaryWc = ctx.tabManager.getActiveTab()?.view.webContents;
15681
+ if (summaryWc) {
15682
+ clickNavSummary = await getPostClickNavSummary(
15683
+ summaryWc,
15684
+ ctx.toolProfile
15685
+ );
15686
+ }
15687
+ }
15688
+ let streakWarning = "";
15689
+ if (name === "click" && !result.startsWith("Error") && !result.startsWith("Blocked")) {
15690
+ const currentUrl = ctx.tabManager.getActiveTab()?.view.webContents.getURL() ?? "";
15691
+ if (currentUrl === clickStreakUrl) {
15692
+ clickStreakCount++;
15693
+ } else {
15694
+ clickStreakUrl = currentUrl;
15695
+ clickStreakCount = 1;
15696
+ }
15697
+ if (clickStreakCount >= CLICK_STREAK_THRESHOLD) {
15698
+ streakWarning = `
15699
+ 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.`;
15700
+ }
15701
+ } else if (["read_page", "inspect_element", "screenshot", "wait_for"].includes(name)) {
15702
+ clickStreakCount = 0;
15703
+ clickStreakUrl = null;
15704
+ }
15705
+ return formattedResult + await getPostActionState$1(ctx, name) + clickNavSummary + streakWarning + flowCtx;
14143
15706
  }
14144
15707
  async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history) {
14145
15708
  const lowerQuery = query.toLowerCase().trim();
@@ -14154,10 +15717,11 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
14154
15717
  } else {
14155
15718
  runtime2.clearTaskTracker();
14156
15719
  }
14157
- const structuredContext = buildScopedContext(
15720
+ const structuredContext = provider.agentToolProfile === "compact" ? buildCompactScopedContext(
14158
15721
  pageContent,
14159
- defaultReadMode
14160
- );
15722
+ defaultReadMode,
15723
+ pageType
15724
+ ) : buildScopedContext(pageContent, defaultReadMode);
14161
15725
  const runtimeState = runtime2.getState();
14162
15726
  const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
14163
15727
  const taskTrackerContext = runtime2.getTaskTrackerContext();
@@ -14181,7 +15745,11 @@ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.ti
14181
15745
  recentCheckpoints: recentCheckpoints || "- none",
14182
15746
  taskTrackerContext: taskTrackerContext || "- none"
14183
15747
  });
14184
- const actionCtx = { tabManager, runtime: runtime2 };
15748
+ const actionCtx = {
15749
+ tabManager,
15750
+ runtime: runtime2,
15751
+ toolProfile: provider.agentToolProfile
15752
+ };
14185
15753
  const contextualTools = pruneToolsForContext(
14186
15754
  AGENT_TOOLS,
14187
15755
  pageType,
@@ -15481,8 +17049,24 @@ async function clickResolvedSelector(wc, selector) {
15481
17049
  await waitForPotentialNavigation(wc, beforeUrl2);
15482
17050
  const afterUrl2 = wc.getURL();
15483
17051
  if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
15484
- const overlayHint2 = await detectPostClickOverlay(wc);
15485
- if (!overlayHint2) return result;
17052
+ let overlayHint2 = await detectPostClickOverlay(wc);
17053
+ if (!overlayHint2 && typeof idxLabel === "string" && isAddToCartText(idxLabel)) {
17054
+ await sleep(1200);
17055
+ overlayHint2 = await detectPostClickOverlay(wc);
17056
+ }
17057
+ if (!overlayHint2) {
17058
+ const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
17059
+ if (hrefMatch) {
17060
+ try {
17061
+ await wc.loadURL(hrefMatch[1]);
17062
+ await waitForLoad(wc, 8e3);
17063
+ const hrefUrl = wc.getURL();
17064
+ if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
17065
+ } catch {
17066
+ }
17067
+ }
17068
+ return result;
17069
+ }
15486
17070
  const dialogActions = typeof idxLabel === "string" && isAddToCartText(idxLabel) ? await getCartDialogActions(wc) : null;
15487
17071
  const actionsSuffix = dialogActions ? `
15488
17072
  ${dialogActions}
@@ -15504,9 +17088,11 @@ ${overlayHint2}${actionsSuffix}`;
15504
17088
  const result = await wc.executeJavaScript(`
15505
17089
  (function() {
15506
17090
  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.";
17091
+ if (!el || !document.contains(el)) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
15508
17092
  if (el instanceof HTMLElement) { el.focus(); el.click(); }
15509
- return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase());
17093
+ var anchor = el instanceof HTMLAnchorElement ? el : el.closest('a[href]');
17094
+ var href = anchor instanceof HTMLAnchorElement ? anchor.href : null;
17095
+ return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase()) + (href ? "\\nhref: " + href : "");
15510
17096
  })()
15511
17097
  `);
15512
17098
  if (typeof result === "string" && result.startsWith("Error")) return result;
@@ -15516,14 +17102,30 @@ ${overlayHint2}${actionsSuffix}`;
15516
17102
  await waitForPotentialNavigation(wc, beforeUrl2);
15517
17103
  const afterUrl2 = wc.getURL();
15518
17104
  if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
15519
- const overlayHint2 = await detectPostClickOverlay(wc);
15520
- if (!overlayHint2) return result;
15521
- const dialogActions = typeof shadowLabel === "string" && isAddToCartText(shadowLabel) ? await getCartDialogActions(wc) : null;
15522
- const actionsSuffix = dialogActions ? `
15523
- ${dialogActions}
17105
+ let overlayHint2 = await detectPostClickOverlay(wc);
17106
+ if (!overlayHint2 && typeof shadowLabel === "string" && isAddToCartText(shadowLabel)) {
17107
+ await sleep(1200);
17108
+ overlayHint2 = await detectPostClickOverlay(wc);
17109
+ }
17110
+ if (!overlayHint2) {
17111
+ const hrefMatch = typeof result === "string" ? result.match(/\nhref: (https?:\/\/\S+)/) : null;
17112
+ if (hrefMatch) {
17113
+ try {
17114
+ await wc.loadURL(hrefMatch[1]);
17115
+ await waitForLoad(wc, 8e3);
17116
+ const hrefUrl = wc.getURL();
17117
+ if (hrefUrl !== beforeUrl2) return `${result.split("\n")[0]} -> ${hrefUrl}`;
17118
+ } catch {
17119
+ }
17120
+ }
17121
+ return result;
17122
+ }
17123
+ const dialogActions2 = typeof shadowLabel === "string" && isAddToCartText(shadowLabel) ? await getCartDialogActions(wc) : null;
17124
+ const actionsSuffix2 = dialogActions2 ? `
17125
+ ${dialogActions2}
15524
17126
  Click one of these dialog actions. Do NOT click any other element.` : "";
15525
17127
  return `${result}
15526
- ${overlayHint2}${actionsSuffix}`;
17128
+ ${overlayHint2}${actionsSuffix2}`;
15527
17129
  }
15528
17130
  const beforeUrl = wc.getURL();
15529
17131
  const elInfo = await describeElementForClick(wc, selector);
@@ -15567,6 +17169,16 @@ Click one of these dialog actions. Do NOT click any other element.` : "";
15567
17169
  ${overlayHint}${actionsSuffix}`;
15568
17170
  }
15569
17171
  if (cartMatch) {
17172
+ await sleep(1200);
17173
+ const delayedOverlayHint = await detectPostClickOverlay(wc);
17174
+ if (delayedOverlayHint) {
17175
+ const dialogActions = await getCartDialogActions(wc);
17176
+ const actionsSuffix = dialogActions ? `
17177
+ ${dialogActions}
17178
+ Click one of these dialog actions. Do NOT click any other element.` : "";
17179
+ return `${clickText} (${clickResult})
17180
+ ${delayedOverlayHint}${actionsSuffix}`;
17181
+ }
15570
17182
  return `${clickText} (${clickResult})`;
15571
17183
  }
15572
17184
  const activationResult = await activateElement(wc, selector);
@@ -20492,8 +22104,9 @@ function registerIpcHandlers(windowState, runtime2) {
20492
22104
  if (!config || typeof config !== "object" || !("id" in config)) {
20493
22105
  return { ok: false, models: [], error: "Invalid provider configuration" };
20494
22106
  }
20495
- const models = await fetchProviderModels(config);
20496
- return { ok: true, models };
22107
+ return await fetchProviderModels(
22108
+ config
22109
+ );
20497
22110
  } catch (err) {
20498
22111
  return { ok: false, models: [], error: err instanceof Error ? err.message : "Unknown error" };
20499
22112
  }
@@ -20983,6 +22596,82 @@ function setNextHint(state2, nextHint) {
20983
22596
  function normalizeResult(result) {
20984
22597
  return result.toLowerCase();
20985
22598
  }
22599
+ function looksLikeListingResult(result) {
22600
+ const lowered = normalizeResult(result);
22601
+ return lowered.includes("### primary results") || lowered.includes("### likely search results") || lowered.includes("[read_page mode=results_only]");
22602
+ }
22603
+ function countSurfacedResults(result) {
22604
+ const matches = result.match(/^\s*-\s+\[#\d+\]/gm);
22605
+ return matches?.length ?? 0;
22606
+ }
22607
+ function looksLikeSearchResultsPage(result) {
22608
+ const lowered = normalizeResult(result);
22609
+ return lowered.includes("/searchresults") || lowered.includes("/books/search") || lowered.includes("search results") || lowered.includes("bestsellers") || lowered.includes("best sellers");
22610
+ }
22611
+ function looksLikeProductDetailResult(result) {
22612
+ const lowered = normalizeResult(result);
22613
+ 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);
22614
+ }
22615
+ function looksLikeCartConfirmation(result) {
22616
+ return /(added to cart|cart confirmation|view cart|continue shopping|shopping cart|checkout)/.test(
22617
+ normalizeResult(result)
22618
+ );
22619
+ }
22620
+ function looksLikeCartPage(result) {
22621
+ const lowered = normalizeResult(result);
22622
+ return /\*\*url:\*\*\s*https?:\/\/[^\s]+\/cart\b/.test(lowered) || /\b(?:navigated to|went back to|went forward to)\s+https?:\/\/[^\s]+\/cart\b/.test(
22623
+ lowered
22624
+ ) || /\*\*title:\*\*.*\b(cart|checkout)\b/.test(lowered) || /\b(shopping cart|cart subtotal|cart total)\b/.test(lowered);
22625
+ }
22626
+ function looksLikeStaleElementError(result) {
22627
+ const lowered = normalizeResult(result);
22628
+ 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");
22629
+ }
22630
+ function extractStructuredUrl(result) {
22631
+ return result.match(/\*\*url:\*\*\s*([^\n]+)/i)?.[1]?.trim() ?? extractNavigatedUrl(result);
22632
+ }
22633
+ function isAddToCartSuccess(actionName, result) {
22634
+ const lowered = normalizeResult(result);
22635
+ if (actionName !== "click") return false;
22636
+ if (lowered.startsWith("blocked:")) return false;
22637
+ const clickedAddToCart = /clicked:.*add(?: item)? to (?:cart|bag|basket)/.test(lowered);
22638
+ return clickedAddToCart && looksLikeCartConfirmation(result);
22639
+ }
22640
+ function extractNavigatedUrl(result) {
22641
+ return result.match(
22642
+ /\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i
22643
+ )?.[1]?.trim() ?? null;
22644
+ }
22645
+ function stepIndexMatching(steps, pattern) {
22646
+ return steps.findIndex((step) => pattern.test(step.label.toLowerCase()));
22647
+ }
22648
+ function finalizeShoppingTracker(state2, detail) {
22649
+ const steps = state2.steps.map((step) => ({ ...step }));
22650
+ const pickIndex = stepIndexMatching(steps, /^pick the requested/);
22651
+ const cartIndex = stepIndexMatching(steps, /^add the chosen .* to the cart$/);
22652
+ const explainIndex = stepIndexMatching(steps, /^explain the recommendations$/);
22653
+ if (pickIndex >= 0) {
22654
+ steps[pickIndex] = {
22655
+ ...steps[pickIndex],
22656
+ status: "done",
22657
+ detail
22658
+ };
22659
+ }
22660
+ if (cartIndex >= 0) {
22661
+ steps[cartIndex] = {
22662
+ ...steps[cartIndex],
22663
+ status: "done",
22664
+ detail
22665
+ };
22666
+ }
22667
+ const activeIndex = explainIndex >= 0 ? explainIndex : state2.currentStepIndex;
22668
+ return {
22669
+ ...state2,
22670
+ steps: setActiveStep(steps, activeIndex),
22671
+ currentStepIndex: activeIndex,
22672
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22673
+ };
22674
+ }
20986
22675
  function createTaskTracker(goal, startUrl) {
20987
22676
  const now = (/* @__PURE__ */ new Date()).toISOString();
20988
22677
  return {
@@ -20993,23 +22682,59 @@ function createTaskTracker(goal, startUrl) {
20993
22682
  currentStepIndex: 0,
20994
22683
  steps: buildInitialSteps(goal),
20995
22684
  lastAction: void 0,
20996
- nextHint: "Open a relevant section or search path and keep working the same request."
22685
+ nextHint: "Use the site's search box or a strong curated section immediately. Avoid rereading the homepage unless search or navigation is hidden."
20997
22686
  };
20998
22687
  }
20999
22688
  function updateTaskTracker(state2, actionName, result) {
21000
- const loweredResult = normalizeResult(result);
22689
+ const requestedCount = state2.requestedCount ?? extractRequestedCount(state2.goal) ?? null;
22690
+ let cartCount = state2.cartCount ?? 0;
22691
+ const cartVisible = state2.cartVisible || looksLikeCartPage(result);
21001
22692
  let nextState = {
21002
22693
  ...state2,
21003
22694
  lastAction: actionName,
22695
+ requestedCount,
22696
+ cartCount,
22697
+ cartVisible,
21004
22698
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21005
22699
  };
21006
22700
  const currentLabel = nextState.steps[nextState.currentStepIndex]?.label.toLowerCase() ?? "";
21007
22701
  if (actionName === "navigate") {
21008
- nextState = completeStep(nextState, "Reached the requested site.");
21009
- return setNextHint(
21010
- nextState,
21011
- "Open a curated section or use the site's book search to find promising titles."
21012
- );
22702
+ if (/navigate to the requested site/.test(currentLabel)) {
22703
+ nextState = completeStep(nextState, "Reached the requested site.");
22704
+ return setNextHint(
22705
+ nextState,
22706
+ "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."
22707
+ );
22708
+ }
22709
+ const navigatedUrl = extractNavigatedUrl(result) || "";
22710
+ if (/pick the requested/.test(currentLabel)) {
22711
+ if (/\/book\//i.test(navigatedUrl)) {
22712
+ return setNextHint(
22713
+ nextState,
22714
+ "You opened a chosen product detail page. Do not restart search. Click Add to Cart here, then wait for cart confirmation before moving on."
22715
+ );
22716
+ }
22717
+ if (looksLikeSearchResultsPage(result)) {
22718
+ return setNextHint(
22719
+ nextState,
22720
+ "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."
22721
+ );
22722
+ }
22723
+ }
22724
+ if (/add the chosen .* to the cart/.test(currentLabel)) {
22725
+ if (/\/book\//i.test(navigatedUrl)) {
22726
+ return setNextHint(
22727
+ nextState,
22728
+ "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."
22729
+ );
22730
+ }
22731
+ if (looksLikeSearchResultsPage(result)) {
22732
+ return setNextHint(
22733
+ nextState,
22734
+ "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."
22735
+ );
22736
+ }
22737
+ }
21013
22738
  }
21014
22739
  const isDiscoveryAction = [
21015
22740
  "read_page",
@@ -21019,31 +22744,101 @@ function updateTaskTracker(state2, actionName, result) {
21019
22744
  "scroll"
21020
22745
  ].includes(actionName);
21021
22746
  if (isDiscoveryAction && /browse or search/.test(currentLabel)) {
22747
+ const surfacedResults = countSurfacedResults(result);
21022
22748
  nextState = completeStep(nextState, "Found a starting point on the site.");
21023
22749
  return setNextHint(
21024
22750
  nextState,
21025
- "Inspect individual books and keep track of the best candidates until you have the full set."
22751
+ 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
22752
  );
21027
22753
  }
21028
22754
  if (/pick the requested/.test(currentLabel) && isDiscoveryAction) {
22755
+ if (isAddToCartSuccess(actionName, result)) {
22756
+ cartCount += 1;
22757
+ nextState = {
22758
+ ...nextState,
22759
+ cartCount
22760
+ };
22761
+ if (requestedCount && cartCount >= requestedCount) {
22762
+ nextState = finalizeShoppingTracker(
22763
+ nextState,
22764
+ `Added ${cartCount} of ${requestedCount} requested items to the cart.`
22765
+ );
22766
+ return setNextHint(
22767
+ nextState,
22768
+ 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."
22769
+ );
22770
+ }
22771
+ return setNextHint(
22772
+ nextState,
22773
+ 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."
22774
+ );
22775
+ }
22776
+ if (looksLikeCartConfirmation(result)) {
22777
+ return setNextHint(
22778
+ nextState,
22779
+ "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."
22780
+ );
22781
+ }
22782
+ const structuredUrl = extractStructuredUrl(result) || "";
22783
+ if (actionName === "read_page" && cartCount > 0 && /\/book\//i.test(structuredUrl) && looksLikeProductDetailResult(result)) {
22784
+ return setNextHint(
22785
+ nextState,
22786
+ "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."
22787
+ );
22788
+ }
22789
+ if (looksLikeProductDetailResult(result)) {
22790
+ return setNextHint(
22791
+ nextState,
22792
+ '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.'
22793
+ );
22794
+ }
22795
+ if (looksLikeSearchResultsPage(result) && !looksLikeListingResult(result)) {
22796
+ return setNextHint(
22797
+ nextState,
22798
+ '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.'
22799
+ );
22800
+ }
22801
+ if (looksLikeListingResult(result)) {
22802
+ const surfacedResults = countSurfacedResults(result);
22803
+ return setNextHint(
22804
+ nextState,
22805
+ 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."
22806
+ );
22807
+ }
22808
+ if (actionName === "click" && looksLikeStaleElementError(result)) {
22809
+ return setNextHint(
22810
+ nextState,
22811
+ "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."
22812
+ );
22813
+ }
21029
22814
  return setNextHint(
21030
22815
  nextState,
21031
- "Keep selecting candidate books. After you have the requested set, add each one to the cart."
22816
+ "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
22817
  );
21033
22818
  }
21034
- if (/add the chosen .* to the cart/.test(currentLabel) && /(added to cart|cart confirmation|shopping cart|view cart|checkout)/.test(
21035
- loweredResult
21036
- )) {
21037
- nextState = completeStep(nextState, "Cart interaction succeeded.");
22819
+ if (/pick the requested/.test(currentLabel) && actionName === "go_back") {
22820
+ return setNextHint(
22821
+ nextState,
22822
+ "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."
22823
+ );
22824
+ }
22825
+ if (/add the chosen .* to the cart/.test(currentLabel) && isAddToCartSuccess(actionName, result)) {
22826
+ cartCount += 1;
22827
+ nextState = {
22828
+ ...nextState,
22829
+ cartCount
22830
+ };
22831
+ const detail = requestedCount ? `Added ${cartCount} of ${requestedCount} requested items to the cart.` : "Cart interaction succeeded.";
22832
+ nextState = completeStep(nextState, detail);
21038
22833
  return setNextHint(
21039
22834
  nextState,
21040
- "Summarize the chosen books and explain why they were recommended."
22835
+ 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
22836
  );
21042
22837
  }
21043
22838
  if (/explain the recommendations/.test(currentLabel)) {
21044
22839
  return setNextHint(
21045
22840
  nextState,
21046
- "Finish by naming the chosen books and giving concise reasons for each."
22841
+ 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
22842
  );
21048
22843
  }
21049
22844
  return nextState;