@quanta-intellect/vessel-browser 0.1.33 → 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,214 +5126,6 @@ function makeImageResult(base64, description, mediaType = "image/png") {
5126
5126
  };
5127
5127
  return JSON.stringify(result);
5128
5128
  }
5129
- class AnthropicProvider {
5130
- client;
5131
- model;
5132
- abortController = null;
5133
- constructor(apiKey, model) {
5134
- this.client = new Anthropic({ apiKey });
5135
- this.model = model || "claude-sonnet-4-20250514";
5136
- }
5137
- async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
5138
- this.abortController = new AbortController();
5139
- const messages = [
5140
- ...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
5141
- { role: "user", content: userMessage }
5142
- ];
5143
- try {
5144
- const stream = this.client.messages.stream(
5145
- {
5146
- model: this.model,
5147
- max_tokens: 4096,
5148
- system: systemPrompt,
5149
- messages
5150
- },
5151
- { signal: this.abortController.signal }
5152
- );
5153
- for await (const event of stream) {
5154
- if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
5155
- onChunk(event.delta.text);
5156
- }
5157
- }
5158
- } catch (err) {
5159
- if (err instanceof Error && err.name !== "AbortError") {
5160
- onChunk(`
5161
-
5162
- [Error: ${err.message}]`);
5163
- }
5164
- } finally {
5165
- this.abortController = null;
5166
- onEnd();
5167
- }
5168
- }
5169
- async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
5170
- this.abortController = new AbortController();
5171
- const messages = [
5172
- ...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
5173
- { role: "user", content: userMessage }
5174
- ];
5175
- try {
5176
- const maxIterations = getEffectiveMaxIterations();
5177
- let iterationsUsed = 0;
5178
- for (let i = 0; i < maxIterations; i++) {
5179
- iterationsUsed = i + 1;
5180
- const stream = this.client.messages.stream(
5181
- {
5182
- model: this.model,
5183
- max_tokens: 4096,
5184
- system: systemPrompt,
5185
- messages,
5186
- tools
5187
- },
5188
- { signal: this.abortController.signal }
5189
- );
5190
- let textContent = "";
5191
- const toolUseBlocks = [];
5192
- let currentToolUse = null;
5193
- const STREAM_IDLE_TIMEOUT_MS = 3e4;
5194
- let idleTimer = null;
5195
- const resetIdleTimer = () => {
5196
- if (idleTimer) clearTimeout(idleTimer);
5197
- idleTimer = setTimeout(() => {
5198
- this.abortController?.abort();
5199
- }, STREAM_IDLE_TIMEOUT_MS);
5200
- };
5201
- resetIdleTimer();
5202
- try {
5203
- for await (const event of stream) {
5204
- resetIdleTimer();
5205
- if (event.type === "content_block_start") {
5206
- if (event.content_block.type === "tool_use") {
5207
- currentToolUse = {
5208
- id: event.content_block.id,
5209
- name: event.content_block.name,
5210
- inputJson: ""
5211
- };
5212
- }
5213
- } else if (event.type === "content_block_delta") {
5214
- if (event.delta.type === "text_delta") {
5215
- textContent += event.delta.text;
5216
- onChunk(event.delta.text);
5217
- } else if (event.delta.type === "input_json_delta" && currentToolUse) {
5218
- currentToolUse.inputJson += event.delta.partial_json;
5219
- }
5220
- } else if (event.type === "content_block_stop" && currentToolUse) {
5221
- try {
5222
- toolUseBlocks.push({
5223
- id: currentToolUse.id,
5224
- name: currentToolUse.name,
5225
- input: JSON.parse(currentToolUse.inputJson || "{}")
5226
- });
5227
- } catch {
5228
- toolUseBlocks.push({
5229
- id: currentToolUse.id,
5230
- name: currentToolUse.name,
5231
- input: {},
5232
- _malformedArgs: currentToolUse.inputJson
5233
- });
5234
- }
5235
- currentToolUse = null;
5236
- }
5237
- }
5238
- } finally {
5239
- if (idleTimer) clearTimeout(idleTimer);
5240
- }
5241
- const finalMessage = await stream.finalMessage();
5242
- const assistantContent = [];
5243
- if (textContent) {
5244
- assistantContent.push({ type: "text", text: textContent });
5245
- }
5246
- for (const tb of toolUseBlocks) {
5247
- assistantContent.push({
5248
- type: "tool_use",
5249
- id: tb.id,
5250
- name: tb.name,
5251
- input: tb.input
5252
- });
5253
- }
5254
- messages.push({ role: "assistant", content: assistantContent });
5255
- if (toolUseBlocks.length === 0) {
5256
- break;
5257
- }
5258
- const toolResults = [];
5259
- for (const tb of toolUseBlocks) {
5260
- if (tb._malformedArgs !== void 0) {
5261
- onChunk(`
5262
- <<tool:${tb.name}:⚠ invalid args>>
5263
- `);
5264
- toolResults.push({
5265
- type: "tool_result",
5266
- tool_use_id: tb.id,
5267
- content: `Error: Invalid JSON in tool arguments — could not parse. Please retry with valid JSON. Raw input: ${tb._malformedArgs.slice(0, 200)}`,
5268
- is_error: true
5269
- });
5270
- continue;
5271
- }
5272
- const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
5273
- onChunk(`
5274
- <<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
5275
- `);
5276
- let result;
5277
- try {
5278
- result = await onToolCall(tb.name, tb.input);
5279
- } catch (toolErr) {
5280
- const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
5281
- result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
5282
- }
5283
- let parsedRich = null;
5284
- try {
5285
- const parsed = JSON.parse(result);
5286
- if (isRichToolResult(parsed)) parsedRich = parsed;
5287
- } catch {
5288
- }
5289
- if (parsedRich) {
5290
- toolResults.push({
5291
- type: "tool_result",
5292
- tool_use_id: tb.id,
5293
- content: parsedRich.content.map((block) => {
5294
- if (block.type === "image") {
5295
- return {
5296
- type: "image",
5297
- source: {
5298
- type: "base64",
5299
- media_type: block.mediaType,
5300
- data: block.base64
5301
- }
5302
- };
5303
- }
5304
- return { type: "text", text: block.text };
5305
- })
5306
- });
5307
- } else {
5308
- toolResults.push({
5309
- type: "tool_result",
5310
- tool_use_id: tb.id,
5311
- content: result
5312
- });
5313
- }
5314
- }
5315
- messages.push({ role: "user", content: toolResults });
5316
- }
5317
- if (iterationsUsed >= maxIterations) {
5318
- onChunk(`
5319
-
5320
- [Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
5321
- }
5322
- } catch (err) {
5323
- if (err instanceof Error && err.name !== "AbortError") {
5324
- onChunk(`
5325
-
5326
- [Error: ${err.message}]`);
5327
- }
5328
- } finally {
5329
- this.abortController = null;
5330
- onEnd();
5331
- }
5332
- }
5333
- cancel() {
5334
- this.abortController?.abort();
5335
- }
5336
- }
5337
5129
  const PROVIDERS = {
5338
5130
  anthropic: {
5339
5131
  id: "anthropic",
@@ -5383,6 +5175,16 @@ const PROVIDERS = {
5383
5175
  apiKeyPlaceholder: "",
5384
5176
  apiKeyHint: "No API key needed — runs locally via Ollama"
5385
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
+ },
5386
5188
  mistral: {
5387
5189
  id: "mistral",
5388
5190
  name: "Mistral AI",
@@ -5423,20 +5225,609 @@ const PROVIDERS = {
5423
5225
  apiKeyPlaceholder: "Bearer token or API key",
5424
5226
  apiKeyHint: "Optional — only if your endpoint requires authentication"
5425
5227
  }
5426
- };
5427
- function toOpenAITools(tools) {
5428
- return tools.map((t) => ({
5429
- type: "function",
5430
- function: {
5431
- name: t.name,
5432
- description: t.description ?? "",
5433
- parameters: t.input_schema
5228
+ };
5229
+ const SAFE_TOOL_ALIASES = {
5230
+ goto_url: "navigate",
5231
+ go_to_url: "navigate",
5232
+ browser_goto: "navigate",
5233
+ browser_navigate: "navigate",
5234
+ open_url: "navigate",
5235
+ visit_url: "navigate",
5236
+ navigate_to: "navigate",
5237
+ open_page: "navigate",
5238
+ google_search: "search",
5239
+ site_search: "search",
5240
+ search_site: "search",
5241
+ page_search: "search",
5242
+ scroll_down: "scroll",
5243
+ scroll_up: "scroll",
5244
+ read: "read_page",
5245
+ read_current_page: "read_page",
5246
+ scan_page: "read_page"
5247
+ };
5248
+ function normalizeToolAlias(name) {
5249
+ const normalized = name.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
5250
+ return SAFE_TOOL_ALIASES[normalized] ?? name;
5251
+ }
5252
+ function parseModelSizeInBillions(model) {
5253
+ const match = model.toLowerCase().match(/(?:^|[:/_\-\s])(\d+(?:\.\d+)?)b(?:$|[:/_\-\s])/i);
5254
+ if (!match) return null;
5255
+ const parsed = Number(match[1]);
5256
+ return Number.isFinite(parsed) ? parsed : null;
5257
+ }
5258
+ function isLoopbackBaseUrl(baseUrl) {
5259
+ if (!baseUrl) return false;
5260
+ try {
5261
+ const url = new URL(baseUrl);
5262
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
5263
+ } catch {
5264
+ return false;
5265
+ }
5266
+ }
5267
+ function resolveAgentToolProfile(config) {
5268
+ const providerId = config.id;
5269
+ const isLocalProvider = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
5270
+ if (!isLocalProvider) return "default";
5271
+ const sizeInBillions = parseModelSizeInBillions(config.model);
5272
+ if (sizeInBillions === null) {
5273
+ return "compact";
5274
+ }
5275
+ return sizeInBillions <= 14 ? "compact" : "default";
5276
+ }
5277
+ const LLAMA_CPP_MIN_CTX_TOKENS$1 = 16384;
5278
+ const LLAMA_CPP_RECOMMENDED_CTX_TOKENS$1 = 32768;
5279
+ function shouldDebugAgentLoop() {
5280
+ const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
5281
+ return value === "1" || value === "true";
5282
+ }
5283
+ function previewDebugValue(value, maxLength = 800) {
5284
+ const normalized = value.replace(/\s+/g, " ").trim();
5285
+ if (normalized.length <= maxLength) return normalized;
5286
+ return `${normalized.slice(0, maxLength)}…`;
5287
+ }
5288
+ function previewToolDebugContent(content) {
5289
+ return previewDebugValue(content, 500);
5290
+ }
5291
+ function toOpenAITools(tools) {
5292
+ return tools.map((t) => ({
5293
+ type: "function",
5294
+ function: {
5295
+ name: t.name,
5296
+ description: t.description ?? "",
5297
+ parameters: t.input_schema
5298
+ }
5299
+ }));
5300
+ }
5301
+ function agentTemperatureForProfile(profile) {
5302
+ return profile === "compact" ? 0.2 : void 0;
5303
+ }
5304
+ function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
5305
+ if (profile !== "compact") return null;
5306
+ const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
5307
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
5308
+ return {
5309
+ role: "user",
5310
+ content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
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 ? `
5312
+ ${stateReminder}` : "") + (phaseReminder ? `
5313
+ ${phaseReminder}` : "")
5314
+ };
5315
+ }
5316
+ function extractSingleGoalDomain(goal) {
5317
+ const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
5318
+ if (!matches || matches.length !== 1) return null;
5319
+ return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
5320
+ }
5321
+ function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
5322
+ const phaseReminder = buildPhaseReminder(userMessage, assistantText);
5323
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
5324
+ const goalDomain = extractSingleGoalDomain(userMessage);
5325
+ const latest = (latestToolResultPreview || "").toLowerCase();
5326
+ const assistant = assistantText.toLowerCase();
5327
+ const alreadyOnGoalSite = !!goalDomain && (latest.includes(goalDomain) || assistant.includes(`https://${goalDomain}`) || assistant.includes(`https://www.${goalDomain}`));
5328
+ const lines = [
5329
+ `The task is still in progress: ${userMessage}`,
5330
+ `Do not ask the user for permission to continue. Choose the next tool now unless the request is fully complete.`
5331
+ ];
5332
+ if (alreadyOnGoalSite) {
5333
+ lines.push(
5334
+ `You are already on the requested site (${goalDomain}). Do not navigate to the homepage again and do not restart discovery from scratch.`
5335
+ );
5336
+ }
5337
+ if (stateReminder) {
5338
+ lines.push(stateReminder);
5339
+ }
5340
+ if (phaseReminder) {
5341
+ lines.push(phaseReminder);
5342
+ }
5343
+ return lines.join("\n");
5344
+ }
5345
+ function buildPhaseReminder(userMessage, assistantText) {
5346
+ const goal = userMessage.toLowerCase();
5347
+ const text = assistantText.toLowerCase();
5348
+ if (!goal || !text) return "";
5349
+ const wantsCart = /\b(cart|bag|basket|checkout)\b/.test(goal);
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
+ );
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);
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);
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);
5357
+ const cartDone = /(added to cart|added them to the cart|cart confirmation|view cart|checkout)/.test(
5358
+ text
5359
+ );
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
+ }
5412
+ if (wantsCart && selectedItems && (intendsCart || !cartDone)) {
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.`;
5414
+ }
5415
+ if (wantsCart && wantsExplanation && cartDone && !explanationDone) {
5416
+ return `Progress reminder: The cart step appears complete. Do not resume browsing. Finish by explaining why the chosen items were recommended.`;
5417
+ }
5418
+ return "";
5419
+ }
5420
+ function buildLatestStateReminder(toolResultPreview) {
5421
+ const text = toolResultPreview.trim();
5422
+ if (!text) return "";
5423
+ const stateMatch = text.match(
5424
+ /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
5425
+ );
5426
+ if (stateMatch) {
5427
+ const url = stateMatch[1]?.trim();
5428
+ const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
5429
+ if (url) {
5430
+ return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
5431
+ }
5432
+ }
5433
+ const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
5434
+ const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
5435
+ if (structuredUrl) {
5436
+ return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
5437
+ }
5438
+ const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i)?.[1]?.trim();
5439
+ const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
5440
+ if (navigatedUrl) {
5441
+ return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
5442
+ }
5443
+ return "";
5444
+ }
5445
+ function shouldRecoverCompactStall(text, userMessage) {
5446
+ const trimmed = text.trim().toLowerCase();
5447
+ if (!trimmed) return true;
5448
+ if (trimmed.length <= 160 && trimmed.includes("?")) return true;
5449
+ if (userMessage && buildPhaseReminder(userMessage, text)) {
5450
+ return true;
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
+ }
5476
+ const completionSignals = [
5477
+ "i found",
5478
+ "i chose",
5479
+ "i selected",
5480
+ "i added",
5481
+ "here are",
5482
+ "these are",
5483
+ "recommendations",
5484
+ "reasoning",
5485
+ "why i chose",
5486
+ "added them to the cart"
5487
+ ];
5488
+ if (completionSignals.some((pattern) => trimmed.includes(pattern))) {
5489
+ return false;
5490
+ }
5491
+ return [
5492
+ "what are you hoping",
5493
+ "what would you like",
5494
+ "how can i help",
5495
+ "let me know",
5496
+ "are you looking for",
5497
+ "just browsing",
5498
+ "i need to",
5499
+ "i will",
5500
+ "i'll",
5501
+ "since i cannot see",
5502
+ "since i can't see",
5503
+ "cannot see the current page",
5504
+ "scroll down to",
5505
+ "load more results",
5506
+ "as placeholders",
5507
+ "would you like me to proceed",
5508
+ "action:",
5509
+ "one moment",
5510
+ "i will now navigate",
5511
+ "navigating to ",
5512
+ "this will take me",
5513
+ "i will use the browser"
5514
+ ].some((pattern) => trimmed.includes(pattern));
5515
+ }
5516
+ function shouldRetryCompactToolLoop(profile, text, hasToolHistory, userMessage) {
5517
+ return profile === "compact" && hasToolHistory && shouldRecoverCompactStall(text, userMessage);
5518
+ }
5519
+ function stableToolSignature(name, args) {
5520
+ const canonicalArgs = canonicalizeArgsForTool(name, args);
5521
+ const sortedEntries = Object.entries(canonicalArgs).sort(
5522
+ ([left], [right]) => left.localeCompare(right)
5523
+ );
5524
+ return JSON.stringify([name, sortedEntries]);
5525
+ }
5526
+ function hasRecentDuplicateToolCall(recentToolSignatures, signature) {
5527
+ return recentToolSignatures.includes(signature);
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
+ }
5540
+ function normalizeToolToken(value) {
5541
+ return value.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
5542
+ }
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 };
5612
+ try {
5613
+ const parsed = JSON.parse(trimmed);
5614
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
5615
+ return { args: parsed, repaired: false };
5616
+ }
5617
+ if (typeof parsed === "string") {
5618
+ const scalarArgs2 = scalarArgsForTool(name, parsed);
5619
+ return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
5620
+ }
5621
+ return null;
5622
+ } catch {
5623
+ }
5624
+ try {
5625
+ const repaired = tryParseJsonWithCommonRepairs(trimmed);
5626
+ if (repaired && typeof repaired === "object" && !Array.isArray(repaired)) {
5627
+ return { args: repaired, repaired: true };
5628
+ }
5629
+ if (typeof repaired === "string") {
5630
+ const scalarArgs2 = scalarArgsForTool(name, repaired);
5631
+ return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
5632
+ }
5633
+ } catch {
5634
+ }
5635
+ const scalarArgs = scalarArgsForTool(name, trimmed);
5636
+ return scalarArgs ? { args: scalarArgs, repaired: true } : null;
5637
+ }
5638
+ function coerceToolArgsForExecution(name, args) {
5639
+ const coerced = { ...args };
5640
+ if (name === "search") {
5641
+ if (typeof coerced.query !== "string" || !coerced.query.trim()) {
5642
+ if (typeof coerced.text === "string" && coerced.text.trim()) {
5643
+ coerced.query = coerced.text.trim();
5644
+ } else if (typeof coerced.term === "string" && coerced.term.trim()) {
5645
+ coerced.query = coerced.term.trim();
5646
+ }
5647
+ }
5648
+ }
5649
+ if (name === "navigate") {
5650
+ if (typeof coerced.url !== "string" || !coerced.url.trim()) {
5651
+ if (typeof coerced.href === "string" && coerced.href.trim()) {
5652
+ coerced.url = coerced.href.trim();
5653
+ } else if (typeof coerced.link === "string" && coerced.link.trim()) {
5654
+ coerced.url = coerced.link.trim();
5655
+ } else if (typeof coerced.text === "string" && /^https?:\/\//i.test(coerced.text.trim())) {
5656
+ coerced.url = coerced.text.trim();
5657
+ }
5658
+ }
5659
+ }
5660
+ return coerced;
5661
+ }
5662
+ function canonicalizeArgsForTool(name, args) {
5663
+ const canonical = coerceToolArgsForExecution(name, args);
5664
+ if (typeof canonical.url === "string") {
5665
+ canonical.url = canonicalizeUrlLike(canonical.url);
5666
+ }
5667
+ if (typeof canonical.query === "string") {
5668
+ canonical.query = canonical.query.trim().replace(/\s+/g, " ").toLowerCase();
5669
+ delete canonical.text;
5670
+ }
5671
+ if (typeof canonical.text === "string") {
5672
+ canonical.text = canonical.text.trim().replace(/\s+/g, " ");
5673
+ }
5674
+ return canonical;
5675
+ }
5676
+ function resolveToolCallName(rawName, args, availableToolNames) {
5677
+ const aliased = normalizeToolAlias(rawName);
5678
+ if (availableToolNames.has(aliased)) return aliased;
5679
+ const normalized = normalizeToolToken(rawName);
5680
+ if (availableToolNames.has(normalized)) return normalized;
5681
+ const hasUrl = typeof args.url === "string" && args.url.trim().length > 0;
5682
+ if (availableToolNames.has("navigate") && (hasUrl || /goto|navigate|open|visit|browser|url|link/.test(normalized))) {
5683
+ return "navigate";
5684
+ }
5685
+ if (availableToolNames.has("search") && (/search|find|lookup|query/.test(normalized) || normalized === "google" || normalized.startsWith("google_"))) {
5686
+ return "search";
5687
+ }
5688
+ if (availableToolNames.has("scroll") && /scroll|page_?down|page_?up/.test(normalized)) {
5689
+ return "scroll";
5690
+ }
5691
+ if (availableToolNames.has("read_page") && /read|scan|inspect|analy[sz]e|summari[sz]e/.test(normalized)) {
5692
+ return "read_page";
5693
+ }
5694
+ return aliased;
5695
+ }
5696
+ function logAgentLoopDebug(payload) {
5697
+ if (!shouldDebugAgentLoop()) return;
5698
+ try {
5699
+ console.log(`[Vessel agent-debug] ${JSON.stringify(payload)}`);
5700
+ } catch (err) {
5701
+ console.warn("[Vessel agent-debug] Failed to serialize debug payload:", err);
5702
+ }
5703
+ }
5704
+ function recoverTextEncodedToolCalls(text, availableToolNames) {
5705
+ const trimmed = text.trim();
5706
+ if (!trimmed) return [];
5707
+ const candidates = trimmed.match(
5708
+ /([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*?\})(?=\s*$|\n{2,}|[A-Za-z0-9._ -]+\s*\[ARGS\])/g
5709
+ );
5710
+ if (!candidates || candidates.length === 0) return [];
5711
+ const recovered = [];
5712
+ for (const candidate of candidates) {
5713
+ const match = candidate.match(
5714
+ /^\s*([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*\})\s*$/
5715
+ );
5716
+ if (!match) continue;
5717
+ const rawName = match[1] ?? "";
5718
+ const argsJson = match[2] ?? "{}";
5719
+ let parsedArgs = {};
5720
+ try {
5721
+ parsedArgs = JSON.parse(argsJson);
5722
+ } catch {
5723
+ continue;
5434
5724
  }
5435
- }));
5725
+ const resolvedName = resolveToolCallName(
5726
+ rawName,
5727
+ parsedArgs,
5728
+ availableToolNames
5729
+ );
5730
+ recovered.push({
5731
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
5732
+ name: resolvedName,
5733
+ argsJson
5734
+ });
5735
+ }
5736
+ return recovered;
5737
+ }
5738
+ function recoverNarratedActionToolCalls(text, availableToolNames) {
5739
+ const trimmed = text.trim();
5740
+ if (!trimmed) return [];
5741
+ const recovered = [];
5742
+ const actionLines = trimmed.match(/^action:\s+.+$/gim) ?? [];
5743
+ for (const rawLine of actionLines) {
5744
+ const line = rawLine.replace(/^action:\s*/i, "").trim();
5745
+ if (!line) continue;
5746
+ const quotedValue = line.match(/"([^"]+)"/)?.[1]?.trim() ?? line.match(/'([^']+)'/)?.[1]?.trim() ?? "";
5747
+ const navigateMatch = line.match(
5748
+ /\b(?:navigate|open|go)\b(?:\s+(?:to|the url))?\s+(https?:\/\/[^\s)]+)\.?/i
5749
+ );
5750
+ if (navigateMatch?.[1]) {
5751
+ const argsJson = JSON.stringify({ url: navigateMatch[1].replace(/\.$/, "") });
5752
+ recovered.push({
5753
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
5754
+ name: resolveToolCallName("navigate", { url: navigateMatch[1] }, availableToolNames),
5755
+ argsJson
5756
+ });
5757
+ continue;
5758
+ }
5759
+ const isSearchAction = /\bsearch\b/i.test(line) || /\btype\b/i.test(line) && /\bsearch box\b/i.test(line);
5760
+ if (isSearchAction && quotedValue && availableToolNames.has("search")) {
5761
+ recovered.push({
5762
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
5763
+ name: "search",
5764
+ argsJson: JSON.stringify({ query: quotedValue })
5765
+ });
5766
+ continue;
5767
+ }
5768
+ if (/\b(?:read|scan)\b.*\bpage\b/i.test(line) && availableToolNames.has("read_page")) {
5769
+ recovered.push({
5770
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
5771
+ name: "read_page",
5772
+ argsJson: JSON.stringify({ mode: "visible_only" })
5773
+ });
5774
+ continue;
5775
+ }
5776
+ const toolRefMatch = line.match(
5777
+ /\b(?:use|call)\s+([a-z_][a-z0-9_]*)(?:\s+tool)?\b/i
5778
+ );
5779
+ if (toolRefMatch?.[1]) {
5780
+ const toolName = resolveToolCallName(toolRefMatch[1], {}, availableToolNames);
5781
+ recovered.push({
5782
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
5783
+ name: toolName,
5784
+ argsJson: "{}"
5785
+ });
5786
+ }
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
+ return recovered;
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;
5436
5825
  }
5437
5826
  class OpenAICompatProvider {
5827
+ agentToolProfile;
5438
5828
  client;
5439
5829
  model;
5830
+ providerId;
5440
5831
  abortController = null;
5441
5832
  constructor(config) {
5442
5833
  const meta = PROVIDERS[config.id];
@@ -5445,7 +5836,9 @@ class OpenAICompatProvider {
5445
5836
  apiKey: config.apiKey || "ollama",
5446
5837
  baseURL
5447
5838
  });
5839
+ this.providerId = config.id;
5448
5840
  this.model = config.model || meta?.defaultModel || "gpt-4o";
5841
+ this.agentToolProfile = resolveAgentToolProfile(config);
5449
5842
  }
5450
5843
  async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
5451
5844
  this.abortController = new AbortController();
@@ -5458,22 +5851,31 @@ class OpenAICompatProvider {
5458
5851
  const stream = await this.client.chat.completions.create(
5459
5852
  {
5460
5853
  model: this.model,
5854
+ max_tokens: 4096,
5461
5855
  stream: true,
5462
5856
  messages
5463
5857
  },
5464
5858
  { signal: this.abortController.signal }
5465
5859
  );
5466
5860
  for await (const chunk of stream) {
5467
- const delta = chunk.choices[0]?.delta?.content;
5468
- if (delta) {
5469
- 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);
5470
5870
  }
5471
5871
  }
5472
5872
  } catch (err) {
5473
5873
  if (err instanceof Error && err.name !== "AbortError") {
5474
- onChunk(`
5874
+ onChunk(
5875
+ `
5475
5876
 
5476
- [Error: ${err.message}]`);
5877
+ [Error: ${formatOpenAICompatErrorMessage(this.providerId, err.message)}]`
5878
+ );
5477
5879
  }
5478
5880
  } finally {
5479
5881
  this.abortController = null;
@@ -5483,6 +5885,7 @@ class OpenAICompatProvider {
5483
5885
  async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
5484
5886
  this.abortController = new AbortController();
5485
5887
  const openAITools = toOpenAITools(tools);
5888
+ const availableToolNames = new Set(tools.map((tool) => tool.name));
5486
5889
  const messages = [
5487
5890
  { role: "system", content: systemPrompt },
5488
5891
  ...(history ?? []).map((m) => ({ role: m.role, content: m.content })),
@@ -5491,18 +5894,31 @@ class OpenAICompatProvider {
5491
5894
  try {
5492
5895
  const maxIterations = getEffectiveMaxIterations();
5493
5896
  let iterationsUsed = 0;
5897
+ let compactRecoveryCount = 0;
5898
+ let compactCorrectionCount = 0;
5899
+ const recentCompactToolSignatures = [];
5900
+ const recentToolNames = [];
5901
+ let clickReadLoopNudged = false;
5494
5902
  for (let i = 0; i < maxIterations; i++) {
5495
5903
  iterationsUsed = i + 1;
5496
5904
  let textAccum = "";
5497
5905
  const toolCallAccums = {};
5498
5906
  let finishReason = null;
5907
+ const hasToolHistory = messages.some((message) => message.role === "tool");
5908
+ const priorToolMessages = messages.filter(
5909
+ (message) => message.role === "tool"
5910
+ );
5911
+ const latestToolMessage = priorToolMessages.length > 0 ? priorToolMessages[priorToolMessages.length - 1] : null;
5912
+ const debugRoundLabel = hasToolHistory ? "post_tool" : "initial";
5499
5913
  const stream = await this.client.chat.completions.create(
5500
5914
  {
5501
5915
  model: this.model,
5916
+ max_tokens: 4096,
5502
5917
  stream: true,
5503
5918
  messages,
5504
5919
  tools: openAITools,
5505
- tool_choice: "auto"
5920
+ tool_choice: "auto",
5921
+ temperature: agentTemperatureForProfile(this.agentToolProfile)
5506
5922
  },
5507
5923
  { signal: this.abortController.signal }
5508
5924
  );
@@ -5511,6 +5927,10 @@ class OpenAICompatProvider {
5511
5927
  if (!choice) continue;
5512
5928
  const delta = choice.delta;
5513
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
+ }
5514
5934
  if (delta.content) {
5515
5935
  textAccum += delta.content;
5516
5936
  onChunk(delta.content);
@@ -5527,17 +5947,62 @@ class OpenAICompatProvider {
5527
5947
  }
5528
5948
  }
5529
5949
  }
5530
- const toolCalls = Object.values(toolCallAccums);
5950
+ let toolCalls = Object.values(toolCallAccums);
5531
5951
  for (const tc of Object.values(toolCallAccums)) {
5532
5952
  if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
5953
+ let 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
+ }
5960
+ }
5961
+ tc.name = resolveToolCallName(tc.name, parsedArgs, availableToolNames);
5962
+ }
5963
+ if (toolCalls.length === 0) {
5964
+ const recoveredToolCalls = recoverTextEncodedToolCalls(
5965
+ textAccum,
5966
+ availableToolNames
5967
+ );
5968
+ if (recoveredToolCalls.length > 0) {
5969
+ toolCalls = recoveredToolCalls;
5970
+ } else {
5971
+ const narratedToolCalls = recoverNarratedActionToolCalls(
5972
+ textAccum,
5973
+ availableToolNames
5974
+ );
5975
+ if (narratedToolCalls.length > 0) {
5976
+ toolCalls = narratedToolCalls;
5977
+ }
5978
+ }
5533
5979
  }
5980
+ logAgentLoopDebug({
5981
+ model: this.model,
5982
+ profile: this.agentToolProfile,
5983
+ iteration: i + 1,
5984
+ round: debugRoundLabel,
5985
+ priorToolCount: priorToolMessages.length,
5986
+ latestToolResultPreview: latestToolMessage ? previewToolDebugContent(String(latestToolMessage.content || "")) : null,
5987
+ finishReason,
5988
+ streamedText: previewDebugValue(textAccum),
5989
+ recoveredFromText: Object.keys(toolCallAccums).length === 0 && toolCalls.length > 0,
5990
+ toolCalls: toolCalls.map((tc) => ({
5991
+ id: tc.id,
5992
+ name: tc.name,
5993
+ argsJson: previewDebugValue(tc.argsJson || "{}", 300)
5994
+ }))
5995
+ });
5534
5996
  const malformedToolCalls = /* @__PURE__ */ new Set();
5535
5997
  for (const tc of toolCalls) {
5536
- try {
5537
- JSON.parse(tc.argsJson || "{}");
5538
- } catch {
5998
+ const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
5999
+ if (!repairedArgs) {
5539
6000
  malformedToolCalls.add(tc.id);
5540
6001
  tc.argsJson = "{}";
6002
+ continue;
6003
+ }
6004
+ if (repairedArgs.repaired) {
6005
+ tc.argsJson = JSON.stringify(repairedArgs.args);
5541
6006
  }
5542
6007
  }
5543
6008
  const assistantMsg = {
@@ -5552,7 +6017,28 @@ class OpenAICompatProvider {
5552
6017
  }
5553
6018
  };
5554
6019
  messages.push(assistantMsg);
5555
- if (toolCalls.length === 0) break;
6020
+ if (toolCalls.length === 0) {
6021
+ if (compactRecoveryCount < 2 && shouldRetryCompactToolLoop(
6022
+ this.agentToolProfile,
6023
+ textAccum,
6024
+ hasToolHistory,
6025
+ userMessage
6026
+ )) {
6027
+ compactRecoveryCount += 1;
6028
+ messages.push({
6029
+ role: "user",
6030
+ content: `[System] ${buildCompactRecoveryPrompt(
6031
+ userMessage,
6032
+ textAccum,
6033
+ latestToolMessage ? String(latestToolMessage.content || "") : null
6034
+ )}`
6035
+ });
6036
+ continue;
6037
+ }
6038
+ break;
6039
+ }
6040
+ compactRecoveryCount = 0;
6041
+ const iterationToolResultPreviews = [];
5556
6042
  for (const tc of toolCalls) {
5557
6043
  if (malformedToolCalls.has(tc.id)) {
5558
6044
  onChunk(`
@@ -5565,43 +6051,341 @@ class OpenAICompatProvider {
5565
6051
  });
5566
6052
  continue;
5567
6053
  }
5568
- let args = {};
5569
- try {
5570
- args = JSON.parse(tc.argsJson || "{}");
5571
- } catch {
6054
+ let args = {};
6055
+ const repairedArgs = parseToolArgsWithRepair(tc.name, tc.argsJson || "{}");
6056
+ if (!repairedArgs) {
6057
+ onChunk(`
6058
+ <<tool:${tc.name}:⚠ invalid args>>
6059
+ `);
6060
+ messages.push({
6061
+ role: "tool",
6062
+ tool_call_id: tc.id,
6063
+ content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON.`
6064
+ });
6065
+ continue;
6066
+ }
6067
+ args = repairedArgs.args;
6068
+ args = coerceToolArgsForExecution(tc.name, args);
6069
+ if (!availableToolNames.has(tc.name)) {
6070
+ onChunk(`
6071
+ <<tool:unsupported_tool:⚠ unsupported>>
6072
+ `);
6073
+ messages.push({
6074
+ role: "tool",
6075
+ tool_call_id: tc.id,
6076
+ content: `Error: ${tc.name} is not a supported tool. Choose one of the available browser tools instead.`
6077
+ });
6078
+ compactCorrectionCount += 1;
6079
+ if (compactCorrectionCount >= 2) {
6080
+ messages.push({
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.`
6083
+ });
6084
+ }
6085
+ continue;
6086
+ }
6087
+ const toolSignature = stableToolSignature(tc.name, args);
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(
6098
+ recentCompactToolSignatures,
6099
+ toolSignature
6100
+ )) {
6101
+ onChunk(`
6102
+ <<tool:${tc.name}:↻ duplicate suppressed>>
6103
+ `);
6104
+ messages.push({
6105
+ role: "tool",
6106
+ tool_call_id: tc.id,
6107
+ content: `Error: Repeated the same tool call (${tc.name}) with the same arguments twice in a row. Do not repeat it. Continue with the next logical step for the original task.`
6108
+ });
6109
+ compactCorrectionCount += 1;
6110
+ if (compactCorrectionCount >= 2) {
6111
+ messages.push({
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.`
6114
+ });
6115
+ }
6116
+ continue;
6117
+ }
6118
+ const argSummary = args.url || args.query || args.text || args.direction || "";
6119
+ onChunk(`
6120
+ <<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
6121
+ `);
6122
+ let result;
6123
+ try {
6124
+ result = await onToolCall(tc.name, args);
6125
+ } catch (toolErr) {
6126
+ const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
6127
+ result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
6128
+ }
6129
+ let toolContent = result;
6130
+ try {
6131
+ const parsed = JSON.parse(result);
6132
+ if (isRichToolResult(parsed)) {
6133
+ toolContent = parsed.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
6134
+ }
6135
+ } catch {
6136
+ }
6137
+ if (this.agentToolProfile === "compact") {
6138
+ recentCompactToolSignatures.push(toolSignature);
6139
+ if (recentCompactToolSignatures.length > 4) {
6140
+ recentCompactToolSignatures.shift();
6141
+ }
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
+ }
6152
+ compactCorrectionCount = 0;
6153
+ iterationToolResultPreviews.push(toolContent);
6154
+ messages.push({
6155
+ role: "tool",
6156
+ tool_call_id: tc.id,
6157
+ content: toolContent
6158
+ });
6159
+ }
6160
+ const followUpReminder = followUpReminderForProfile(
6161
+ this.agentToolProfile,
6162
+ userMessage,
6163
+ textAccum,
6164
+ iterationToolResultPreviews.length > 0 ? iterationToolResultPreviews[iterationToolResultPreviews.length - 1] : null
6165
+ );
6166
+ if (followUpReminder) {
6167
+ messages.push(followUpReminder);
6168
+ }
6169
+ }
6170
+ if (iterationsUsed >= maxIterations) {
6171
+ onChunk(`
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) {
5572
6327
  onChunk(`
5573
- <<tool:${tc.name}:⚠ invalid args>>
6328
+ <<tool:${tb.name}:⚠ invalid args>>
5574
6329
  `);
5575
- messages.push({
5576
- role: "tool",
5577
- tool_call_id: tc.id,
5578
- content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON.`
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
5579
6335
  });
5580
6336
  continue;
5581
6337
  }
5582
- const argSummary = args.url || args.text || args.direction || "";
6338
+ const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
5583
6339
  onChunk(`
5584
- <<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
6340
+ <<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
5585
6341
  `);
5586
6342
  let result;
5587
6343
  try {
5588
- result = await onToolCall(tc.name, args);
6344
+ result = await onToolCall(tb.name, tb.input);
5589
6345
  } catch (toolErr) {
5590
6346
  const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
5591
6347
  result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
5592
6348
  }
5593
- let toolContent = result;
6349
+ let parsedRich = null;
5594
6350
  try {
5595
6351
  const parsed = JSON.parse(result);
5596
- if (isRichToolResult(parsed)) {
5597
- toolContent = parsed.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
5598
- }
6352
+ if (isRichToolResult(parsed)) parsedRich = parsed;
5599
6353
  } catch {
5600
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;
5601
6386
  messages.push({
5602
- role: "tool",
5603
- tool_call_id: tc.id,
5604
- content: toolContent
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.`
5605
6389
  });
5606
6390
  }
5607
6391
  }
@@ -5653,6 +6437,55 @@ function validateProviderConnection(config, options = { requireModel: true }) {
5653
6437
  }
5654
6438
  return null;
5655
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
+ }
5656
6489
  async function fetchProviderModels(config) {
5657
6490
  const normalized = sanitizeProviderConfig(config);
5658
6491
  const error = validateProviderConnection(normalized, { requireModel: false });
@@ -5662,7 +6495,7 @@ async function fetchProviderModels(config) {
5662
6495
  if (normalized.id === "anthropic") {
5663
6496
  const client2 = new Anthropic({ apiKey: normalized.apiKey });
5664
6497
  const page2 = await client2.models.list();
5665
- return page2.data.map((model) => model.id);
6498
+ return { ok: true, models: page2.data.map((model) => model.id) };
5666
6499
  }
5667
6500
  const meta = PROVIDERS[normalized.id];
5668
6501
  const baseURL = normalized.baseUrl || meta?.defaultBaseUrl || "https://api.openai.com/v1";
@@ -5671,7 +6504,13 @@ async function fetchProviderModels(config) {
5671
6504
  baseURL
5672
6505
  });
5673
6506
  const page = await client.models.list();
5674
- 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
+ };
5675
6514
  }
5676
6515
  function createProvider(config) {
5677
6516
  const normalized = sanitizeProviderConfig(config);
@@ -5728,7 +6567,7 @@ function createTraceSession(query, url, title) {
5728
6567
  }
5729
6568
  const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
5730
6569
  const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
5731
- function elementLabel(el) {
6570
+ function elementLabel$1(el) {
5732
6571
  return el.text?.trim() || el.label?.trim() || el.value?.trim() || el.placeholder?.trim() || void 0;
5733
6572
  }
5734
6573
  function isOverlayAction(el) {
@@ -5744,11 +6583,11 @@ function isRadioOption(el) {
5744
6583
  function normalizeAction(el) {
5745
6584
  return {
5746
6585
  index: el.index,
5747
- label: elementLabel(el),
6586
+ label: elementLabel$1(el),
5748
6587
  selector: el.selector,
5749
6588
  role: el.role,
5750
6589
  labelSource: el.labelSource,
5751
- looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel(el))
6590
+ looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel$1(el))
5752
6591
  };
5753
6592
  }
5754
6593
  function normalizeStoredAction(action) {
@@ -6044,7 +6883,7 @@ function getCartItemLinks(page) {
6044
6883
  return false;
6045
6884
  }
6046
6885
  if (blockedText.test(text) || blockedHref.test(href)) return false;
6047
- const key = `${normalizeComparable(text)}|${normalizeUrlForMatch(href) || href}`;
6886
+ const key = `${normalizeComparable$1(text)}|${normalizeUrlForMatch$1(href) || href}`;
6048
6887
  if (seen.has(key)) return false;
6049
6888
  seen.add(key);
6050
6889
  return true;
@@ -6113,6 +6952,67 @@ function formatCartSnapshot(page) {
6113
6952
  function isVisibleToUser(el) {
6114
6953
  return el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true;
6115
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
+ }
6116
7016
  function getDialogFocusedElements(page) {
6117
7017
  return page.interactiveElements.filter(
6118
7018
  (el) => isVisibleToUser(el) && el.context === "dialog"
@@ -6166,6 +7066,10 @@ function formatInteractiveElements(elements) {
6166
7066
  const scoreEl = (el) => {
6167
7067
  let s = 0;
6168
7068
  if (el.context === "dialog") s -= 40;
7069
+ const purchasePriority = purchaseActionPriority(el);
7070
+ if (Number.isFinite(purchasePriority)) {
7071
+ s -= 25 - purchasePriority * 5;
7072
+ }
6169
7073
  if (el.visible === false) s += 100;
6170
7074
  if (el.inViewport === false) s += 50;
6171
7075
  if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
@@ -6525,10 +7429,10 @@ function chooseAgentReadMode(page) {
6525
7429
  return "visible_only";
6526
7430
  }
6527
7431
  }
6528
- function normalizeComparable(value) {
7432
+ function normalizeComparable$1(value) {
6529
7433
  return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
6530
7434
  }
6531
- function normalizeUrlForMatch(value) {
7435
+ function normalizeUrlForMatch$1(value) {
6532
7436
  if (!value) return null;
6533
7437
  try {
6534
7438
  const url = new URL(value);
@@ -6538,7 +7442,7 @@ function normalizeUrlForMatch(value) {
6538
7442
  return value.trim().replace(/\/+$/, "").toLowerCase() || null;
6539
7443
  }
6540
7444
  }
6541
- function getUrlPathSegments(value) {
7445
+ function getUrlPathSegments$1(value) {
6542
7446
  if (!value) return [];
6543
7447
  try {
6544
7448
  return new URL(value).pathname.split("/").filter(Boolean);
@@ -6547,7 +7451,7 @@ function getUrlPathSegments(value) {
6547
7451
  }
6548
7452
  }
6549
7453
  function isSearchOrListingPage(page) {
6550
- const haystack = normalizeComparable(
7454
+ const haystack = normalizeComparable$1(
6551
7455
  [
6552
7456
  page.url,
6553
7457
  page.title,
@@ -6587,30 +7491,30 @@ function getResultCandidates(page) {
6587
7491
  const entityItems = collectJsonLdEntityItems(page.jsonLd ?? []);
6588
7492
  const entityNames = new Set(
6589
7493
  entityItems.map(
6590
- (item) => typeof item.name === "string" ? normalizeComparable(item.name) : ""
7494
+ (item) => typeof item.name === "string" ? normalizeComparable$1(item.name) : ""
6591
7495
  ).filter(Boolean)
6592
7496
  );
6593
7497
  const entityUrls = new Set(
6594
7498
  entityItems.map(
6595
- (item) => typeof item.url === "string" ? normalizeUrlForMatch(item.url) : null
7499
+ (item) => typeof item.url === "string" ? normalizeUrlForMatch$1(item.url) : null
6596
7500
  ).filter((value) => Boolean(value))
6597
7501
  );
6598
- const pageHost = normalizeUrlForMatch(page.url);
7502
+ const pageHost = normalizeUrlForMatch$1(page.url);
6599
7503
  const searchOrListingPage = isSearchOrListingPage(page);
6600
7504
  const scored = page.interactiveElements.filter(
6601
7505
  (element) => element.type === "link" && element.text?.trim() && element.href
6602
7506
  ).map((element) => {
6603
7507
  const text = element.text?.trim() || "";
6604
- const comparableText = normalizeComparable(text);
6605
- const href = normalizeUrlForMatch(element.href);
6606
- const haystack = normalizeComparable(
7508
+ const comparableText = normalizeComparable$1(text);
7509
+ const href = normalizeUrlForMatch$1(element.href);
7510
+ const haystack = normalizeComparable$1(
6607
7511
  [element.text, element.description, element.selector, element.href].filter(Boolean).join(" ")
6608
7512
  );
6609
7513
  let score = 0;
6610
7514
  if (entityNames.has(comparableText)) score += 6;
6611
7515
  if (href && entityUrls.has(href)) score += 6;
6612
7516
  if (entityItems.some((item) => {
6613
- const name = typeof item.name === "string" ? normalizeComparable(item.name) : "";
7517
+ const name = typeof item.name === "string" ? normalizeComparable$1(item.name) : "";
6614
7518
  return Boolean(name) && (name.includes(comparableText) || comparableText.includes(name));
6615
7519
  })) {
6616
7520
  score += 4;
@@ -6624,7 +7528,7 @@ function getResultCandidates(page) {
6624
7528
  } catch {
6625
7529
  }
6626
7530
  }
6627
- const hrefSegments = getUrlPathSegments(element.href);
7531
+ const hrefSegments = getUrlPathSegments$1(element.href);
6628
7532
  if (hrefSegments.length >= 2) score += 1;
6629
7533
  if (text.includes("/")) score += 1;
6630
7534
  if (searchOrListingPage && (element.context === "article" || element.context === "main" || element.context === "content")) {
@@ -6657,7 +7561,7 @@ function getResultCandidates(page) {
6657
7561
  );
6658
7562
  const seen = /* @__PURE__ */ new Set();
6659
7563
  return scored.map(({ element }) => element).filter((element) => {
6660
- const key = `${normalizeComparable(element.text || "")}|${normalizeUrlForMatch(element.href) || ""}`;
7564
+ const key = `${normalizeComparable$1(element.text || "")}|${normalizeUrlForMatch$1(element.href) || ""}`;
6661
7565
  if (seen.has(key)) return false;
6662
7566
  seen.add(key);
6663
7567
  return true;
@@ -6893,6 +7797,10 @@ function buildScopedContext(page, mode) {
6893
7797
  })).filter((form) => form.fields.length > 0)
6894
7798
  };
6895
7799
  const quantityElements = getQuantityElements(visiblePage);
7800
+ const purchaseActions = getPurchaseActionElements(visiblePage, {
7801
+ visibleOnly: true
7802
+ });
7803
+ const offscreenPurchaseActions = getOffscreenPurchaseActionElements(page);
6896
7804
  const cartSnapshot = formatCartSnapshot(visiblePage);
6897
7805
  const visibleForms = visiblePage.forms;
6898
7806
  const dialogFocus = formatDialogFocus(page);
@@ -6948,6 +7856,19 @@ function buildScopedContext(page, mode) {
6948
7856
  sections.push(formatQuantityElements(quantityElements));
6949
7857
  sections.push("");
6950
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
+ }
6951
7872
  if (visiblePage.interactiveElements.length > 0) {
6952
7873
  sections.push(
6953
7874
  `### Visible In-Viewport Interactive Elements (${visiblePage.interactiveElements.length})`
@@ -7216,28 +8137,466 @@ ${truncateContent(page.content)}
7216
8137
  **Task:** Summarize this web page based on the structure and content above. Identify the main purpose, key sections, and important interactive elements.`
7217
8138
  };
7218
8139
  }
7219
- function buildQuestionPrompt(page, question) {
7220
- const structuredContext = buildStructuredContext(page);
7221
- return {
7222
- system: "You are Vessel, an AI browsing assistant. Use the provided page structure and content to answer questions accurately. You can reference specific elements by their labels or positions.",
7223
- user: `${structuredContext}
7224
-
7225
- ## PAGE CONTENT
7226
-
7227
- ${truncateContent(page.content)}
7228
-
7229
- ---
7230
-
7231
- **Question:** ${question}
7232
-
7233
- **Instructions:** Answer based on the page structure and content above. If the question asks about interactive elements, forms, or navigation, use the structured context to provide specific details.`
7234
- };
8140
+ function buildQuestionPrompt(page, question) {
8141
+ const structuredContext = buildStructuredContext(page);
8142
+ return {
8143
+ system: "You are Vessel, an AI browsing assistant. Use the provided page structure and content to answer questions accurately. You can reference specific elements by their labels or positions.",
8144
+ user: `${structuredContext}
8145
+
8146
+ ## PAGE CONTENT
8147
+
8148
+ ${truncateContent(page.content)}
8149
+
8150
+ ---
8151
+
8152
+ **Question:** ${question}
8153
+
8154
+ **Instructions:** Answer based on the page structure and content above. If the question asks about interactive elements, forms, or navigation, use the structured context to provide specific details.`
8155
+ };
8156
+ }
8157
+ function buildGeneralPrompt(query) {
8158
+ return {
8159
+ system: "You are Vessel, an AI assistant embedded in a web browser. You can normally see the content of the page the user is viewing, but no page is currently active. Help the user with their browsing needs. Be concise and helpful.",
8160
+ user: query
8161
+ };
8162
+ }
8163
+ const SHARED_CORE_INSTRUCTIONS = [
8164
+ "You can see the page the user is viewing. The content above is from the page.",
8165
+ "The structured page context always refers to the tab currently visible to the human unless a later tool call changes tabs.",
8166
+ "Use tools to interact with the page when asked to do something (navigate, click, type, select options, submit forms, press keys, scroll).",
8167
+ "Only say you completed an action after the corresponding tool succeeds. If no tool supports the request, say so plainly.",
8168
+ "Call one tool at a time unless you are certain your provider supports parallel tool calls. Sequential calls are more reliable.",
8169
+ "ACT, DON'T HEDGE: You have a full browser. If the user asks you to go somewhere and do something, start doing it immediately."
8170
+ ];
8171
+ const SHARED_NAVIGATION_INSTRUCTIONS = [
8172
+ "Use current_tab when you only need to know what the human is currently looking at. Use list_tabs before switching context across multiple tabs.",
8173
+ "Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.",
8174
+ "After navigating to a new site, do not call read_page immediately unless you are genuinely stuck. Prefer the site's search box, known navigation patterns, or clicking a visible section first.",
8175
+ "On retail and marketplace sites, prefer the site's visible search box, filters, and result pages over direct product URLs.",
8176
+ "For broad discovery tasks, prefer direct sources and site-specific search over generic search engines."
8177
+ ];
8178
+ const SHARED_READ_INSTRUCTIONS = [
8179
+ "The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.",
8180
+ "When you only need detail on one result, card, or form section, use inspect_element instead of reading the whole page.",
8181
+ 'Escalate page reads progressively: read_page(mode="glance"), then visible_only/results_only/forms_only/summary/text_only as needed. Use read_page(mode="debug") only as a last resort.',
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.',
8183
+ "Use screenshot when you need the exact rendered page or text extraction is failing.",
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.",
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."
8186
+ ];
8187
+ const DEFAULT_EXTRA_INSTRUCTIONS = [
8188
+ "Create a checkpoint before risky multi-step flows or before leaving an important state.",
8189
+ "Use save_session after completing a login flow you may need again later, and load_session to resume that authenticated state in future runs.",
8190
+ "If the user says they highlighted or selected text, use read_page before falling back to screenshots because it includes active selection and visible unsaved highlights.",
8191
+ "If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.",
8192
+ "If the page context reports a rate limit, human verification, or access warning, stop using that page and switch to a different source.",
8193
+ "Reference interactive elements by their index number (shown as [#N] in the listings above).",
8194
+ "Be concise. Explain what you're doing as you go.",
8195
+ "For simple questions about the page, just answer directly without using tools.",
8196
+ "VISUAL AWARENESS: The human is watching the browser alongside this chat. Use highlights proactively when you reference specific on-page findings or errors.",
8197
+ "After completing a task or answering a question, offer 1-2 brief, natural follow-up suggestions that make sense in context.",
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.',
8199
+ "USE YOUR KNOWLEDGE: When the user asks for recommendations, make a clear recommendation, explain your reasoning briefly, and then execute.",
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."
8202
+ ];
8203
+ const COMPACT_FOCUS_INSTRUCTIONS = [
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.",
8205
+ "Do not ask the user for permission to continue a task they already requested.",
8206
+ "Stay on the current task until it is complete. Do not restart completed phases such as re-navigating to the same site or redoing discovery after you already have candidates.",
8207
+ "If you are already on the requested site, do not navigate to its homepage again unless the current page is clearly unusable.",
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.",
8209
+ "Use current_tab only if you are genuinely unsure of the current page after reading the latest tool result.",
8210
+ "On retail tasks, prefer this sequence: navigate -> site search or curated section -> inspect/read results -> click a product -> add to cart -> explain.",
8211
+ 'On product/detail pages, prefer read_page(mode="visible_only") to find indexed purchase controls like Add to Cart, quantity, and checkout actions.',
8212
+ "When read_page or inspect_element gives you an element index, prefer click(index=N) over selector-based clicks.",
8213
+ 'If a product page has no visible purchase control, scroll and call read_page(mode="visible_only") again. Do not loop on generic inspect_element calls against navigation or unrelated regions.',
8214
+ "After adding an item to cart and going back, ALWAYS call read_page to see the current results. The system shows which products are already in your cart — do NOT click those again. Pick a DIFFERENT product from the list. If all visible results are already in cart, scroll down for more.",
8215
+ "Keep your reasoning short. Prefer taking the next tool action over writing a long plan."
8216
+ ];
8217
+ function buildInstructionBlock(instructions) {
8218
+ return instructions.map((line) => `- ${line}`).join("\n");
8219
+ }
8220
+ function buildContextBlock(input) {
8221
+ return `You are Vessel, an AI agent embedded in a web browser. You can see the current page and interact with it using tools.
8222
+
8223
+ THE USER IS CURRENTLY LOOKING AT:
8224
+ Title: ${input.activeTabTitle}
8225
+ URL: ${input.activeTabUrl}${input.tabSummary || ""}
8226
+
8227
+ When the user says "this page", "this article", "this site", or asks about what they're viewing, they mean the page above. The context below is from that page.
8228
+
8229
+ Current page context:
8230
+ This brief is intentionally minimal and filtered for speed. It omits most page text and low-value chrome unless you explicitly ask for more.
8231
+ Default brief mode: ${input.defaultReadMode}
8232
+ Detected page type: ${input.pageType}
8233
+
8234
+ ${input.structuredContext}
8235
+
8236
+ Supervisor state:
8237
+ - paused: ${input.supervisorPaused ? "yes" : "no"}
8238
+ - approval mode: ${input.approvalMode}
8239
+ - pending approvals: ${input.pendingApprovals}
8240
+
8241
+ Recent checkpoints:
8242
+ ${input.recentCheckpoints || "- none"}
8243
+
8244
+ Task tracker:
8245
+ ${input.taskTrackerContext || "- none"}`;
8246
+ }
8247
+ function buildAgentSystemPrompt(input) {
8248
+ const instructionBlocks = input.profile === "compact" ? [
8249
+ buildInstructionBlock(SHARED_CORE_INSTRUCTIONS),
8250
+ buildInstructionBlock(SHARED_NAVIGATION_INSTRUCTIONS),
8251
+ buildInstructionBlock(SHARED_READ_INSTRUCTIONS),
8252
+ buildInstructionBlock(COMPACT_FOCUS_INSTRUCTIONS)
8253
+ ] : [
8254
+ buildInstructionBlock(SHARED_CORE_INSTRUCTIONS),
8255
+ buildInstructionBlock(SHARED_NAVIGATION_INSTRUCTIONS),
8256
+ buildInstructionBlock(SHARED_READ_INSTRUCTIONS),
8257
+ buildInstructionBlock(DEFAULT_EXTRA_INSTRUCTIONS)
8258
+ ];
8259
+ return [
8260
+ buildContextBlock(input),
8261
+ "Instructions:",
8262
+ ...instructionBlocks
8263
+ ].join("\n\n");
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
+ );
7235
8481
  }
7236
- function buildGeneralPrompt(query) {
7237
- return {
7238
- system: "You are Vessel, an AI assistant embedded in a web browser. You can normally see the content of the page the user is viewing, but no page is currently active. Help the user with their browsing needs. Be concise and helpful.",
7239
- user: query
7240
- };
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");
7241
8600
  }
7242
8601
  const WRAPPING_QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
7243
8602
  function stripWrappingQuotes(value) {
@@ -7384,10 +8743,11 @@ const TOOL_DEFINITIONS = [
7384
8743
  {
7385
8744
  name: "click",
7386
8745
  title: "Click Element",
7387
- 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.",
7388
8747
  inputSchema: {
7389
8748
  index: zod.z.number().optional().describe("Element index from the page content listing"),
7390
- 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")
7391
8751
  },
7392
8752
  tier: 0
7393
8753
  },
@@ -7508,10 +8868,11 @@ const TOOL_DEFINITIONS = [
7508
8868
  {
7509
8869
  name: "inspect_element",
7510
8870
  title: "Inspect Element",
7511
- 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.",
7512
8872
  inputSchema: {
7513
8873
  index: zod.z.number().optional().describe("Element index to inspect"),
7514
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"),
7515
8876
  limit: zod.z.number().optional().describe("Maximum nearby controls to include (default 8)")
7516
8877
  },
7517
8878
  tier: 1,
@@ -7863,10 +9224,11 @@ const TOOL_DEFINITIONS = [
7863
9224
  {
7864
9225
  name: "scroll_to_element",
7865
9226
  title: "Scroll To Element",
7866
- 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.",
7867
9228
  inputSchema: {
7868
9229
  index: zod.z.number().optional().describe("Element index to scroll to"),
7869
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"),
7870
9232
  position: zod.z.enum(["center", "top", "bottom"]).optional().describe(
7871
9233
  "Where to position the element in the viewport (default center)"
7872
9234
  )
@@ -7956,10 +9318,16 @@ const CONTEXT_HINTS = {
7956
9318
  scroll: "💡 Long content — scroll to continue — "
7957
9319
  }
7958
9320
  };
7959
- function scoreForContext(toolName, pageType) {
9321
+ function scoreForContext(toolName, pageType, intents) {
7960
9322
  const def = defByName[toolName];
7961
9323
  if (!def) return 500;
7962
9324
  if (pageType === "SEARCH_READY") {
9325
+ if (intents.has("navigate")) {
9326
+ if (toolName === "navigate") return -30;
9327
+ if (toolName === "search") return 2;
9328
+ if (toolName === "type_text") return 5;
9329
+ if (toolName === "press_key") return 6;
9330
+ }
7963
9331
  if (toolName === "search") return -20;
7964
9332
  if (toolName === "type_text") return 5;
7965
9333
  if (toolName === "press_key") return 6;
@@ -7993,10 +9361,53 @@ const ALWAYS_FAST_TOOL_NAMES = /* @__PURE__ */ new Set([
7993
9361
  "screenshot",
7994
9362
  "inspect_element"
7995
9363
  ]);
9364
+ const COMPACT_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
9365
+ "navigate",
9366
+ "go_back",
9367
+ "click",
9368
+ "type_text",
9369
+ "press_key",
9370
+ "scroll",
9371
+ "dismiss_popup",
9372
+ "clear_overlays",
9373
+ "accept_cookies",
9374
+ "read_page",
9375
+ "wait_for",
9376
+ "inspect_element",
9377
+ "search"
9378
+ ]);
9379
+ const COMPACT_CONTEXTUAL_TOOL_NAMES = {
9380
+ LOGIN: ["fill_form", "submit_form", "login"],
9381
+ FORM: ["fill_form", "select_option", "submit_form"],
9382
+ SHOPPING: ["select_option", "fill_form", "submit_form"],
9383
+ SEARCH_RESULTS: ["paginate", "scroll_to_element"],
9384
+ PAGINATED_LIST: ["paginate", "scroll_to_element"]
9385
+ };
9386
+ const COMPACT_INTENT_TOOL_NAMES = {
9387
+ tabs: ["current_tab", "list_tabs", "switch_tab", "create_tab"],
9388
+ bookmarks: [
9389
+ "list_bookmarks",
9390
+ "search_bookmarks",
9391
+ "create_bookmark_folder",
9392
+ "save_bookmark",
9393
+ "organize_bookmark",
9394
+ "archive_bookmark",
9395
+ "open_bookmark"
9396
+ ],
9397
+ sessions: ["login", "save_session", "load_session", "list_sessions", "delete_session"],
9398
+ workflow: ["create_checkpoint", "restore_checkpoint", "flow_start", "flow_advance", "flow_status", "flow_end"],
9399
+ metrics: ["metrics"],
9400
+ highlight: ["highlight", "clear_highlights"],
9401
+ table: ["extract_table"],
9402
+ debug: ["current_tab", "reload", "set_ad_blocking", "suggest", "screenshot"]
9403
+ };
7996
9404
  function inferIntent(query) {
7997
9405
  const lowered = query.toLowerCase();
7998
9406
  const intents = /* @__PURE__ */ new Set();
7999
9407
  if (/\b(tab|tabs|window|windows)\b/.test(lowered)) intents.add("tabs");
9408
+ if (/\b(go to|goto|open|visit|navigate to)\b/.test(lowered) || /\b[a-z0-9-]+\.(com|org|net|io|dev|app|ai|co|edu|gov)\b/.test(lowered) || /\bhttps?:\/\//.test(lowered)) {
9409
+ intents.add("navigate");
9410
+ }
8000
9411
  if (/\b(bookmark|bookmarks|save this|folder)\b/.test(lowered)) {
8001
9412
  intents.add("bookmarks");
8002
9413
  }
@@ -8019,7 +9430,18 @@ function inferIntent(query) {
8019
9430
  }
8020
9431
  return intents;
8021
9432
  }
8022
- function shouldIncludeTool(toolName, pageType, intents) {
9433
+ function shouldIncludeTool(toolName, pageType, intents, profile) {
9434
+ if (profile === "compact") {
9435
+ if (COMPACT_CORE_TOOL_NAMES.has(toolName)) return true;
9436
+ const contextualTools = COMPACT_CONTEXTUAL_TOOL_NAMES[pageType] ?? [];
9437
+ if (contextualTools.includes(toolName)) return true;
9438
+ for (const intent of intents) {
9439
+ if ((COMPACT_INTENT_TOOL_NAMES[intent] ?? []).includes(toolName)) {
9440
+ return true;
9441
+ }
9442
+ }
9443
+ return false;
9444
+ }
8023
9445
  if (ALWAYS_FAST_TOOL_NAMES.has(toolName)) return true;
8024
9446
  switch (toolName) {
8025
9447
  case "select_option":
@@ -8076,13 +9498,14 @@ function shouldIncludeTool(toolName, pageType, intents) {
8076
9498
  return !defByName[toolName]?.hiddenByDefault;
8077
9499
  }
8078
9500
  }
8079
- function pruneToolsForContext(tools, pageType, query = "") {
9501
+ function pruneToolsForContext(tools, pageType, query = "", options = {}) {
8080
9502
  const ctx = pageType ?? "GENERAL";
8081
9503
  const hints = CONTEXT_HINTS[ctx] ?? {};
8082
9504
  const intents = inferIntent(query);
8083
- const scored = tools.filter((tool) => shouldIncludeTool(tool.name, ctx, intents)).map((tool) => ({
9505
+ const profile = options.profile ?? "default";
9506
+ const scored = tools.filter((tool) => shouldIncludeTool(tool.name, ctx, intents, profile)).map((tool) => ({
8084
9507
  tool,
8085
- score: scoreForContext(tool.name, ctx)
9508
+ score: scoreForContext(tool.name, ctx, intents)
8086
9509
  }));
8087
9510
  scored.sort((a, b) => a.score - b.score);
8088
9511
  return scored.map(({ tool, score }) => {
@@ -8746,6 +10169,87 @@ async function captureScreenshot(wc) {
8746
10169
  }
8747
10170
  return { ok: false, error: "Page image was empty after 3 attempts" };
8748
10171
  }
10172
+ function normalizeForComparison(value) {
10173
+ return value.toLowerCase().replace(/https?:\/\//g, "").replace(/www\./g, "").replace(/[^a-z0-9]+/g, " ").trim();
10174
+ }
10175
+ function canonicalizeUrlForComparison(value) {
10176
+ try {
10177
+ const url = new URL(value);
10178
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
10179
+ url.hostname = url.hostname.replace(/^www\./, "");
10180
+ url.hash = "";
10181
+ if (url.pathname.endsWith("/") && url.pathname !== "/") {
10182
+ url.pathname = url.pathname.replace(/\/+$/, "");
10183
+ }
10184
+ return url.toString();
10185
+ } catch {
10186
+ return null;
10187
+ }
10188
+ }
10189
+ function isRedundantNavigateTarget(currentUrl, targetUrl) {
10190
+ const current = canonicalizeUrlForComparison(currentUrl);
10191
+ const target = canonicalizeUrlForComparison(targetUrl);
10192
+ return current !== null && target !== null && current === target;
10193
+ }
10194
+ function looksLikeCurrentSiteNameQuery(query, currentUrl, currentTitle) {
10195
+ const normalizedQuery = normalizeForComparison(query);
10196
+ if (!normalizedQuery) return false;
10197
+ let hostnameLabel = "";
10198
+ try {
10199
+ const url = new URL(currentUrl);
10200
+ hostnameLabel = url.hostname.replace(/^www\./, "").split(".")[0] || "";
10201
+ } catch {
10202
+ }
10203
+ const normalizedTitle = normalizeForComparison(currentTitle);
10204
+ const normalizedHost = normalizeForComparison(hostnameLabel);
10205
+ const normalizedTitlePrefix = normalizeForComparison(
10206
+ currentTitle.split("|")[0]?.split("—")[0]?.split("-")[0] || currentTitle
10207
+ );
10208
+ if (normalizedTitle && normalizedQuery === normalizedTitle) return true;
10209
+ if (normalizedTitlePrefix && normalizedQuery === normalizedTitlePrefix) {
10210
+ return true;
10211
+ }
10212
+ if (normalizedHost && normalizedQuery === normalizedHost) return true;
10213
+ const titleTokens = new Set(normalizedTitle.split(/\s+/).filter(Boolean));
10214
+ const queryTokens = normalizedQuery.split(/\s+/).filter(Boolean);
10215
+ if (normalizedHost && queryTokens.includes(normalizedHost) && queryTokens.every((token) => titleTokens.has(token) || token === normalizedHost)) {
10216
+ return true;
10217
+ }
10218
+ return false;
10219
+ }
10220
+ function extractExplicitDomains(goal) {
10221
+ const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
10222
+ if (!matches) return [];
10223
+ const normalized = matches.map(
10224
+ (match) => match.replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase()
10225
+ );
10226
+ return [...new Set(normalized)];
10227
+ }
10228
+ function apexDomain(hostname) {
10229
+ const parts = hostname.replace(/^www\./, "").split(".").filter(Boolean);
10230
+ if (parts.length <= 2) return parts.join(".");
10231
+ return parts.slice(-2).join(".");
10232
+ }
10233
+ function shouldBlockOffGoalDomainNavigation(goal, targetUrl) {
10234
+ const explicitDomains = extractExplicitDomains(goal);
10235
+ if (explicitDomains.length !== 1) return null;
10236
+ let targetHost = "";
10237
+ try {
10238
+ const url = new URL(targetUrl);
10239
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
10240
+ targetHost = url.hostname.replace(/^www\./, "").toLowerCase();
10241
+ } catch {
10242
+ return null;
10243
+ }
10244
+ const requestedDomain = explicitDomains[0];
10245
+ if (targetHost === requestedDomain || targetHost.endsWith(`.${requestedDomain}`) || apexDomain(targetHost) === apexDomain(requestedDomain)) {
10246
+ return null;
10247
+ }
10248
+ return {
10249
+ requestedDomain,
10250
+ targetDomain: targetHost
10251
+ };
10252
+ }
8749
10253
  const SESSION_VERSION = 1;
8750
10254
  function getSessionsDir() {
8751
10255
  return path$1.join(electron.app.getPath("userData"), "named-sessions");
@@ -9024,6 +10528,340 @@ function deleteNamedSession(name) {
9024
10528
  fs$1.unlinkSync(filePath);
9025
10529
  return true;
9026
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
+ }
9027
10865
  const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS = 1500;
9028
10866
  const QUIET_NAVIGATION_WINDOW_MS = 1200;
9029
10867
  const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
@@ -9358,6 +11196,47 @@ WARNING: Blocking overlay detected (${overlaySignal}). Call clear_overlays or ac
9358
11196
  }
9359
11197
  return titleLine;
9360
11198
  }
11199
+ async function getPostSearchSummary(wc) {
11200
+ await waitForLoad$1(wc, 2e3);
11201
+ try {
11202
+ const content = await Promise.race([
11203
+ extractContent(wc),
11204
+ new Promise((resolve) => setTimeout(() => resolve(null), 2500))
11205
+ ]);
11206
+ if (content && content.content.length > 0) {
11207
+ const scoped = buildScopedContext(content, "results_only");
11208
+ const truncated = scoped.length > 2600 ? `${scoped.slice(0, 2600)}
11209
+ [Search results snapshot truncated...]` : scoped;
11210
+ return `
11211
+ Search results snapshot:
11212
+ ${truncated}`;
11213
+ }
11214
+ } catch {
11215
+ }
11216
+ const fallback = await getPostNavSummary(wc);
11217
+ return fallback ? `${fallback}
11218
+ Search results snapshot unavailable. Use read_page(mode="results_only") if needed.` : `
11219
+ Search results snapshot unavailable. Use read_page(mode="results_only") if needed.`;
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
+ }
9361
11240
  async function scrollPage$1(wc, deltaY) {
9362
11241
  const getScrollY = async () => {
9363
11242
  const scrollY = await executePageScript(
@@ -9550,9 +11429,17 @@ async function describeElementForClick$1(wc, selector) {
9550
11429
  if (!el) return { error: "Element not found" };
9551
11430
  const anchor = el instanceof HTMLAnchorElement ? el : el.closest("a[href]");
9552
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;
9553
11437
  return {
9554
11438
  text: text || "Element",
9555
11439
  href: anchor instanceof HTMLAnchorElement ? anchor.href : undefined,
11440
+ target: anchor instanceof HTMLAnchorElement ? (anchor.getAttribute("target") || "") : undefined,
11441
+ tag,
11442
+ isInteractive,
9556
11443
  };
9557
11444
  })()
9558
11445
  `,
@@ -9571,7 +11458,10 @@ async function describeElementForClick$1(wc, selector) {
9571
11458
  }
9572
11459
  return {
9573
11460
  text: "text" in result && typeof result.text === "string" ? result.text : "Element",
9574
- 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
9575
11465
  };
9576
11466
  }
9577
11467
  async function inspectElement(wc, selector, limit = 8) {
@@ -9655,6 +11545,18 @@ async function inspectElement(wc, selector, limit = 8) {
9655
11545
  ) || "element";
9656
11546
  }
9657
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
+
9658
11560
  function chooseRegion(target) {
9659
11561
  const preferred = target.closest(
9660
11562
  "[data-testid], article, [role='article'], [role='listitem'], li, tr, form, section, aside, dialog, [role='dialog']"
@@ -9680,19 +11582,69 @@ async function inspectElement(wc, selector, limit = 8) {
9680
11582
  const region = chooseRegion(target);
9681
11583
  const nearby = [];
9682
11584
  const seen = new Set();
11585
+ const purchaseActions = [];
11586
+ const purchaseSeen = new Set();
9683
11587
  region.querySelectorAll("a[href], button, input:not([type='hidden']), select, textarea").forEach((el) => {
9684
11588
  if (!(el instanceof HTMLElement) || !isVisible(el)) return;
9685
11589
  const candidateSelector = selectorFor(el);
9686
11590
  if (!candidateSelector || seen.has(candidateSelector)) return;
9687
11591
  seen.add(candidateSelector);
11592
+ const candidateLabel = labelFor(el).slice(0, 100);
11593
+ const candidateHref = el instanceof HTMLAnchorElement ? text(el.href) : undefined;
9688
11594
  nearby.push({
9689
- 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,
9690
11634
  type: el.tagName.toLowerCase(),
9691
11635
  selector: candidateSelector,
9692
- href: el instanceof HTMLAnchorElement ? text(el.href) : undefined,
11636
+ href: candidateHref,
11637
+ source: "page",
11638
+ rank: purchaseRank,
9693
11639
  });
9694
11640
  });
9695
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
+
9696
11648
  return {
9697
11649
  target: {
9698
11650
  label: labelFor(target).slice(0, 120),
@@ -9709,6 +11661,14 @@ async function inspectElement(wc, selector, limit = 8) {
9709
11661
  text: text(region.textContent)?.slice(0, 400),
9710
11662
  },
9711
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
+ })),
9712
11672
  };
9713
11673
  })()
9714
11674
  `,
@@ -9741,10 +11701,25 @@ async function inspectElement(wc, selector, limit = 8) {
9741
11701
  lines.push("Nearby controls:");
9742
11702
  for (const item of result.nearby) {
9743
11703
  const hrefSuffix = item.href ? ` -> ${item.href}` : "";
11704
+ const indexPrefix = typeof item.index === "number" ? `[#${item.index}] ` : "";
11705
+ lines.push(
11706
+ `- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
11707
+ );
11708
+ }
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}] ` : "";
9744
11716
  lines.push(
9745
- `- ${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}`
11717
+ `- ${indexPrefix}${item.label} [${item.type}] selector=${item.selector}${hrefSuffix}${sourceSuffix}`
9746
11718
  );
9747
11719
  }
11720
+ lines.push(
11721
+ "When an index is available, prefer click(index=N) over selector-based clicks because it is more stable."
11722
+ );
9748
11723
  }
9749
11724
  return lines.join("\n");
9750
11725
  }
@@ -9833,6 +11808,11 @@ const ADD_TO_CART_PATTERNS = [
9833
11808
  ];
9834
11809
  const recentCartClicks = /* @__PURE__ */ new Map();
9835
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;
9836
11816
  function isAddToCartText(text) {
9837
11817
  const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
9838
11818
  return ADD_TO_CART_PATTERNS.some((p) => normalized.includes(p));
@@ -9852,20 +11832,115 @@ function isDuplicateCartClick(url, text) {
9852
11832
  recentCartClicks.delete(url);
9853
11833
  return false;
9854
11834
  }
9855
- return isAddToCartText(text);
11835
+ return isAddToCartText(text);
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}`;
9856
11925
  }
9857
11926
  async function clickResolvedSelector$1(wc, selector) {
9858
11927
  if (selector.startsWith("__vessel_idx:")) {
9859
11928
  const idx = Number(selector.slice("__vessel_idx:".length));
9860
11929
  const beforeUrl2 = wc.getURL();
11930
+ let idxCartMatch = false;
9861
11931
  const idxLabel = await executePageScript(
9862
11932
  wc,
9863
11933
  `window.__vessel?.getElementText?.(${idx}) || ""`,
9864
11934
  { label: "shadow element text" }
9865
11935
  );
9866
- if (typeof idxLabel === "string" && isAddToCartText(idxLabel) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
11936
+ if (typeof idxLabel === "string" && (idxCartMatch = isAddToCartText(idxLabel)) && isDuplicateCartClick(beforeUrl2, idxLabel)) {
9867
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).`;
9868
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
+ }
9869
11944
  const result = await executePageScript(
9870
11945
  wc,
9871
11946
  `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`,
@@ -9875,18 +11950,40 @@ async function clickResolvedSelector$1(wc, selector) {
9875
11950
  );
9876
11951
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
9877
11952
  if (typeof result === "string" && result.startsWith("Error")) return result;
9878
- if (typeof idxLabel === "string" && isAddToCartText(idxLabel)) {
11953
+ if (idxCartMatch) {
9879
11954
  recordCartClick(beforeUrl2, idxLabel);
9880
11955
  }
9881
11956
  await waitForPotentialNavigation$1(wc, beforeUrl2);
9882
11957
  const afterUrl2 = wc.getURL();
9883
11958
  if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
9884
- 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
+ }
9885
11980
  return idxOverlay ? `${result}
9886
- ${idxOverlay}` : result;
11981
+ ${idxOverlay}` : `${result}
11982
+ Note: Page did not change after click.`;
9887
11983
  }
9888
11984
  if (selector.includes(" >>> ")) {
9889
11985
  const beforeUrl2 = wc.getURL();
11986
+ let shadowCartMatch = false;
9890
11987
  const shadowLabel = await executePageScript(
9891
11988
  wc,
9892
11989
  `(function() {
@@ -9895,17 +11992,24 @@ ${idxOverlay}` : result;
9895
11992
  })()`,
9896
11993
  { label: "shadow element text" }
9897
11994
  );
9898
- if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
11995
+ if (typeof shadowLabel === "string" && (shadowCartMatch = isAddToCartText(shadowLabel)) && isDuplicateCartClick(beforeUrl2, shadowLabel)) {
9899
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).`;
9900
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
+ }
9901
12003
  const result = await executePageScript(
9902
12004
  wc,
9903
12005
  `
9904
12006
  (function() {
9905
12007
  var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
9906
- 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.";
9907
12009
  if (el instanceof HTMLElement) { el.focus(); el.click(); }
9908
- 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 : "");
9909
12013
  })()
9910
12014
  `,
9911
12015
  {
@@ -9914,15 +12018,36 @@ ${idxOverlay}` : result;
9914
12018
  );
9915
12019
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
9916
12020
  if (typeof result === "string" && result.startsWith("Error")) return result;
9917
- if (typeof shadowLabel === "string" && isAddToCartText(shadowLabel)) {
12021
+ if (shadowCartMatch) {
9918
12022
  recordCartClick(beforeUrl2, shadowLabel);
9919
12023
  }
9920
12024
  await waitForPotentialNavigation$1(wc, beforeUrl2);
9921
12025
  const afterUrl2 = wc.getURL();
9922
12026
  if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
9923
- 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
+ }
9924
12048
  return shadowOverlay ? `${result}
9925
- ${shadowOverlay}` : result;
12049
+ ${shadowOverlay}` : `${result}
12050
+ Note: Page did not change after click.`;
9926
12051
  }
9927
12052
  const beforeUrl = wc.getURL();
9928
12053
  const elInfo = await describeElementForClick$1(wc, selector);
@@ -9945,10 +12070,16 @@ Click one of these dialog actions instead.`;
9945
12070
  return formatDeadLinkMessage(elInfo.text, validation);
9946
12071
  }
9947
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
+ }
9948
12078
  if (cartMatch) {
9949
12079
  recordCartClick(beforeUrl, elInfo.text);
9950
12080
  }
9951
- 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}`;
9952
12083
  const clickResult = await clickElement$1(wc, selector);
9953
12084
  if (clickResult.startsWith("Error:")) return clickResult;
9954
12085
  await waitForPotentialNavigation$1(wc, beforeUrl);
@@ -9958,15 +12089,30 @@ Click one of these dialog actions instead.`;
9958
12089
  }
9959
12090
  const overlayHint = await detectPostClickOverlay$1(wc);
9960
12091
  if (overlayHint) {
9961
- const dialogActions = cartMatch ? await getCartDialogActions$1(wc) : null;
9962
- const actionsSuffix = dialogActions ? `
9963
- ${dialogActions}
9964
- 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
+ }
9965
12099
  return `${clickText} (${clickResult})
9966
- ${overlayHint}${actionsSuffix}`;
12100
+ ${overlayHint}`;
9967
12101
  }
9968
12102
  if (cartMatch) {
9969
- 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
+ )}`;
9970
12116
  }
9971
12117
  const activationResult = await activateElement$1(wc, selector);
9972
12118
  if (!activationResult.startsWith("Error:")) {
@@ -9981,7 +12127,65 @@ ${overlayHint}${actionsSuffix}`;
9981
12127
  return `${clickText} (${clickResult})
9982
12128
  ${postActivationOverlayHint}`;
9983
12129
  }
9984
- 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;
9985
12189
  }
9986
12190
  async function getCartDialogActions$1(wc) {
9987
12191
  const result = await executePageScript(
@@ -10787,6 +12991,24 @@ async function resolveSelector$1(wc, index, selector) {
10787
12991
  if (extractedSelector) return extractedSelector;
10788
12992
  return null;
10789
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
+ }
10790
13012
  function normalizeFieldToken(value) {
10791
13013
  return typeof value === "string" ? value.trim() : "";
10792
13014
  }
@@ -12094,6 +14316,9 @@ async function searchPage(wc, args) {
12094
14316
  if (buttonLikePatterns.some((p) => queryLower.includes(p))) {
12095
14317
  return `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`;
12096
14318
  }
14319
+ if (looksLikeCurrentSiteNameQuery(query, wc.getURL(), wc.getTitle() || "")) {
14320
+ return `Error: "${query}" looks like the current site's name, not a product query. You are already on ${wc.getURL()}. Open a section like staff picks/new releases or search for actual book titles, authors, or genres instead.`;
14321
+ }
12097
14322
  if (typeof args.selector !== "string") {
12098
14323
  const shortcut = buildSearchShortcut(wc.getURL(), query);
12099
14324
  if (shortcut) {
@@ -12104,7 +14329,7 @@ async function searchPage(wc, args) {
12104
14329
  const afterUrl2 = wc.getURL();
12105
14330
  const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
12106
14331
  const destination = shortcut.section ? ` ${shortcut.section}` : "";
12107
- return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}`;
14332
+ return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}${await getPostSearchSummary(wc)}`;
12108
14333
  }
12109
14334
  }
12110
14335
  const searchInfo = await locateSearchTarget(
@@ -12133,7 +14358,7 @@ async function searchPage(wc, args) {
12133
14358
  await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
12134
14359
  let afterUrl = wc.getURL();
12135
14360
  if (afterUrl !== beforeUrl) {
12136
- return `Searched "${query}" → ${afterUrl}`;
14361
+ return `Searched "${query}" → ${afterUrl}${await getPostSearchSummary(wc)}`;
12137
14362
  }
12138
14363
  if (searchInfo.submitSelector) {
12139
14364
  const clickResult = await clickElementBySelector(wc, searchInfo.submitSelector);
@@ -12141,11 +14366,11 @@ async function searchPage(wc, args) {
12141
14366
  await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
12142
14367
  afterUrl = wc.getURL();
12143
14368
  if (afterUrl !== beforeUrl) {
12144
- return `Searched "${query}" (via search button) → ${afterUrl}`;
14369
+ return `Searched "${query}" (via search button) → ${afterUrl}${await getPostSearchSummary(wc)}`;
12145
14370
  }
12146
14371
  }
12147
14372
  }
12148
- return `Searched "${query}" (same page — results may have loaded dynamically; inspect with read_page(mode="results_only") or read_page(mode="visible_only") before navigating directly elsewhere)`;
14373
+ return `Searched "${query}" (same page — results may have loaded dynamically)${await getPostSearchSummary(wc)}`;
12149
14374
  }
12150
14375
  async function pressKey$1(wc, args) {
12151
14376
  const key = typeof args.key === "string" ? args.key.trim() : "";
@@ -12234,8 +14459,49 @@ async function getPostActionState$1(ctx, name) {
12234
14459
  if (wc.isLoading()) {
12235
14460
  await waitForLoad$1(wc);
12236
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
+ }
12237
14484
  return `
12238
- [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
+ }
12239
14505
  }
12240
14506
  if (interactActions.includes(name)) {
12241
14507
  return `
@@ -12307,6 +14573,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
12307
14573
  "wait_for_navigation"
12308
14574
  ]);
12309
14575
  async function executeAction(name, args, ctx) {
14576
+ name = normalizeToolAlias(name);
12310
14577
  if (!KNOWN_TOOLS.has(name)) {
12311
14578
  for (const known of KNOWN_TOOLS) {
12312
14579
  if (name.startsWith(known) && name.length > known.length) {
@@ -12423,6 +14690,19 @@ async function executeAction(name, args, ctx) {
12423
14690
  }
12424
14691
  case "navigate": {
12425
14692
  if (!wc || !tabId) return "Error: No active tab";
14693
+ const taskGoal = ctx.runtime.getState().taskTracker?.goal;
14694
+ if (taskGoal && typeof args.url === "string") {
14695
+ const domainDrift = shouldBlockOffGoalDomainNavigation(
14696
+ taskGoal,
14697
+ args.url
14698
+ );
14699
+ if (domainDrift) {
14700
+ return `Navigation blocked: ${args.url} drifts away from the requested site ${domainDrift.requestedDomain}. Stay on the requested domain and continue the original task there.`;
14701
+ }
14702
+ }
14703
+ if (typeof args.url === "string" && !args.postBody && isRedundantNavigateTarget(wc.getURL(), args.url)) {
14704
+ return `Already on ${wc.getURL()}. Do not navigate to the same URL again. Use click, inspect_element, read_page, or search for actual book terms instead.`;
14705
+ }
12426
14706
  const navValidation = await validateLinkDestination(args.url);
12427
14707
  if (navValidation.status === "dead") {
12428
14708
  return `Navigation blocked: ${args.url} returned ${navValidation.detail || "dead link"}. Try a different URL or go back and choose another link.`;
@@ -12462,14 +14742,48 @@ async function executeAction(name, args, ctx) {
12462
14742
  }
12463
14743
  case "click": {
12464
14744
  if (!wc) return "Error: No active tab";
12465
- 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);
12466
- 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
+ }
12467
14767
  return clickResolvedSelector$1(wc, selector);
12468
14768
  }
12469
14769
  case "inspect_element": {
12470
14770
  if (!wc) return "Error: No active tab";
12471
- const selector = await resolveSelector$1(wc, args.index, args.selector);
12472
- 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
+ }
12473
14787
  return inspectElement(
12474
14788
  wc,
12475
14789
  selector,
@@ -12618,7 +14932,8 @@ async function executeAction(name, args, ctx) {
12618
14932
  const livePrefix = liveSelectionSection ? `${liveSelectionSection}
12619
14933
 
12620
14934
  ` : "";
12621
- 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;
12622
14937
  if (requestedMode === "debug" || requestedMode === "full") {
12623
14938
  const structured = buildStructuredContext(content);
12624
14939
  const truncated = content.content.length > 2e4 ? content.content.slice(0, 2e4) + "\n[Content truncated...]" : content.content;
@@ -12630,7 +14945,7 @@ ${structured}
12630
14945
 
12631
14946
  ${truncated}`;
12632
14947
  }
12633
- const scoped = buildScopedContext(content, requestedMode);
14948
+ const scoped = ctx.toolProfile === "compact" ? buildCompactScopedContext(content, requestedMode) : buildScopedContext(content, requestedMode);
12634
14949
  return [
12635
14950
  livePrefix ? livePrefix.trimEnd() : "",
12636
14951
  `[read_page mode=${requestedMode}]`,
@@ -13307,9 +15622,20 @@ ${JSON.stringify(tableJson, null, 2)}`;
13307
15622
  }
13308
15623
  case "scroll_to_element": {
13309
15624
  if (!wc) return "Error: No active tab";
13310
- const sel = await resolveSelector$1(wc, args.index, args.selector);
13311
- if (!sel)
13312
- 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
+ }
13313
15639
  const block = args.position === "top" ? "start" : args.position === "bottom" ? "end" : "center";
13314
15640
  if (sel.startsWith("__vessel_idx:")) {
13315
15641
  const idx = Number(sel.slice("__vessel_idx:".length));
@@ -13347,8 +15673,36 @@ ${JSON.stringify(tableJson, null, 2)}`;
13347
15673
  }
13348
15674
  }
13349
15675
  });
15676
+ const formattedResult = ctx.toolProfile === "compact" ? formatCompactToolResult(name, result) : result;
13350
15677
  const flowCtx = ctx.runtime.getFlowContext();
13351
- 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;
13352
15706
  }
13353
15707
  async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history) {
13354
15708
  const lowerQuery = query.toLowerCase().trim();
@@ -13358,81 +15712,49 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
13358
15712
  const pageContent = await extractContent(activeWebContents);
13359
15713
  const pageType = detectPageType(pageContent);
13360
15714
  const defaultReadMode = chooseAgentReadMode(pageContent);
13361
- const structuredContext = buildScopedContext(
15715
+ if (provider.agentToolProfile === "compact") {
15716
+ runtime2.ensureTaskTracker(query, pageContent.url || activeWebContents.getURL());
15717
+ } else {
15718
+ runtime2.clearTaskTracker();
15719
+ }
15720
+ const structuredContext = provider.agentToolProfile === "compact" ? buildCompactScopedContext(
13362
15721
  pageContent,
13363
- defaultReadMode
13364
- );
15722
+ defaultReadMode,
15723
+ pageType
15724
+ ) : buildScopedContext(pageContent, defaultReadMode);
13365
15725
  const runtimeState = runtime2.getState();
13366
15726
  const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
15727
+ const taskTrackerContext = runtime2.getTaskTrackerContext();
13367
15728
  const activeTabTitle = pageContent.title || "(untitled)";
13368
15729
  const activeTabUrl = pageContent.url || activeWebContents.getURL();
13369
15730
  const allTabs = tabManager.getAllStates();
13370
15731
  const activeTabId = tabManager.getActiveTabId();
13371
15732
  const tabSummary = allTabs.length > 1 ? `
13372
15733
  All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.title || "New Tab"} (${t.url})`).join(" | ")}` : "";
13373
- const systemPrompt = `You are Vessel, an AI agent embedded in a web browser. You can see the current page and interact with it using tools.
13374
-
13375
- THE USER IS CURRENTLY LOOKING AT:
13376
- Title: ${activeTabTitle}
13377
- URL: ${activeTabUrl}${tabSummary}
13378
-
13379
- When the user says "this page", "this article", "this site", or asks about what they're viewing, they mean the page above. The context below is from that page — answer directly without needing to call read_page or current_tab first.
13380
-
13381
- Current page context:
13382
- This brief is intentionally minimal and filtered for speed. It omits most page text and low-value chrome unless you explicitly ask for more.
13383
- Default brief mode: ${defaultReadMode}
13384
- Detected page type: ${pageType}
13385
-
13386
- ${structuredContext}
13387
-
13388
- Supervisor state:
13389
- - paused: ${runtimeState.supervisor.paused ? "yes" : "no"}
13390
- - approval mode: ${runtimeState.supervisor.approvalMode}
13391
- - pending approvals: ${runtimeState.supervisor.pendingApprovals.length}
13392
-
13393
- Recent checkpoints:
13394
- ${recentCheckpoints || "- none"}
13395
-
13396
- Instructions:
13397
- - You can see the page the user is viewing. The content above is from the page.
13398
- - The structured page context always refers to the tab currently visible to the human unless a later tool call changes tabs.
13399
- - Use tools to interact with the page when asked to do something (navigate, click, type, select options, submit forms, press keys, scroll).
13400
- - Only say you completed an action after the corresponding tool succeeds. If no tool supports the request, say so plainly.
13401
- - Use current_tab when you only need to know what the human is currently looking at. Use list_tabs before switching context across multiple tabs.
13402
- - Create a checkpoint before risky multi-step flows or before leaving an important state.
13403
- - Use save_session after completing a login flow you may need again later, and load_session to resume that authenticated state in future runs.
13404
- - Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.
13405
- - After navigating to a new site, DO NOT call read_page immediately. Instead, act on what you already know: use the search tool to search the site, type_text to enter queries in search bars, or click on known navigation patterns. You know what major sites look like — use that knowledge. Only call read_page if you're genuinely stuck and need to discover unfamiliar page structure.
13406
- - On retail and marketplace sites (like Newegg, Amazon, Walmart, Etsy, eBay), prefer the site's visible search box, filters, and result pages over direct product URLs. Only navigate directly to a product page if the user gave you that URL or the site's own search UI is clearly unavailable after a reasonable attempt.
13407
- - The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
13408
- - When you only need detail on one product/result/card/form section, use inspect_element instead of reading the page.
13409
- - Escalate page reads progressively: read_page(mode="glance") for a fast viewport snapshot on heavy/slow pages, then read_page(mode="visible_only"), read_page(mode="results_only"), read_page(mode="forms_only"), read_page(mode="summary"), or read_page(mode="text_only") depending on what you need.
13410
- - Use read_page(mode="glance") when a page is slow to load or extraction times out — it shows what's on screen (headings, links, buttons, inputs) without waiting for heavy JS. It's what a human would see by just looking at the page.
13411
- - Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
13412
- - If read_page returns empty or times out, do NOT retry with the same mode. Switch to read_page(mode="glance") or use screenshot to see the page visually.
13413
- - Use screenshot when you need to see exactly what the user sees — visual layout, rendered content, images, or when text extraction is failing. The screenshot returns the actual rendered page image for visual analysis. It works even when the JS thread is completely blocked.
13414
- - VIEWPORT SYNC: Treat scrolling as a real, user-visible browser action. If you say you are going to scroll, call scroll or scroll_to_element so the human sees the page move too.
13415
- - read_page inspects the page without moving the human-visible viewport. Do not describe read_page as scrolling. If you want more context without changing the user's view, say you're reading the page; if you want the user to follow along lower on the page, actually scroll first.
13416
- - After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. Do not jump straight to read_page(mode="debug").
13417
- - If the user says they highlighted or selected text, use read_page before falling back to screenshots because it includes active selection and visible unsaved highlights.
13418
- - If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.
13419
- - For broad discovery tasks, prefer direct sources, official sites, venue directories, and site-specific search over generic search engines, which often rate-limit automated browser traffic.
13420
- - If the page context reports a rate limit, human verification, or access warning, stop using that page and switch to a different source.
13421
- - Reference interactive elements by their index number (shown as [#N] in the listings above).
13422
- - Be concise. Explain what you're doing as you go.
13423
- - For simple questions about the page, just answer directly without using tools.
13424
- - VISUAL AWARENESS: The human is watching the browser alongside this chat. Highlights are your pointing finger — they show the user exactly what you're looking at on the page. Use them proactively: highlight key findings, important elements, errors, or anything you're referencing. Don't wait to be asked. If you mention something specific on the page, highlight it. Colors: yellow (default/attention), red (errors/warnings), green (success/good), blue (info/neutral), purple (important/notable), orange (caution). Clear highlights when moving to a new topic or page.
13425
- - After completing a task or answering a question, offer 1-2 brief, natural follow-up suggestions that make sense in context (e.g. "Want me to highlight any of these?" or "I can save these to a bookmark folder if you'd like"). Keep suggestions short and conversational — don't list every possible action.
13426
- - Call one tool at a time unless you are certain your provider supports parallel tool calls. Sequential calls are more reliable.
13427
- - MINIMIZE TOOL CALLS: Every tool call takes time and costs a round trip. Be efficient. Don't use flow_start/flow_advance for simple multi-step tasks — just do the work. Don't call read_page after navigating — use search or type_text directly. Don't retry failed tools with slight variations — if search fails, go straight to type_text + press_key Enter, don't try read_page in between. The fastest path is usually: navigate → search → wait_for or read_page(mode="results_only") → click.
13428
- - ACT, DON'T HEDGE: You have a full browser — you can navigate to any website, see live content, search, click, add to cart, fill forms, and interact with real pages in real time. Never claim you "don't have access" to a website's inventory, pricing, or content. If the user asks you to go somewhere and do something, start doing it immediately. Don't ask for permission to do what the user just asked you to do — that's redundant and frustrating. Jump straight into action.
13429
- - USE YOUR KNOWLEDGE: You have broad, practical knowledge about technology, products, cooking, travel, finance, and countless other domains. When the user asks for recommendations, GIVE them — don't deflect to Reddit, YouTubers, or other sources. You know enough to recommend PC parts, suggest restaurants, pick a good laptop, or advise on most consumer decisions. Make a clear recommendation, explain your reasoning briefly, and then execute. If there's genuine ambiguity (e.g. AMD vs Intel is preference-dependent), state your pick and why, then ask only the questions that would actually change your recommendation. Never refuse a recommendation by claiming you're "not an expert" — the user chose to ask you, so help them.
13430
- - NEVER USE EMOJIS unless the user uses them first.`;
13431
- const actionCtx = { tabManager, runtime: runtime2 };
15734
+ const systemPrompt = buildAgentSystemPrompt({
15735
+ profile: provider.agentToolProfile,
15736
+ activeTabTitle,
15737
+ activeTabUrl,
15738
+ tabSummary,
15739
+ defaultReadMode,
15740
+ pageType,
15741
+ structuredContext,
15742
+ supervisorPaused: runtimeState.supervisor.paused,
15743
+ approvalMode: runtimeState.supervisor.approvalMode,
15744
+ pendingApprovals: runtimeState.supervisor.pendingApprovals.length,
15745
+ recentCheckpoints: recentCheckpoints || "- none",
15746
+ taskTrackerContext: taskTrackerContext || "- none"
15747
+ });
15748
+ const actionCtx = {
15749
+ tabManager,
15750
+ runtime: runtime2,
15751
+ toolProfile: provider.agentToolProfile
15752
+ };
13432
15753
  const contextualTools = pruneToolsForContext(
13433
15754
  AGENT_TOOLS,
13434
15755
  pageType,
13435
- query
15756
+ query,
15757
+ { profile: provider.agentToolProfile }
13436
15758
  );
13437
15759
  const trace = createTraceSession(query, activeTabUrl, activeTabTitle);
13438
15760
  let accumulatedResponse = "";
@@ -13450,6 +15772,14 @@ Instructions:
13450
15772
  let isError = false;
13451
15773
  try {
13452
15774
  output = await executeAction(name, args, actionCtx);
15775
+ if (provider.agentToolProfile === "compact") {
15776
+ runtime2.updateTaskTracker(name, output);
15777
+ const trackerCtx = runtime2.getTaskTrackerContext();
15778
+ if (trackerCtx) {
15779
+ output = `${output}
15780
+ ${trackerCtx}`;
15781
+ }
15782
+ }
13453
15783
  } catch (err) {
13454
15784
  isError = true;
13455
15785
  output = err instanceof Error ? err.message : String(err);
@@ -14719,8 +17049,24 @@ async function clickResolvedSelector(wc, selector) {
14719
17049
  await waitForPotentialNavigation(wc, beforeUrl2);
14720
17050
  const afterUrl2 = wc.getURL();
14721
17051
  if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
14722
- const overlayHint2 = await detectPostClickOverlay(wc);
14723
- 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
+ }
14724
17070
  const dialogActions = typeof idxLabel === "string" && isAddToCartText(idxLabel) ? await getCartDialogActions(wc) : null;
14725
17071
  const actionsSuffix = dialogActions ? `
14726
17072
  ${dialogActions}
@@ -14742,9 +17088,11 @@ ${overlayHint2}${actionsSuffix}`;
14742
17088
  const result = await wc.executeJavaScript(`
14743
17089
  (function() {
14744
17090
  var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
14745
- 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.";
14746
17092
  if (el instanceof HTMLElement) { el.focus(); el.click(); }
14747
- 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 : "");
14748
17096
  })()
14749
17097
  `);
14750
17098
  if (typeof result === "string" && result.startsWith("Error")) return result;
@@ -14754,14 +17102,30 @@ ${overlayHint2}${actionsSuffix}`;
14754
17102
  await waitForPotentialNavigation(wc, beforeUrl2);
14755
17103
  const afterUrl2 = wc.getURL();
14756
17104
  if (afterUrl2 !== beforeUrl2) return `${result} -> ${afterUrl2}`;
14757
- const overlayHint2 = await detectPostClickOverlay(wc);
14758
- if (!overlayHint2) return result;
14759
- const dialogActions = typeof shadowLabel === "string" && isAddToCartText(shadowLabel) ? await getCartDialogActions(wc) : null;
14760
- const actionsSuffix = dialogActions ? `
14761
- ${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}
14762
17126
  Click one of these dialog actions. Do NOT click any other element.` : "";
14763
17127
  return `${result}
14764
- ${overlayHint2}${actionsSuffix}`;
17128
+ ${overlayHint2}${actionsSuffix2}`;
14765
17129
  }
14766
17130
  const beforeUrl = wc.getURL();
14767
17131
  const elInfo = await describeElementForClick(wc, selector);
@@ -14805,6 +17169,16 @@ Click one of these dialog actions. Do NOT click any other element.` : "";
14805
17169
  ${overlayHint}${actionsSuffix}`;
14806
17170
  }
14807
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
+ }
14808
17182
  return `${clickText} (${clickResult})`;
14809
17183
  }
14810
17184
  const activationResult = await activateElement(wc, selector);
@@ -19730,8 +22104,9 @@ function registerIpcHandlers(windowState, runtime2) {
19730
22104
  if (!config || typeof config !== "object" || !("id" in config)) {
19731
22105
  return { ok: false, models: [], error: "Invalid provider configuration" };
19732
22106
  }
19733
- const models = await fetchProviderModels(config);
19734
- return { ok: true, models };
22107
+ return await fetchProviderModels(
22108
+ config
22109
+ );
19735
22110
  } catch (err) {
19736
22111
  return { ok: false, models: [], error: err instanceof Error ? err.message : "Unknown error" };
19737
22112
  }
@@ -20140,6 +22515,353 @@ function registerIpcHandlers(windowState, runtime2) {
20140
22515
  });
20141
22516
  registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
20142
22517
  }
22518
+ function makeStep(label, status = "pending") {
22519
+ return { label, status };
22520
+ }
22521
+ function extractRequestedCount(goal) {
22522
+ const digitMatch = goal.match(/\b(\d+)\b/);
22523
+ if (digitMatch) return Number(digitMatch[1]);
22524
+ const wordMap = {
22525
+ one: 1,
22526
+ two: 2,
22527
+ three: 3,
22528
+ four: 4,
22529
+ five: 5,
22530
+ six: 6,
22531
+ seven: 7,
22532
+ eight: 8,
22533
+ nine: 9,
22534
+ ten: 10
22535
+ };
22536
+ for (const [word, count] of Object.entries(wordMap)) {
22537
+ if (new RegExp(`\\b${word}\\b`, "i").test(goal)) return count;
22538
+ }
22539
+ return null;
22540
+ }
22541
+ function buildInitialSteps(goal) {
22542
+ const lowered = goal.toLowerCase();
22543
+ const requestedCount = extractRequestedCount(goal);
22544
+ const itemLabel = /\b(book|books)\b/.test(lowered) ? "books" : "items";
22545
+ const countLabel = requestedCount ? `${requestedCount} ${itemLabel}` : itemLabel;
22546
+ const steps = [
22547
+ makeStep("Navigate to the requested site", "active")
22548
+ ];
22549
+ if (/\b(find|browse|look|discover|select|recommend|interesting)\b/.test(lowered)) {
22550
+ steps.push(makeStep(`Browse or search for relevant ${itemLabel}`));
22551
+ }
22552
+ steps.push(makeStep(`Pick the requested ${countLabel}`));
22553
+ if (/\b(cart|checkout|bag)\b/.test(lowered)) {
22554
+ steps.push(makeStep(`Add the chosen ${itemLabel} to the cart`));
22555
+ }
22556
+ if (/\b(explain|reason|why)\b/.test(lowered)) {
22557
+ steps.push(makeStep("Explain the recommendations"));
22558
+ }
22559
+ return steps;
22560
+ }
22561
+ function setActiveStep(steps, currentStepIndex) {
22562
+ return steps.map((step, index) => {
22563
+ if (step.status === "done" || step.status === "failed") return step;
22564
+ return {
22565
+ ...step,
22566
+ status: index === currentStepIndex ? "active" : "pending"
22567
+ };
22568
+ });
22569
+ }
22570
+ function completeStep(state2, detail) {
22571
+ const steps = state2.steps.map((step) => ({ ...step }));
22572
+ const current = steps[state2.currentStepIndex];
22573
+ if (current) {
22574
+ current.status = "done";
22575
+ current.detail = detail || current.detail;
22576
+ }
22577
+ const nextIndex = Math.min(state2.currentStepIndex + 1, steps.length - 1);
22578
+ const normalizedSteps = setActiveStep(
22579
+ steps,
22580
+ current ? nextIndex : state2.currentStepIndex
22581
+ );
22582
+ return {
22583
+ ...state2,
22584
+ steps: normalizedSteps,
22585
+ currentStepIndex: current ? nextIndex : state2.currentStepIndex,
22586
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22587
+ };
22588
+ }
22589
+ function setNextHint(state2, nextHint) {
22590
+ return {
22591
+ ...state2,
22592
+ nextHint,
22593
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22594
+ };
22595
+ }
22596
+ function normalizeResult(result) {
22597
+ return result.toLowerCase();
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
+ }
22675
+ function createTaskTracker(goal, startUrl) {
22676
+ const now = (/* @__PURE__ */ new Date()).toISOString();
22677
+ return {
22678
+ goal,
22679
+ startedAt: now,
22680
+ updatedAt: now,
22681
+ startUrl,
22682
+ currentStepIndex: 0,
22683
+ steps: buildInitialSteps(goal),
22684
+ lastAction: void 0,
22685
+ nextHint: "Use the site's search box or a strong curated section immediately. Avoid rereading the homepage unless search or navigation is hidden."
22686
+ };
22687
+ }
22688
+ function updateTaskTracker(state2, actionName, result) {
22689
+ const requestedCount = state2.requestedCount ?? extractRequestedCount(state2.goal) ?? null;
22690
+ let cartCount = state2.cartCount ?? 0;
22691
+ const cartVisible = state2.cartVisible || looksLikeCartPage(result);
22692
+ let nextState = {
22693
+ ...state2,
22694
+ lastAction: actionName,
22695
+ requestedCount,
22696
+ cartCount,
22697
+ cartVisible,
22698
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22699
+ };
22700
+ const currentLabel = nextState.steps[nextState.currentStepIndex]?.label.toLowerCase() ?? "";
22701
+ if (actionName === "navigate") {
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
+ }
22738
+ }
22739
+ const isDiscoveryAction = [
22740
+ "read_page",
22741
+ "search",
22742
+ "click",
22743
+ "inspect_element",
22744
+ "scroll"
22745
+ ].includes(actionName);
22746
+ if (isDiscoveryAction && /browse or search/.test(currentLabel)) {
22747
+ const surfacedResults = countSurfacedResults(result);
22748
+ nextState = completeStep(nextState, "Found a starting point on the site.");
22749
+ return setNextHint(
22750
+ nextState,
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."
22752
+ );
22753
+ }
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
+ }
22814
+ return setNextHint(
22815
+ nextState,
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."
22817
+ );
22818
+ }
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);
22833
+ return setNextHint(
22834
+ nextState,
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."
22836
+ );
22837
+ }
22838
+ if (/explain the recommendations/.test(currentLabel)) {
22839
+ return setNextHint(
22840
+ nextState,
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."
22842
+ );
22843
+ }
22844
+ return nextState;
22845
+ }
22846
+ function formatTaskTracker(state2) {
22847
+ if (!state2) return "";
22848
+ const completed = state2.steps.filter((step) => step.status === "done").map((step) => step.label);
22849
+ const current = state2.steps[state2.currentStepIndex]?.label ?? "Task in progress";
22850
+ const remaining = state2.steps.filter((step, index) => step.status !== "done" && index !== state2.currentStepIndex).map((step) => step.label);
22851
+ const lines = [
22852
+ "--- Task Tracker ---",
22853
+ `Goal: ${state2.goal}`,
22854
+ `Completed: ${completed.length > 0 ? completed.join("; ") : "none yet"}`,
22855
+ `Current: ${current}`,
22856
+ `Remaining: ${remaining.length > 0 ? remaining.join("; ") : "wrap up the response"}`
22857
+ ];
22858
+ if (state2.nextHint) {
22859
+ lines.push(`Next: ${state2.nextHint}`);
22860
+ }
22861
+ return `
22862
+ ${lines.join("\n")}
22863
+ ---`;
22864
+ }
20143
22865
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
20144
22866
  const PERSIST_DEBOUNCE_MS = 500;
20145
22867
  function clone(value) {
@@ -20175,7 +22897,8 @@ function sanitizePersistence(persisted) {
20175
22897
  checkpoints: Array.isArray(persisted?.checkpoints) ? persisted.checkpoints.slice(-20) : [],
20176
22898
  transcript: [],
20177
22899
  mcpStatus: "stopped",
20178
- flowState: null
22900
+ flowState: null,
22901
+ taskTracker: null
20179
22902
  };
20180
22903
  }
20181
22904
  class AgentRuntime {
@@ -20294,6 +23017,32 @@ class AgentRuntime {
20294
23017
  this.emit();
20295
23018
  return this.getState();
20296
23019
  }
23020
+ ensureTaskTracker(goal, startUrl) {
23021
+ const trimmedGoal = goal.trim();
23022
+ if (this.state.taskTracker && this.state.taskTracker.goal.trim() === trimmedGoal) {
23023
+ return clone(this.state.taskTracker);
23024
+ }
23025
+ this.state.taskTracker = createTaskTracker(trimmedGoal, startUrl);
23026
+ this.emit();
23027
+ return clone(this.state.taskTracker);
23028
+ }
23029
+ updateTaskTracker(actionName, result) {
23030
+ if (!this.state.taskTracker) return null;
23031
+ this.state.taskTracker = updateTaskTracker(
23032
+ this.state.taskTracker,
23033
+ actionName,
23034
+ result
23035
+ );
23036
+ this.emit();
23037
+ return clone(this.state.taskTracker);
23038
+ }
23039
+ clearTaskTracker() {
23040
+ this.state.taskTracker = null;
23041
+ this.emit();
23042
+ }
23043
+ getTaskTrackerContext() {
23044
+ return formatTaskTracker(this.state.taskTracker);
23045
+ }
20297
23046
  // --- Speedee Flow State ---
20298
23047
  startFlow(goal, steps, startUrl) {
20299
23048
  const flow = {