@quanta-intellect/vessel-browser 0.1.9 → 0.1.11

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
@@ -6,13 +6,13 @@ const fs = require("fs");
6
6
  const crypto = require("crypto");
7
7
  const Anthropic = require("@anthropic-ai/sdk");
8
8
  const OpenAI = require("openai");
9
+ const zod = require("zod");
9
10
  const path$1 = require("node:path");
10
11
  const node_crypto = require("node:crypto");
11
12
  const http = require("node:http");
12
13
  const os = require("node:os");
13
14
  const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
14
15
  const streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
15
- const zod = require("zod");
16
16
  const MAX_CUSTOM_HISTORY = 50;
17
17
  class Tab {
18
18
  id;
@@ -62,6 +62,26 @@ class Tab {
62
62
  isReaderMode: false,
63
63
  adBlockingEnabled: options?.adBlockingEnabled ?? true
64
64
  };
65
+ this.view.webContents.on("before-input-event", (event, input) => {
66
+ if (!input.control && !input.meta) return;
67
+ const key = input.key.toLowerCase();
68
+ const wc = this.view.webContents;
69
+ if (input.type === "keyDown") {
70
+ if (key === "c") {
71
+ wc.copy();
72
+ event.preventDefault();
73
+ } else if (key === "v") {
74
+ wc.paste();
75
+ event.preventDefault();
76
+ } else if (key === "x") {
77
+ wc.cut();
78
+ event.preventDefault();
79
+ } else if (key === "a") {
80
+ wc.selectAll();
81
+ event.preventDefault();
82
+ }
83
+ }
84
+ });
65
85
  this.setupListeners();
66
86
  if (url) {
67
87
  this.lastCommittedUrl = url;
@@ -527,10 +547,12 @@ function resolveColor(color) {
527
547
  }
528
548
  const VESSEL_HIGHLIGHT_CSS = `
529
549
  .__vessel-highlight {
530
- outline: 3px solid #f0c636 !important;
531
- outline-offset: 2px !important;
532
- box-shadow: 0 0 12px rgba(240, 198, 54, 0.5) !important;
533
- transition: outline-color 0.3s, box-shadow 0.3s;
550
+ background: rgba(240, 198, 54, 0.3) !important;
551
+ outline: 2px solid rgba(240, 198, 54, 0.6) !important;
552
+ outline-offset: 1px !important;
553
+ border-radius: 2px !important;
554
+ box-shadow: 0 0 8px rgba(240, 198, 54, 0.3) !important;
555
+ transition: background 0.3s, outline-color 0.3s, box-shadow 0.3s;
534
556
  }
535
557
  .__vessel-highlight-text {
536
558
  background: rgba(240, 198, 54, 0.3) !important;
@@ -553,6 +575,11 @@ const VESSEL_HIGHLIGHT_CSS = `
553
575
  line-height: 1.3;
554
576
  overflow-wrap: break-word;
555
577
  box-shadow: 0 2px 6px rgba(0,0,0,0.3);
578
+ opacity: 0;
579
+ transition: opacity 0.15s ease-in-out;
580
+ }
581
+ .__vessel-highlight-label.visible {
582
+ opacity: 1;
556
583
  }
557
584
  `;
558
585
  async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, color) {
@@ -583,14 +610,14 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
583
610
  if (!label) return null;
584
611
  var anchor = label.__vesselAnchor;
585
612
  if (!anchor || !anchor.isConnected) {
586
- label.style.opacity = '0';
613
+ label.classList.remove('visible');
587
614
  return null;
588
615
  }
589
616
  var rect = anchor.getBoundingClientRect();
590
617
  var viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
591
618
  var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
592
619
  if (!viewportWidth || !viewportHeight || rect.width === 0 && rect.height === 0) {
593
- label.style.opacity = '0';
620
+ label.classList.remove('visible');
594
621
  return null;
595
622
  }
596
623
  var margin = 8;
@@ -606,7 +633,7 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
606
633
  var visible = rect.bottom >= 0 && rect.top <= viewportHeight && rect.right >= 0 && rect.left <= viewportWidth;
607
634
  label.style.top = top + 'px';
608
635
  label.style.left = left + 'px';
609
- label.style.opacity = visible ? '1' : '0';
636
+ if (!visible) label.classList.remove('visible');
610
637
  return {
611
638
  left: left,
612
639
  top: top,
@@ -655,9 +682,14 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
655
682
  (function() {
656
683
  var el = document.querySelector(${JSON.stringify(resolvedSelector)});
657
684
  if (!el) return 'Element not found';
685
+ // Remove any existing badge on this element to avoid duplicates
686
+ document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) {
687
+ if (b.__vesselAnchor === el) b.remove();
688
+ });
658
689
  el.classList.add('__vessel-highlight');
690
+ el.style.setProperty('background', ${JSON.stringify(c.bg)}, 'important');
659
691
  el.style.setProperty('outline-color', ${JSON.stringify(c.solid)}, 'important');
660
- el.style.setProperty('box-shadow', '0 0 12px ' + ${JSON.stringify(c.glow)}, 'important');
692
+ el.style.setProperty('box-shadow', '0 0 8px ' + ${JSON.stringify(c.glow)}, 'important');
661
693
  el.scrollIntoView({ behavior: 'smooth', block: 'center' });
662
694
  var label = ${JSON.stringify(label || "")};
663
695
  var badge = null;
@@ -671,6 +703,8 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
671
703
  badge.__vesselAnchor = el;
672
704
  document.body.appendChild(badge);
673
705
  window.__vesselHighlightLabelManager.positionAll();
706
+ el.addEventListener('mouseenter', function() { badge.classList.add('visible'); });
707
+ el.addEventListener('mouseleave', function() { badge.classList.remove('visible'); });
674
708
  }
675
709
  var duration = ${durationMs ?? 0};
676
710
  if (duration > 0) {
@@ -691,6 +725,10 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
691
725
  var bgColor = ${JSON.stringify(c.bg)};
692
726
  var labelBg = ${JSON.stringify(c.label)};
693
727
  var labelText = ${JSON.stringify(c.text)};
728
+ // Remove any existing badges whose text matches to avoid duplicates
729
+ document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) {
730
+ if (b.textContent === ${JSON.stringify(label || "")}) b.remove();
731
+ });
694
732
  var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};
695
733
  // Collect matching text nodes first, then wrap — avoids TreeWalker
696
734
  // seeing newly created nodes from surroundContents and re-matching.
@@ -747,6 +785,11 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
747
785
  badge.__vesselAnchor = firstMark;
748
786
  document.body.appendChild(badge);
749
787
  window.__vesselHighlightLabelManager.positionAll();
788
+ var marks = document.querySelectorAll('mark.__vessel-highlight-text[data-vessel-highlight]');
789
+ marks.forEach(function(m) {
790
+ m.addEventListener('mouseenter', function() { if (badge) { badge.__vesselAnchor = m; window.__vesselHighlightLabelManager.positionAll(); badge.classList.add('visible'); } });
791
+ m.addEventListener('mouseleave', function() { if (badge) badge.classList.remove('visible'); });
792
+ });
750
793
  }
751
794
  var duration = ${durationMs ?? 0};
752
795
  if (duration > 0) {
@@ -1836,7 +1879,8 @@ const defaults = {
1836
1879
  obsidianVaultPath: "",
1837
1880
  approvalMode: "confirm-dangerous",
1838
1881
  agentTranscriptMode: "summary",
1839
- chatProvider: null
1882
+ chatProvider: null,
1883
+ maxToolIterations: 200
1840
1884
  };
1841
1885
  let settings = null;
1842
1886
  let settingsIssues = [];
@@ -1902,6 +1946,28 @@ function setSetting(key, value) {
1902
1946
  saveSettings();
1903
1947
  return { ...settings };
1904
1948
  }
1949
+ function enableClipboardShortcuts(view) {
1950
+ view.webContents.on("before-input-event", (event, input) => {
1951
+ if (!input.control && !input.meta) return;
1952
+ const key = input.key.toLowerCase();
1953
+ const wc = view.webContents;
1954
+ if (input.type === "keyDown") {
1955
+ if (key === "c") {
1956
+ wc.copy();
1957
+ event.preventDefault();
1958
+ } else if (key === "v") {
1959
+ wc.paste();
1960
+ event.preventDefault();
1961
+ } else if (key === "x") {
1962
+ wc.cut();
1963
+ event.preventDefault();
1964
+ } else if (key === "a") {
1965
+ wc.selectAll();
1966
+ event.preventDefault();
1967
+ }
1968
+ }
1969
+ });
1970
+ }
1905
1971
  const CHROME_HEIGHT = 110;
1906
1972
  const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
1907
1973
  const MIN_DEVTOOLS_PANEL = 120;
@@ -1954,6 +2020,9 @@ function createMainWindow(onTabStateChange) {
1954
2020
  });
1955
2021
  devtoolsPanelView.setBackgroundColor("#00000000");
1956
2022
  mainWindow.contentView.addChildView(devtoolsPanelView);
2023
+ enableClipboardShortcuts(chromeView);
2024
+ enableClipboardShortcuts(sidebarView);
2025
+ enableClipboardShortcuts(devtoolsPanelView);
1957
2026
  const settings2 = loadSettings();
1958
2027
  const uiState = {
1959
2028
  sidebarOpen: false,
@@ -3712,7 +3781,7 @@ function setMcpHealth(update) {
3712
3781
  mcpStatusChangeListener?.(state$1.mcp.status);
3713
3782
  }
3714
3783
  }
3715
- const MAX_AGENT_ITERATIONS$1 = 15;
3784
+ const DEFAULT_MAX_ITERATIONS$1 = 200;
3716
3785
  class AnthropicProvider {
3717
3786
  client;
3718
3787
  model;
@@ -3760,7 +3829,10 @@ class AnthropicProvider {
3760
3829
  { role: "user", content: userMessage }
3761
3830
  ];
3762
3831
  try {
3763
- for (let i = 0; i < MAX_AGENT_ITERATIONS$1; i++) {
3832
+ const maxIterations = loadSettings().maxToolIterations || DEFAULT_MAX_ITERATIONS$1;
3833
+ let iterationsUsed = 0;
3834
+ for (let i = 0; i < maxIterations; i++) {
3835
+ iterationsUsed = i + 1;
3764
3836
  const stream = this.client.messages.stream(
3765
3837
  {
3766
3838
  model: this.model,
@@ -3821,14 +3893,15 @@ class AnthropicProvider {
3821
3893
  });
3822
3894
  }
3823
3895
  messages.push({ role: "assistant", content: assistantContent });
3824
- if (finalMessage.stop_reason !== "tool_use" || toolUseBlocks.length === 0) {
3896
+ if (toolUseBlocks.length === 0) {
3825
3897
  break;
3826
3898
  }
3827
3899
  const toolResults = [];
3828
3900
  for (const tb of toolUseBlocks) {
3829
3901
  const argSummary = tb.input.url || tb.input.text || tb.input.direction || "";
3830
3902
  onChunk(`
3831
- \`[${tb.name}${argSummary ? ": " + argSummary : ""}]\` `);
3903
+ <<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
3904
+ `);
3832
3905
  const result = await onToolCall(tb.name, tb.input);
3833
3906
  toolResults.push({
3834
3907
  type: "tool_result",
@@ -3838,6 +3911,11 @@ class AnthropicProvider {
3838
3911
  }
3839
3912
  messages.push({ role: "user", content: toolResults });
3840
3913
  }
3914
+ if (iterationsUsed >= maxIterations) {
3915
+ onChunk(`
3916
+
3917
+ [Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
3918
+ }
3841
3919
  } catch (err) {
3842
3920
  if (err.name !== "AbortError") {
3843
3921
  onChunk(`
@@ -3943,7 +4021,7 @@ const PROVIDERS = {
3943
4021
  apiKeyHint: "Any OpenAI-compatible API endpoint"
3944
4022
  }
3945
4023
  };
3946
- const MAX_AGENT_ITERATIONS = 15;
4024
+ const DEFAULT_MAX_ITERATIONS = 200;
3947
4025
  function toOpenAITools(tools) {
3948
4026
  return tools.map((t) => ({
3949
4027
  type: "function",
@@ -4009,7 +4087,10 @@ class OpenAICompatProvider {
4009
4087
  { role: "user", content: userMessage }
4010
4088
  ];
4011
4089
  try {
4012
- for (let i = 0; i < MAX_AGENT_ITERATIONS; i++) {
4090
+ const maxIterations = loadSettings().maxToolIterations || DEFAULT_MAX_ITERATIONS;
4091
+ let iterationsUsed = 0;
4092
+ for (let i = 0; i < maxIterations; i++) {
4093
+ iterationsUsed = i + 1;
4013
4094
  let textAccum = "";
4014
4095
  const toolCallAccums = {};
4015
4096
  let finishReason = null;
@@ -4066,7 +4147,8 @@ class OpenAICompatProvider {
4066
4147
  }
4067
4148
  const argSummary = args.url || args.text || args.direction || "";
4068
4149
  onChunk(`
4069
- \`[${tc.name}${argSummary ? ": " + argSummary : ""}]\` `);
4150
+ <<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
4151
+ `);
4070
4152
  const result = await onToolCall(tc.name, args);
4071
4153
  messages.push({
4072
4154
  role: "tool",
@@ -4075,6 +4157,11 @@ class OpenAICompatProvider {
4075
4157
  });
4076
4158
  }
4077
4159
  }
4160
+ if (iterationsUsed >= maxIterations) {
4161
+ onChunk(`
4162
+
4163
+ [Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
4164
+ }
4078
4165
  } catch (err) {
4079
4166
  if (err.name !== "AbortError") {
4080
4167
  onChunk(`
@@ -4633,6 +4720,11 @@ function buildScopedContext(page, mode) {
4633
4720
  const largePageHint = formatLargePageHint(page);
4634
4721
  if (largePageHint) sections.push(`**Reading Hint:** ${largePageHint}`);
4635
4722
  sections.push("");
4723
+ const summaryIntent = analyzePageIntent(page);
4724
+ if (summaryIntent) {
4725
+ sections.push(summaryIntent);
4726
+ sections.push("");
4727
+ }
4636
4728
  if ((page.pageIssues?.length ?? 0) > 0) {
4637
4729
  sections.push("### Page Access Warnings");
4638
4730
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -4688,6 +4780,11 @@ function buildScopedContext(page, mode) {
4688
4780
  sections.push(`**Title:** ${page.title}`);
4689
4781
  sections.push(`**Viewport:** ${formatViewport(page)}`);
4690
4782
  sections.push("");
4783
+ const interactivesIntent = analyzePageIntent(page);
4784
+ if (interactivesIntent) {
4785
+ sections.push(interactivesIntent);
4786
+ sections.push("");
4787
+ }
4691
4788
  const interactivesHighlights = getHighlightsForPage(page.url);
4692
4789
  if (interactivesHighlights.length > 0) {
4693
4790
  sections.push("### Highlights & Annotations");
@@ -4864,6 +4961,62 @@ function buildScopedContext(page, mode) {
4864
4961
  return buildStructuredContext(page);
4865
4962
  }
4866
4963
  }
4964
+ function analyzePageIntent(page) {
4965
+ const hints = [];
4966
+ const url = page.url.toLowerCase();
4967
+ (page.title || "").toLowerCase();
4968
+ const hasPasswordField = page.forms.some(
4969
+ (f) => f.fields.some((el) => el.inputType === "password")
4970
+ );
4971
+ const hasSearchInput = page.interactiveElements.some(
4972
+ (el) => el.inputType === "search" || el.name === "q" || el.name === "query" || el.name === "search" || (el.placeholder || "").toLowerCase().includes("search")
4973
+ ) || page.forms.some(
4974
+ (f) => f.fields.some(
4975
+ (el) => el.inputType === "search" || el.name === "q" || el.name === "query"
4976
+ )
4977
+ );
4978
+ const formCount = page.forms.length;
4979
+ const hasCart = page.interactiveElements.some(
4980
+ (el) => (el.text || "").toLowerCase().includes("cart") || (el.text || "").toLowerCase().includes("checkout")
4981
+ ) || url.includes("cart") || url.includes("checkout");
4982
+ const hasResults = page.interactiveElements.filter((el) => el.type === "link").length > 10;
4983
+ const hasPagination = page.interactiveElements.some(
4984
+ (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»" || (el.label || "").toLowerCase().includes("next page")
4985
+ );
4986
+ if (hasPasswordField) {
4987
+ hints.push("Page type: LOGIN/SIGNUP");
4988
+ hints.push("Suggested: vessel_login or vessel_fill_form → auto-fills credentials and submits");
4989
+ const userField = page.forms.flatMap((f) => f.fields).find(
4990
+ (el) => el.inputType === "email" || el.name === "email" || el.name === "username" || el.autocomplete === "username"
4991
+ );
4992
+ if (userField) {
4993
+ hints.push(`Username field: #${userField.index} [${userField.label || userField.name || userField.placeholder || "input"}]`);
4994
+ }
4995
+ } else if (hasSearchInput && !hasResults) {
4996
+ hints.push("Page type: SEARCH READY");
4997
+ hints.push("Suggested: vessel_search → auto-finds search box, types query, and submits");
4998
+ } else if (hasResults && hasSearchInput) {
4999
+ hints.push("Page type: SEARCH RESULTS");
5000
+ hints.push("Suggested: click a result link, or vessel_paginate for more results");
5001
+ if (hasPagination) hints.push("Pagination detected — vessel_paginate available");
5002
+ } else if (hasCart) {
5003
+ hints.push("Page type: SHOPPING/CHECKOUT");
5004
+ hints.push("Suggested: vessel_fill_form for payment/address fields");
5005
+ } else if (formCount > 0 && !hasPasswordField) {
5006
+ const totalFields = page.forms.reduce((n, f) => n + f.fields.length, 0);
5007
+ hints.push(`Page type: FORM (${formCount} form${formCount > 1 ? "s" : ""}, ${totalFields} fields)`);
5008
+ hints.push("Suggested: vessel_fill_form → fill all fields in one call");
5009
+ } else if (hasPagination) {
5010
+ hints.push("Page type: PAGINATED LIST");
5011
+ hints.push("Suggested: vessel_paginate to navigate between pages");
5012
+ } else if (page.content.length > 3e3 && page.interactiveElements.length < 10) {
5013
+ hints.push("Page type: ARTICLE/CONTENT");
5014
+ hints.push("Suggested: vessel_extract_content for readable text");
5015
+ }
5016
+ if (hints.length === 0) return "";
5017
+ return `### Page Intent (Speedee)
5018
+ ${hints.join("\n")}`;
5019
+ }
4867
5020
  function buildStructuredContext(page) {
4868
5021
  const sections = [];
4869
5022
  sections.push("## PAGE STRUCTURE");
@@ -4877,6 +5030,11 @@ function buildStructuredContext(page) {
4877
5030
  if (page.byline) sections.push(`**Author:** ${page.byline}`);
4878
5031
  if (page.excerpt) sections.push(`**Summary:** ${page.excerpt}`);
4879
5032
  sections.push("");
5033
+ const pageIntent = analyzePageIntent(page);
5034
+ if (pageIntent) {
5035
+ sections.push(pageIntent);
5036
+ sections.push("");
5037
+ }
4880
5038
  if ((page.pageIssues?.length ?? 0) > 0) {
4881
5039
  sections.push("### Page Access Warnings");
4882
5040
  sections.push(formatPageIssues(page.pageIssues ?? []));
@@ -4988,567 +5146,432 @@ function buildGeneralPrompt(query) {
4988
5146
  user: query
4989
5147
  };
4990
5148
  }
4991
- const AGENT_TOOLS = [
5149
+ const TOOL_DEFINITIONS = [
5150
+ // --- Tab Management ---
4992
5151
  {
4993
5152
  name: "current_tab",
4994
- description: "Get the browser tab the human is actively looking at right now. Use this instead of list_tabs when you only need the focused tab.",
4995
- input_schema: {
4996
- type: "object",
4997
- properties: {}
4998
- }
5153
+ title: "Get Active Tab",
5154
+ description: "Get the browser tab the human is actively looking at right now. Use this instead of list_tabs when you only need the focused tab."
4999
5155
  },
5000
5156
  {
5001
5157
  name: "list_tabs",
5002
- description: "List all open browser tabs with their IDs, titles, and URLs.",
5003
- input_schema: {
5004
- type: "object",
5005
- properties: {}
5006
- }
5158
+ title: "List Tabs",
5159
+ description: "List all open browser tabs with their IDs, titles, and URLs."
5007
5160
  },
5008
5161
  {
5009
5162
  name: "switch_tab",
5163
+ title: "Switch Tab",
5010
5164
  description: "Switch to a browser tab by tab ID, or by matching part of the title or URL.",
5011
- input_schema: {
5012
- type: "object",
5013
- properties: {
5014
- tabId: { type: "string", description: "Exact tab ID to switch to" },
5015
- match: {
5016
- type: "string",
5017
- description: "Case-insensitive partial match against tab title or URL"
5018
- }
5019
- }
5165
+ inputSchema: {
5166
+ tabId: zod.z.string().optional().describe("Exact tab ID to switch to"),
5167
+ match: zod.z.string().optional().describe(
5168
+ "Case-insensitive partial match against tab title or URL"
5169
+ )
5020
5170
  }
5021
5171
  },
5022
5172
  {
5023
5173
  name: "create_tab",
5174
+ title: "Create Tab",
5024
5175
  description: "Open a new browser tab, optionally navigating to a URL.",
5025
- input_schema: {
5026
- type: "object",
5027
- properties: {
5028
- url: { type: "string", description: "Optional URL to open" }
5029
- }
5176
+ inputSchema: {
5177
+ url: zod.z.string().optional().describe("Optional URL to open")
5030
5178
  }
5031
5179
  },
5180
+ // --- Navigation ---
5032
5181
  {
5033
5182
  name: "navigate",
5183
+ title: "Navigate",
5034
5184
  description: "Navigate the browser to a URL.",
5035
- input_schema: {
5036
- type: "object",
5037
- properties: {
5038
- url: { type: "string", description: "The URL to navigate to" }
5039
- },
5040
- required: ["url"]
5185
+ inputSchema: {
5186
+ url: zod.z.string().describe("The URL to navigate to")
5041
5187
  }
5042
5188
  },
5043
5189
  {
5044
5190
  name: "go_back",
5045
- description: "Go back to the previous page in browser history.",
5046
- input_schema: {
5047
- type: "object",
5048
- properties: {}
5049
- }
5191
+ title: "Go Back",
5192
+ description: "Go back to the previous page in browser history."
5050
5193
  },
5051
5194
  {
5052
5195
  name: "go_forward",
5053
- description: "Go forward in browser history.",
5054
- input_schema: {
5055
- type: "object",
5056
- properties: {}
5057
- }
5196
+ title: "Go Forward",
5197
+ description: "Go forward in browser history."
5058
5198
  },
5059
5199
  {
5060
5200
  name: "reload",
5061
- description: "Reload the current page.",
5062
- input_schema: {
5063
- type: "object",
5064
- properties: {}
5065
- }
5201
+ title: "Reload",
5202
+ description: "Reload the current page."
5066
5203
  },
5204
+ // --- Interaction ---
5067
5205
  {
5068
5206
  name: "click",
5069
- description: "Click an element on the page. Use the element index from the page content listing, or a CSS selector.",
5070
- input_schema: {
5071
- type: "object",
5072
- properties: {
5073
- index: {
5074
- type: "number",
5075
- description: "The element index number from the page content"
5076
- },
5077
- selector: {
5078
- type: "string",
5079
- description: "CSS selector as fallback if index is not available"
5080
- }
5081
- }
5207
+ title: "Click Element",
5208
+ description: "Click an element on the page by its index number or CSS selector.",
5209
+ inputSchema: {
5210
+ index: zod.z.number().optional().describe("Element index from the page content listing"),
5211
+ selector: zod.z.string().optional().describe("CSS selector as fallback")
5082
5212
  }
5083
5213
  },
5084
5214
  {
5085
5215
  name: "type_text",
5216
+ title: "Type Text",
5086
5217
  description: "Type text into an input field or textarea. Clears existing content first.",
5087
- input_schema: {
5088
- type: "object",
5089
- properties: {
5090
- index: { type: "number", description: "The element index number" },
5091
- selector: { type: "string", description: "CSS selector as fallback" },
5092
- text: { type: "string", description: "The text to type" },
5093
- mode: {
5094
- type: "string",
5095
- enum: ["default", "keystroke"],
5096
- description: '"default" sets value directly and fires input+change events. "keystroke" simulates character-by-character key events for apps that validate on keypress.'
5097
- }
5098
- },
5099
- required: ["text"]
5218
+ inputSchema: {
5219
+ index: zod.z.number().optional().describe("The element index number"),
5220
+ selector: zod.z.string().optional().describe("CSS selector as fallback"),
5221
+ text: zod.z.string().describe("The text to type"),
5222
+ mode: zod.z.enum(["default", "keystroke"]).optional().describe(
5223
+ '"default" sets value directly. "keystroke" simulates character-by-character key events.'
5224
+ )
5100
5225
  }
5101
5226
  },
5102
5227
  {
5103
5228
  name: "select_option",
5229
+ title: "Select Option",
5104
5230
  description: "Select an option in a dropdown by visible label or option value.",
5105
- input_schema: {
5106
- type: "object",
5107
- properties: {
5108
- index: {
5109
- type: "number",
5110
- description: "The select element index number"
5111
- },
5112
- selector: { type: "string", description: "CSS selector as fallback" },
5113
- label: {
5114
- type: "string",
5115
- description: "Visible option label to match"
5116
- },
5117
- value: {
5118
- type: "string",
5119
- description: "Option value attribute to match"
5120
- }
5121
- }
5231
+ inputSchema: {
5232
+ index: zod.z.number().optional().describe("The select element index number"),
5233
+ selector: zod.z.string().optional().describe("CSS selector as fallback"),
5234
+ label: zod.z.string().optional().describe("Visible option label to match"),
5235
+ value: zod.z.string().optional().describe("Option value attribute to match")
5122
5236
  }
5123
5237
  },
5124
5238
  {
5125
5239
  name: "submit_form",
5240
+ title: "Submit Form",
5126
5241
  description: "Submit a form using a field index, submit button index, form selector, or button selector.",
5127
- input_schema: {
5128
- type: "object",
5129
- properties: {
5130
- index: {
5131
- type: "number",
5132
- description: "Index of a field or submit button inside the target form"
5133
- },
5134
- selector: {
5135
- type: "string",
5136
- description: "Form or submit button selector"
5137
- }
5138
- }
5242
+ inputSchema: {
5243
+ index: zod.z.number().optional().describe("Index of a form field or submit button"),
5244
+ selector: zod.z.string().optional().describe("Form or submit button selector")
5139
5245
  }
5140
5246
  },
5141
5247
  {
5142
5248
  name: "press_key",
5143
- description: "Press a keyboard key, optionally targeting a specific element first.",
5144
- input_schema: {
5145
- type: "object",
5146
- properties: {
5147
- key: {
5148
- type: "string",
5149
- description: "Keyboard key, for example Enter or Escape"
5150
- },
5151
- index: { type: "number", description: "Element index to focus first" },
5152
- selector: {
5153
- type: "string",
5154
- description: "CSS selector to focus first"
5155
- }
5156
- },
5157
- required: ["key"]
5249
+ title: "Press Key",
5250
+ description: "Press a keyboard key, optionally after focusing an element.",
5251
+ inputSchema: {
5252
+ key: zod.z.string().describe("Keyboard key such as Enter or Escape"),
5253
+ index: zod.z.number().optional().describe("Element index to focus first"),
5254
+ selector: zod.z.string().optional().describe("CSS selector to focus first")
5158
5255
  }
5159
5256
  },
5160
5257
  {
5161
5258
  name: "scroll",
5259
+ title: "Scroll",
5162
5260
  description: "Scroll the page up or down.",
5163
- input_schema: {
5164
- type: "object",
5165
- properties: {
5166
- direction: {
5167
- type: "string",
5168
- enum: ["up", "down"],
5169
- description: "Scroll direction"
5170
- },
5171
- amount: {
5172
- type: "number",
5173
- description: "Pixels to scroll (default 500)"
5174
- }
5175
- },
5176
- required: ["direction"]
5261
+ inputSchema: {
5262
+ direction: zod.z.enum(["up", "down"]).describe("Scroll direction"),
5263
+ amount: zod.z.number().optional().describe("Pixels to scroll (default 500)")
5177
5264
  }
5178
5265
  },
5179
5266
  {
5180
5267
  name: "hover",
5268
+ title: "Hover Element",
5181
5269
  description: "Move the mouse pointer over an element to trigger hover states, tooltips, or dropdown menus.",
5182
- input_schema: {
5183
- type: "object",
5184
- properties: {
5185
- index: { type: "number", description: "Element index number" },
5186
- selector: { type: "string", description: "CSS selector as fallback" }
5187
- }
5270
+ inputSchema: {
5271
+ index: zod.z.number().optional().describe("Element index number"),
5272
+ selector: zod.z.string().optional().describe("CSS selector as fallback")
5188
5273
  }
5189
5274
  },
5190
5275
  {
5191
5276
  name: "focus",
5277
+ title: "Focus Element",
5192
5278
  description: "Focus an input, button, or interactive element. Useful before pressing keys or to trigger focus-dependent UI.",
5193
- input_schema: {
5194
- type: "object",
5195
- properties: {
5196
- index: { type: "number", description: "Element index number" },
5197
- selector: { type: "string", description: "CSS selector as fallback" }
5198
- }
5279
+ inputSchema: {
5280
+ index: zod.z.number().optional().describe("Element index number"),
5281
+ selector: zod.z.string().optional().describe("CSS selector as fallback")
5199
5282
  }
5200
5283
  },
5284
+ // --- Page & Content ---
5201
5285
  {
5202
5286
  name: "set_ad_blocking",
5203
- description: "Enable or disable ad blocking for the active tab or a matched tab. Reload after changes unless you explicitly set reload to false.",
5204
- input_schema: {
5205
- type: "object",
5206
- properties: {
5207
- enabled: {
5208
- type: "boolean",
5209
- description: "Whether ad blocking should be enabled for the tab"
5210
- },
5211
- tabId: {
5212
- type: "string",
5213
- description: "Exact tab ID to target instead of the active tab"
5214
- },
5215
- match: {
5216
- type: "string",
5217
- description: "Case-insensitive partial match against tab title or URL"
5218
- },
5219
- reload: {
5220
- type: "boolean",
5221
- description: "Reload the tab after changing the setting (default true)"
5222
- }
5223
- },
5224
- required: ["enabled"]
5287
+ title: "Set Ad Blocking",
5288
+ description: "Enable or disable ad blocking for the active tab or a matched tab. Reload after changes unless reload is false.",
5289
+ inputSchema: {
5290
+ enabled: zod.z.boolean().describe("Whether ad blocking should be enabled for the tab"),
5291
+ tabId: zod.z.string().optional().describe("Exact tab ID to target instead of the active tab"),
5292
+ match: zod.z.string().optional().describe(
5293
+ "Case-insensitive partial match against tab title or URL"
5294
+ ),
5295
+ reload: zod.z.boolean().optional().describe("Reload the tab after changing (default true)")
5225
5296
  }
5226
5297
  },
5227
5298
  {
5228
5299
  name: "dismiss_popup",
5229
- description: "Dismiss a modal, popup, newsletter gate, cookie banner, or overlay using common close/decline actions.",
5230
- input_schema: {
5231
- type: "object",
5232
- properties: {}
5233
- }
5300
+ title: "Dismiss Popup",
5301
+ description: "Dismiss a modal, popup, newsletter gate, cookie banner, or overlay using common close/decline actions."
5234
5302
  },
5235
5303
  {
5236
5304
  name: "read_page",
5237
- description: "Re-read the current page content. Includes active text selection and visible unsaved highlights on the active tab when present. Use after navigation or interaction to see updated content.",
5238
- input_schema: {
5239
- type: "object",
5240
- properties: {}
5241
- }
5305
+ title: "Read Page",
5306
+ description: "Re-read the current page content. Includes active text selection and visible unsaved highlights. Use after navigation or interaction to see updated content."
5242
5307
  },
5243
5308
  {
5244
5309
  name: "wait_for",
5310
+ title: "Wait For",
5245
5311
  description: "Wait for a text string or CSS selector to appear on the page before continuing.",
5246
- input_schema: {
5247
- type: "object",
5248
- properties: {
5249
- text: {
5250
- type: "string",
5251
- description: "Text that should appear in the page body"
5252
- },
5253
- selector: {
5254
- type: "string",
5255
- description: "CSS selector that should match an element"
5256
- },
5257
- timeoutMs: {
5258
- type: "number",
5259
- description: "Maximum time to wait in milliseconds (default 5000)"
5260
- }
5261
- }
5312
+ inputSchema: {
5313
+ text: zod.z.string().optional().describe("Text that should appear in the page body"),
5314
+ selector: zod.z.string().optional().describe("CSS selector that should match an element"),
5315
+ timeoutMs: zod.z.number().optional().describe("Maximum time to wait in milliseconds (default 5000)")
5262
5316
  }
5263
5317
  },
5318
+ // --- Checkpoints & Sessions ---
5264
5319
  {
5265
5320
  name: "create_checkpoint",
5321
+ title: "Create Checkpoint",
5266
5322
  description: "Capture the current browser session as a named checkpoint for later recovery.",
5267
- input_schema: {
5268
- type: "object",
5269
- properties: {
5270
- name: {
5271
- type: "string",
5272
- description: "Short checkpoint name"
5273
- },
5274
- note: {
5275
- type: "string",
5276
- description: "Optional note about why this checkpoint matters"
5277
- }
5278
- }
5323
+ inputSchema: {
5324
+ name: zod.z.string().optional().describe("Short checkpoint name"),
5325
+ note: zod.z.string().optional().describe("Optional note about why this checkpoint matters")
5279
5326
  }
5280
5327
  },
5281
5328
  {
5282
5329
  name: "restore_checkpoint",
5330
+ title: "Restore Checkpoint",
5283
5331
  description: "Restore a previously captured checkpoint by name or ID.",
5284
- input_schema: {
5285
- type: "object",
5286
- properties: {
5287
- checkpointId: {
5288
- type: "string",
5289
- description: "Exact checkpoint ID"
5290
- },
5291
- name: {
5292
- type: "string",
5293
- description: "Checkpoint name to match if ID is unknown"
5294
- }
5295
- }
5332
+ inputSchema: {
5333
+ checkpointId: zod.z.string().optional().describe("Exact checkpoint ID"),
5334
+ name: zod.z.string().optional().describe("Checkpoint name to match if ID is unknown")
5296
5335
  }
5297
5336
  },
5298
5337
  {
5299
5338
  name: "save_session",
5339
+ title: "Save Session",
5300
5340
  description: "Persist the current browser cookies, localStorage, and tab layout under a reusable session name.",
5301
- input_schema: {
5302
- type: "object",
5303
- properties: {
5304
- name: {
5305
- type: "string",
5306
- description: "Session name such as github-logged-in"
5307
- }
5308
- },
5309
- required: ["name"]
5341
+ inputSchema: {
5342
+ name: zod.z.string().describe("Session name such as github-logged-in")
5310
5343
  }
5311
5344
  },
5312
5345
  {
5313
5346
  name: "load_session",
5347
+ title: "Load Session",
5314
5348
  description: "Load a previously saved named session, restoring cookies, localStorage, and saved tabs.",
5315
- input_schema: {
5316
- type: "object",
5317
- properties: {
5318
- name: {
5319
- type: "string",
5320
- description: "Previously saved session name"
5321
- }
5322
- },
5323
- required: ["name"]
5349
+ inputSchema: {
5350
+ name: zod.z.string().describe("Previously saved session name")
5324
5351
  }
5325
5352
  },
5326
5353
  {
5327
5354
  name: "list_sessions",
5328
- description: "List previously saved named browser sessions with cookie and storage counts.",
5329
- input_schema: {
5330
- type: "object",
5331
- properties: {}
5332
- }
5355
+ title: "List Sessions",
5356
+ description: "List previously saved named browser sessions with cookie and storage counts."
5333
5357
  },
5334
5358
  {
5335
5359
  name: "delete_session",
5360
+ title: "Delete Session",
5336
5361
  description: "Delete a previously saved named browser session.",
5337
- input_schema: {
5338
- type: "object",
5339
- properties: {
5340
- name: {
5341
- type: "string",
5342
- description: "Saved session name to delete"
5343
- }
5344
- },
5345
- required: ["name"]
5362
+ inputSchema: {
5363
+ name: zod.z.string().describe("Saved session name to delete")
5346
5364
  }
5347
5365
  },
5366
+ // --- Bookmarks ---
5348
5367
  {
5349
5368
  name: "list_bookmarks",
5369
+ title: "List Bookmarks",
5350
5370
  description: "List bookmark folders and saved pages. Optionally filter by folder name or ID.",
5351
- input_schema: {
5352
- type: "object",
5353
- properties: {
5354
- folderId: {
5355
- type: "string",
5356
- description: "Exact bookmark folder ID to filter by"
5357
- },
5358
- folderName: {
5359
- type: "string",
5360
- description: "Exact bookmark folder name to filter by"
5361
- }
5362
- }
5371
+ inputSchema: {
5372
+ folderId: zod.z.string().optional().describe("Exact bookmark folder ID to filter by"),
5373
+ folderName: zod.z.string().optional().describe("Exact bookmark folder name to filter by")
5363
5374
  }
5364
5375
  },
5365
5376
  {
5366
5377
  name: "search_bookmarks",
5378
+ title: "Search Bookmarks",
5367
5379
  description: "Search bookmarks by title, URL, note, folder name, or folder summary.",
5368
- input_schema: {
5369
- type: "object",
5370
- properties: {
5371
- query: {
5372
- type: "string",
5373
- description: "Search term to match against saved bookmarks"
5374
- }
5375
- },
5376
- required: ["query"]
5380
+ inputSchema: {
5381
+ query: zod.z.string().describe("Search term to match against saved bookmarks")
5377
5382
  }
5378
5383
  },
5379
5384
  {
5380
5385
  name: "create_bookmark_folder",
5381
- description: "Create a bookmark folder for organizing saved pages. If the same folder already exists, return it instead of creating a duplicate.",
5382
- input_schema: {
5383
- type: "object",
5384
- properties: {
5385
- name: {
5386
- type: "string",
5387
- description: "Folder name to create"
5388
- },
5389
- summary: {
5390
- type: "string",
5391
- description: "Optional one-sentence summary shown in the UI for this folder"
5392
- }
5393
- },
5394
- required: ["name"]
5386
+ title: "Create Bookmark Folder",
5387
+ description: "Create a bookmark folder for organizing saved pages. Returns existing folder if the same name exists.",
5388
+ inputSchema: {
5389
+ name: zod.z.string().describe("Folder name to create"),
5390
+ summary: zod.z.string().optional().describe("Optional one-sentence summary for this folder")
5395
5391
  }
5396
5392
  },
5397
5393
  {
5398
5394
  name: "save_bookmark",
5399
- description: "Save the current page, a specified URL, or a link target from the current page as a bookmark. If folderName is provided and missing, create it automatically.",
5400
- input_schema: {
5401
- type: "object",
5402
- properties: {
5403
- url: {
5404
- type: "string",
5405
- description: "URL to save. Omit to save the current page, or provide index/selector to save a link target from the page."
5406
- },
5407
- title: {
5408
- type: "string",
5409
- description: "Title for the bookmark. Omit to use the current page title or the selected link text."
5410
- },
5411
- index: {
5412
- type: "number",
5413
- description: "Element index of a link on the current page to bookmark without opening it."
5414
- },
5415
- selector: {
5416
- type: "string",
5417
- description: "CSS selector of a link on the current page to bookmark without opening it."
5418
- },
5419
- folderId: {
5420
- type: "string",
5421
- description: "Folder ID to save into"
5422
- },
5423
- folderName: {
5424
- type: "string",
5425
- description: "Folder name to save into. Created automatically if missing."
5426
- },
5427
- folderSummary: {
5428
- type: "string",
5429
- description: "Optional summary used if a new folder is created"
5430
- },
5431
- createFolderIfMissing: {
5432
- type: "boolean",
5433
- description: "Create folderName automatically when it does not exist"
5434
- },
5435
- note: {
5436
- type: "string",
5437
- description: "Optional note about why the page was saved"
5438
- },
5439
- onDuplicate: {
5440
- type: "string",
5441
- enum: ["ask", "update", "duplicate"],
5442
- description: 'How to handle an existing bookmark with the same URL in the same folder: "ask" (default), "update", or "duplicate".'
5443
- }
5444
- }
5395
+ title: "Save Bookmark",
5396
+ description: "Save the current page, a specified URL, or a link target from the current page as a bookmark.",
5397
+ inputSchema: {
5398
+ url: zod.z.string().optional().describe("URL to save. Omit to save the current page."),
5399
+ title: zod.z.string().optional().describe("Title for the bookmark"),
5400
+ index: zod.z.number().optional().describe("Element index of a link to bookmark without opening"),
5401
+ selector: zod.z.string().optional().describe("CSS selector of a link to bookmark without opening"),
5402
+ folderId: zod.z.string().optional().describe("Folder ID to save into"),
5403
+ folderName: zod.z.string().optional().describe("Folder name to save into. Created automatically if missing."),
5404
+ folderSummary: zod.z.string().optional().describe("Optional summary used if a new folder is created"),
5405
+ createFolderIfMissing: zod.z.boolean().optional().describe("Create folderName automatically when it does not exist"),
5406
+ note: zod.z.string().optional().describe("Optional note about why the page was saved"),
5407
+ onDuplicate: zod.z.enum(["ask", "update", "duplicate"]).optional().describe("How to handle duplicate URLs in the same folder")
5445
5408
  }
5446
5409
  },
5447
5410
  {
5448
5411
  name: "organize_bookmark",
5449
- description: 'Organize a bookmark by intent: move an existing bookmark or save the current page, a URL, or a link target from the current page into a folder, creating the folder if needed. If archive is true, use the default "Archive" folder.',
5450
- input_schema: {
5451
- type: "object",
5452
- properties: {
5453
- bookmarkId: {
5454
- type: "string",
5455
- description: "Existing bookmark ID to move or update"
5456
- },
5457
- url: {
5458
- type: "string",
5459
- description: "URL to organize. Omit to use the current page, or provide index/selector to organize a link target from the page."
5460
- },
5461
- title: {
5462
- type: "string",
5463
- description: "Optional title when saving a new bookmark or retitling an existing one"
5464
- },
5465
- index: {
5466
- type: "number",
5467
- description: "Element index of a link on the current page to organize without opening it."
5468
- },
5469
- selector: {
5470
- type: "string",
5471
- description: "CSS selector of a link on the current page to organize without opening it."
5472
- },
5473
- folderId: {
5474
- type: "string",
5475
- description: "Exact bookmark folder ID target"
5476
- },
5477
- folderName: {
5478
- type: "string",
5479
- description: "Folder name target. Created automatically if missing"
5480
- },
5481
- folderSummary: {
5482
- type: "string",
5483
- description: "Optional summary used if a new folder is created"
5484
- },
5485
- createFolderIfMissing: {
5486
- type: "boolean",
5487
- description: "Create folderName automatically when it does not exist"
5488
- },
5489
- note: {
5490
- type: "string",
5491
- description: "Optional note to attach or update on the bookmark"
5492
- },
5493
- archive: {
5494
- type: "boolean",
5495
- description: 'If true, organize into the default "Archive" folder'
5496
- }
5497
- }
5412
+ title: "Organize Bookmark",
5413
+ description: "Move an existing bookmark or save the current page into a folder, creating the folder if needed.",
5414
+ inputSchema: {
5415
+ bookmarkId: zod.z.string().optional().describe("Existing bookmark ID to move"),
5416
+ url: zod.z.string().optional().describe("URL to organize"),
5417
+ title: zod.z.string().optional().describe("Optional title"),
5418
+ index: zod.z.number().optional().describe("Element index of a link to organize"),
5419
+ selector: zod.z.string().optional().describe("CSS selector of a link to organize"),
5420
+ folderId: zod.z.string().optional().describe("Target folder ID"),
5421
+ folderName: zod.z.string().optional().describe("Target folder name. Created automatically if missing"),
5422
+ folderSummary: zod.z.string().optional().describe("Optional summary for new folder"),
5423
+ createFolderIfMissing: zod.z.boolean().optional().describe("Create folderName automatically when it does not exist"),
5424
+ note: zod.z.string().optional().describe("Optional note"),
5425
+ archive: zod.z.boolean().optional().describe('If true, organize into the default "Archive" folder')
5498
5426
  }
5499
5427
  },
5500
5428
  {
5501
5429
  name: "archive_bookmark",
5502
- description: 'Archive the current page, a URL, a link target from the current page, or an existing bookmark into the default "Archive" folder.',
5503
- input_schema: {
5504
- type: "object",
5505
- properties: {
5506
- bookmarkId: {
5507
- type: "string",
5508
- description: "Existing bookmark ID to archive"
5509
- },
5510
- url: {
5511
- type: "string",
5512
- description: "URL to archive. Omit to use the current page, or provide index/selector to archive a link target from the page."
5513
- },
5514
- title: {
5515
- type: "string",
5516
- description: "Optional title when saving a new archived bookmark"
5517
- },
5518
- index: {
5519
- type: "number",
5520
- description: "Element index of a link on the current page to archive without opening it."
5521
- },
5522
- selector: {
5523
- type: "string",
5524
- description: "CSS selector of a link on the current page to archive without opening it."
5525
- },
5526
- note: {
5527
- type: "string",
5528
- description: "Optional note about why the page was archived"
5529
- }
5530
- }
5430
+ title: "Archive Bookmark",
5431
+ description: 'Archive the current page, a URL, a link target, or an existing bookmark into the "Archive" folder.',
5432
+ inputSchema: {
5433
+ bookmarkId: zod.z.string().optional().describe("Existing bookmark ID to archive"),
5434
+ url: zod.z.string().optional().describe("URL to archive"),
5435
+ title: zod.z.string().optional().describe("Optional title"),
5436
+ index: zod.z.number().optional().describe("Element index of a link to archive"),
5437
+ selector: zod.z.string().optional().describe("CSS selector of a link to archive"),
5438
+ note: zod.z.string().optional().describe("Optional note")
5531
5439
  }
5532
5440
  },
5533
5441
  {
5534
5442
  name: "open_bookmark",
5443
+ title: "Open Bookmark",
5535
5444
  description: "Open a saved bookmark by its bookmark ID. Optionally open it in a new tab.",
5536
- input_schema: {
5537
- type: "object",
5538
- properties: {
5539
- bookmarkId: {
5540
- type: "string",
5541
- description: "Exact bookmark ID to open"
5542
- },
5543
- newTab: {
5544
- type: "boolean",
5545
- description: "Open the bookmark in a new tab instead of the current tab"
5546
- }
5547
- },
5548
- required: ["bookmarkId"]
5445
+ inputSchema: {
5446
+ bookmarkId: zod.z.string().describe("Exact bookmark ID to open"),
5447
+ newTab: zod.z.boolean().optional().describe("Open in a new tab instead of the current tab")
5448
+ }
5449
+ },
5450
+ // --- Highlights ---
5451
+ {
5452
+ name: "highlight",
5453
+ title: "Highlight Element",
5454
+ description: "Visually highlight an element or text on the page for the user. Use to draw attention to specific content. Highlights persist until cleared.",
5455
+ inputSchema: {
5456
+ index: zod.z.number().optional().describe("Element index from page content to highlight"),
5457
+ selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
5458
+ text: zod.z.string().optional().describe("Text to find and highlight on the page (all occurrences)"),
5459
+ label: zod.z.string().optional().describe("Annotation label to display near the highlight"),
5460
+ durationMs: zod.z.number().optional().describe("Auto-clear after this many milliseconds (omit for permanent)"),
5461
+ color: zod.z.enum(["yellow", "red", "green", "blue", "purple", "orange"]).optional().describe("Highlight color (default yellow)")
5462
+ }
5463
+ },
5464
+ {
5465
+ name: "clear_highlights",
5466
+ title: "Clear Highlights",
5467
+ description: "Remove all visual highlights from the current page."
5468
+ },
5469
+ // --- Speedee System: Flow State ---
5470
+ {
5471
+ name: "flow_start",
5472
+ title: "Start Workflow",
5473
+ description: "Begin tracking a multi-step web workflow. Vessel will show progress after every action so you always know where you are.",
5474
+ inputSchema: {
5475
+ goal: zod.z.string().describe("What this workflow accomplishes (e.g. 'Purchase item from Amazon')"),
5476
+ steps: zod.z.array(zod.z.string()).describe(
5477
+ "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
5478
+ )
5479
+ }
5480
+ },
5481
+ {
5482
+ name: "flow_advance",
5483
+ title: "Advance Workflow Step",
5484
+ description: "Mark the current workflow step as done and move to the next one.",
5485
+ inputSchema: {
5486
+ detail: zod.z.string().optional().describe("Brief note about what was accomplished")
5487
+ }
5488
+ },
5489
+ {
5490
+ name: "flow_status",
5491
+ title: "Workflow Status",
5492
+ description: "Check the current workflow progress."
5493
+ },
5494
+ {
5495
+ name: "flow_end",
5496
+ title: "End Workflow",
5497
+ description: "Clear the active workflow tracker."
5498
+ },
5499
+ // --- Speedee System: Suggestion Engine ---
5500
+ {
5501
+ name: "suggest",
5502
+ title: "What Should I Do?",
5503
+ description: "Analyze the current page and return the most relevant tools and suggested next actions. Call this when unsure what to do."
5504
+ },
5505
+ // --- Speedee System: Composable Macros ---
5506
+ {
5507
+ name: "fill_form",
5508
+ title: "Fill Form",
5509
+ description: "Fill multiple form fields at once. Much faster than calling type_text for each field individually.",
5510
+ inputSchema: {
5511
+ fields: zod.z.array(
5512
+ zod.z.object({
5513
+ index: zod.z.number().optional().describe("Element index from page content"),
5514
+ selector: zod.z.string().optional().describe("CSS selector fallback"),
5515
+ value: zod.z.string().describe("Value to enter")
5516
+ })
5517
+ ).describe("Fields to fill"),
5518
+ submit: zod.z.boolean().optional().describe("Submit the form after filling (default false)")
5519
+ }
5520
+ },
5521
+ {
5522
+ name: "login",
5523
+ title: "Login",
5524
+ description: "Compound action: navigate to a login page, fill credentials, and submit. Handles the full login flow in one call.",
5525
+ inputSchema: {
5526
+ url: zod.z.string().optional().describe("Login page URL (skip if already on login page)"),
5527
+ username: zod.z.string().describe("Username or email"),
5528
+ password: zod.z.string().describe("Password"),
5529
+ username_selector: zod.z.string().optional().describe("CSS selector for username field (auto-detected if omitted)"),
5530
+ password_selector: zod.z.string().optional().describe("CSS selector for password field (auto-detected if omitted)"),
5531
+ submit_selector: zod.z.string().optional().describe("CSS selector for submit button (auto-detected if omitted)")
5532
+ }
5533
+ },
5534
+ {
5535
+ name: "search",
5536
+ title: "Search",
5537
+ description: "Find a search box on the current page, type a query, and submit. Returns the resulting page state.",
5538
+ inputSchema: {
5539
+ query: zod.z.string().describe("Search query text"),
5540
+ selector: zod.z.string().optional().describe("CSS selector for search input (auto-detected if omitted)")
5541
+ }
5542
+ },
5543
+ {
5544
+ name: "paginate",
5545
+ title: "Paginate",
5546
+ description: "Navigate to the next or previous page of results. Auto-detects pagination controls.",
5547
+ inputSchema: {
5548
+ direction: zod.z.enum(["next", "prev"]).describe("Pagination direction"),
5549
+ selector: zod.z.string().optional().describe("CSS selector for pagination link (auto-detected if omitted)")
5549
5550
  }
5550
5551
  }
5551
5552
  ];
5553
+ function toAnthropicTools(defs) {
5554
+ return defs.filter((d) => !d.mcpOnly).map((d) => {
5555
+ let inputSchema;
5556
+ if (d.inputSchema) {
5557
+ const jsonSchema = zod.z.toJSONSchema(zod.z.object(d.inputSchema));
5558
+ delete jsonSchema.$schema;
5559
+ delete jsonSchema.additionalProperties;
5560
+ inputSchema = jsonSchema;
5561
+ } else {
5562
+ inputSchema = {
5563
+ type: "object",
5564
+ properties: {}
5565
+ };
5566
+ }
5567
+ return {
5568
+ name: d.name,
5569
+ description: d.description,
5570
+ input_schema: inputSchema
5571
+ };
5572
+ });
5573
+ }
5574
+ const AGENT_TOOLS = toAnthropicTools(TOOL_DEFINITIONS);
5552
5575
  function trimText(value) {
5553
5576
  return typeof value === "string" ? value.trim() : "";
5554
5577
  }
@@ -6643,6 +6666,17 @@ async function describeElementForClick$1(wc, selector) {
6643
6666
  };
6644
6667
  }
6645
6668
  async function clickResolvedSelector$1(wc, selector) {
6669
+ if (selector.startsWith("__vessel_idx:")) {
6670
+ const idx = Number(selector.slice("__vessel_idx:".length));
6671
+ const beforeUrl2 = wc.getURL();
6672
+ const result = await wc.executeJavaScript(
6673
+ `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`
6674
+ );
6675
+ if (typeof result === "string" && result.startsWith("Error")) return result;
6676
+ await waitForPotentialNavigation$1(wc, beforeUrl2);
6677
+ const afterUrl2 = wc.getURL();
6678
+ return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
6679
+ }
6646
6680
  const beforeUrl = wc.getURL();
6647
6681
  const elInfo = await describeElementForClick$1(wc, selector);
6648
6682
  if ("error" in elInfo) return `Error: ${elInfo.error}`;
@@ -6750,6 +6784,10 @@ async function dismissPopup$1(wc) {
6750
6784
  document.querySelectorAll("dialog, [role='dialog'], [role='alertdialog'], [aria-modal='true']").forEach((el) => {
6751
6785
  if (isVisible(el)) nodes.push(el);
6752
6786
  });
6787
+ // Detect known consent manager containers by ID/class patterns
6788
+ document.querySelectorAll("#onetrust-consent-sdk, #onetrust-banner-sdk, [id*='onetrust'], [class*='onetrust'], #CybotCookiebotDialog, #truste-consent-track, [id*='cookie-banner'], [id*='consent-banner'], [class*='cookie-consent'], [class*='consent-banner'], [id*='gdpr'], [class*='gdpr']").forEach((el) => {
6789
+ if (el instanceof HTMLElement && isVisible(el)) nodes.push(el);
6790
+ });
6753
6791
  document.querySelectorAll("body *").forEach((el) => {
6754
6792
  if (!(el instanceof HTMLElement) || !isVisible(el)) return;
6755
6793
  const style = window.getComputedStyle(el);
@@ -6779,14 +6817,20 @@ async function dismissPopup$1(wc) {
6779
6817
  el.textContent ||
6780
6818
  el.getAttribute("value"),
6781
6819
  ).toLowerCase();
6782
- const classText = text(el.className).toLowerCase();
6820
+ const classText = text(typeof el.className === "string" ? el.className : "").toLowerCase();
6783
6821
  const idText = text(el.id).toLowerCase();
6822
+ const combined = classText + " " + idText;
6784
6823
  let score = rooted ? 30 : 0;
6785
6824
  if (/^x$|^×$/.test(label)) score += 120;
6786
- if (/no thanks|no, thanks|not now|maybe later|dismiss|close|skip|cancel|continue without|no thank you/.test(label)) score += 100;
6787
- if (/close|dismiss|modal-close|overlay-close/.test(classText + " " + idText)) score += 90;
6825
+ if (/no thanks|no, thanks|not now|maybe later|dismiss|close|skip|cancel|continue without|no thank you|reject|decline/.test(label)) score += 100;
6826
+ if (/close|dismiss|modal-close|overlay-close/.test(combined)) score += 90;
6827
+ // Known consent manager dismiss/reject buttons get a big boost
6828
+ if (/onetrust-close|onetrust-reject|cookie.*close|consent.*close|cookie.*reject|consent.*reject/.test(combined)) score += 110;
6829
+ // OneTrust "Accept" is valid for dismissing the banner (user just wants it gone)
6830
+ if (/onetrust-accept|cookie.*accept|consent.*accept/.test(combined)) score += 80;
6788
6831
  if (el.getAttribute("aria-label")) score += 20;
6789
- if (/accept|continue|submit|sign up|subscribe|join|start|next/.test(label)) score -= 80;
6832
+ // Penalize general accept/subscribe buttons that aren't consent-related
6833
+ if (/accept|continue|submit|sign up|subscribe|join|start|next/.test(label) && !/cookie|consent|onetrust/.test(combined)) score -= 80;
6790
6834
  const rect = el.getBoundingClientRect();
6791
6835
  if (rect.top < 120) score += 10;
6792
6836
  if (rect.right > (window.innerWidth || 0) - 120) score += 15;
@@ -6802,13 +6846,26 @@ async function dismissPopup$1(wc) {
6802
6846
  if (!(el instanceof HTMLElement) || !isVisible(el)) return;
6803
6847
  const candidateSelector = selectorFor(el);
6804
6848
  if (!candidateSelector) return;
6805
- const label = text(
6849
+ var label = text(
6806
6850
  el.getAttribute("aria-label") ||
6807
6851
  el.getAttribute("title") ||
6808
6852
  el.textContent ||
6809
6853
  el.getAttribute("value"),
6810
6854
  );
6811
- if (!label) return;
6855
+ // Don't skip empty-label buttons from known consent managers
6856
+ if (!label) {
6857
+ var idLower = (el.id || "").toLowerCase();
6858
+ var classLower = (typeof el.className === "string" ? el.className : "").toLowerCase();
6859
+ var combined = idLower + " " + classLower;
6860
+ if (/onetrust|consent|cookie|banner|gdpr|trustarc|cookiebot/.test(combined)) {
6861
+ label = idLower.includes("accept") ? "Accept cookies"
6862
+ : idLower.includes("reject") ? "Reject cookies"
6863
+ : idLower.includes("close") || classLower.includes("close") ? "Close"
6864
+ : "Consent button";
6865
+ } else {
6866
+ return;
6867
+ }
6868
+ }
6812
6869
  results.push({
6813
6870
  selector: candidateSelector,
6814
6871
  label: label.slice(0, 120),
@@ -6877,7 +6934,11 @@ async function resolveSelector$1(wc, index, selector) {
6877
6934
  `
6878
6935
  );
6879
6936
  if (typeof authoritativeSelector === "string" && authoritativeSelector) {
6880
- return authoritativeSelector;
6937
+ const resolves = await wc.executeJavaScript(
6938
+ `!!document.querySelector(${JSON.stringify(authoritativeSelector)})`
6939
+ );
6940
+ if (resolves) return authoritativeSelector;
6941
+ return `__vessel_idx:${index}`;
6881
6942
  }
6882
6943
  const page = await extractContent(wc);
6883
6944
  const extractedSelector = findSelectorByIndex(page, index);
@@ -6978,10 +7039,20 @@ function isDangerousAction$1(name) {
6978
7039
  "create_tab",
6979
7040
  "switch_tab",
6980
7041
  "restore_checkpoint",
6981
- "load_session"
7042
+ "load_session",
7043
+ "login",
7044
+ "fill_form",
7045
+ "search",
7046
+ "paginate"
6982
7047
  ].includes(name);
6983
7048
  }
6984
7049
  async function setElementValue$1(wc, selector, value) {
7050
+ if (selector.startsWith("__vessel_idx:")) {
7051
+ const idx = Number(selector.slice("__vessel_idx:".length));
7052
+ return wc.executeJavaScript(
7053
+ `window.__vessel?.interactByIndex?.(${idx}, "value", ${JSON.stringify(value)}) || "Error: interactByIndex not available"`
7054
+ );
7055
+ }
6985
7056
  return wc.executeJavaScript(`
6986
7057
  (function() {
6987
7058
  const el = document.querySelector(${JSON.stringify(selector)});
@@ -7407,9 +7478,12 @@ async function getPostActionState$1(ctx, name) {
7407
7478
  "click",
7408
7479
  "submit_form",
7409
7480
  "reload",
7410
- "press_key"
7481
+ "press_key",
7482
+ "login",
7483
+ "search",
7484
+ "paginate"
7411
7485
  ];
7412
- const interactActions = ["type_text", "select_option", "hover", "focus"];
7486
+ const interactActions = ["type_text", "select_option", "hover", "focus", "fill_form"];
7413
7487
  const tabActions = [
7414
7488
  "create_tab",
7415
7489
  "switch_tab",
@@ -7479,7 +7553,12 @@ async function executeAction(name, args, ctx) {
7479
7553
  "save_bookmark",
7480
7554
  "organize_bookmark",
7481
7555
  "archive_bookmark",
7482
- "open_bookmark"
7556
+ "open_bookmark",
7557
+ "flow_start",
7558
+ "flow_advance",
7559
+ "flow_status",
7560
+ "flow_end",
7561
+ "suggest"
7483
7562
  ].includes(name)) {
7484
7563
  return "Error: No active tab";
7485
7564
  }
@@ -7964,12 +8043,251 @@ ${truncated}`;
7964
8043
  if (!wc) return "Error: No active tab";
7965
8044
  return clearHighlights(wc);
7966
8045
  }
8046
+ // --- Speedee System ---
8047
+ case "flow_start": {
8048
+ const goal = typeof args.goal === "string" ? args.goal : "";
8049
+ const steps = Array.isArray(args.steps) ? args.steps.map(String) : [];
8050
+ if (!goal || steps.length === 0) return "Error: goal and steps are required";
8051
+ const flow = ctx.runtime.startFlow(goal, steps, wc?.getURL());
8052
+ return `Flow started: ${flow.goal}
8053
+ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`;
8054
+ }
8055
+ case "flow_advance": {
8056
+ const flow = ctx.runtime.advanceFlow(
8057
+ typeof args.detail === "string" ? args.detail : void 0
8058
+ );
8059
+ if (!flow) return "No active flow to advance";
8060
+ return `Step completed.${ctx.runtime.getFlowContext()}`;
8061
+ }
8062
+ case "flow_status": {
8063
+ const flow = ctx.runtime.getFlowState();
8064
+ if (!flow) return "No active workflow.";
8065
+ return ctx.runtime.getFlowContext();
8066
+ }
8067
+ case "flow_end": {
8068
+ ctx.runtime.clearFlow();
8069
+ return "Workflow ended.";
8070
+ }
8071
+ case "suggest": {
8072
+ if (!wc) return "No active tab. Use navigate to open a page.";
8073
+ let page;
8074
+ try {
8075
+ page = await extractContent(wc);
8076
+ } catch {
8077
+ return "Could not read page. Try navigate to a working URL.";
8078
+ }
8079
+ const suggestions = [];
8080
+ suggestions.push(`Page: ${page.title || "(untitled)"}`);
8081
+ suggestions.push(`URL: ${page.url}`);
8082
+ suggestions.push("");
8083
+ const flowCtx2 = ctx.runtime.getFlowContext();
8084
+ if (flowCtx2) {
8085
+ suggestions.push(flowCtx2);
8086
+ suggestions.push("");
8087
+ }
8088
+ const hasPasswordField = page.forms.some(
8089
+ (f) => f.fields.some((el) => el.inputType === "password")
8090
+ );
8091
+ const hasSearchInput = page.interactiveElements.some(
8092
+ (el) => el.inputType === "search" || el.name === "q" || el.name === "query" || (el.placeholder || "").toLowerCase().includes("search")
8093
+ );
8094
+ const formCount = page.forms.length;
8095
+ const totalFields = page.forms.reduce((n, f) => n + f.fields.length, 0);
8096
+ const linkCount = page.interactiveElements.filter((el) => el.type === "link").length;
8097
+ const hasPagination = page.interactiveElements.some(
8098
+ (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»"
8099
+ );
8100
+ const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
8101
+ if (hasOverlays) {
8102
+ suggestions.push("BLOCKING OVERLAY detected — dismiss it first:");
8103
+ suggestions.push(" → dismiss_popup or click on close/accept button");
8104
+ suggestions.push("");
8105
+ }
8106
+ if (hasPasswordField) {
8107
+ suggestions.push("LOGIN PAGE detected:");
8108
+ suggestions.push(" → login(username, password) — handles the full flow");
8109
+ suggestions.push(" → Or fill_form + submit_form for manual control");
8110
+ } else if (hasSearchInput && linkCount < 10) {
8111
+ suggestions.push("SEARCH PAGE detected:");
8112
+ suggestions.push(" → search(query) — finds the box, types, submits");
8113
+ } else if (hasSearchInput && linkCount >= 10) {
8114
+ suggestions.push("SEARCH RESULTS detected:");
8115
+ suggestions.push(" → click on a result link");
8116
+ if (hasPagination) suggestions.push(" → paginate('next') for more results");
8117
+ } else if (formCount > 0) {
8118
+ suggestions.push(`FORM detected (${totalFields} fields):`);
8119
+ suggestions.push(" → fill_form(fields) — fill all fields at once");
8120
+ } else if (hasPagination) {
8121
+ suggestions.push("PAGINATED CONTENT:");
8122
+ suggestions.push(" → read_page to read this page");
8123
+ suggestions.push(" → paginate('next') for the next page");
8124
+ } else if (page.content.length > 3e3 && page.interactiveElements.length < 10) {
8125
+ suggestions.push("ARTICLE/CONTENT page:");
8126
+ suggestions.push(" → read_page for readable text");
8127
+ suggestions.push(" → scroll to see more");
8128
+ } else {
8129
+ suggestions.push("GENERAL PAGE:");
8130
+ suggestions.push(" → read_page to understand the page structure");
8131
+ suggestions.push(" → click on any element by index");
8132
+ suggestions.push(" → navigate to go somewhere new");
8133
+ }
8134
+ suggestions.push("");
8135
+ suggestions.push(`Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`);
8136
+ return suggestions.join("\n");
8137
+ }
8138
+ case "fill_form": {
8139
+ if (!wc) return "Error: No active tab";
8140
+ const fields = Array.isArray(args.fields) ? args.fields : [];
8141
+ if (fields.length === 0) return "Error: No fields provided";
8142
+ const results = [];
8143
+ for (const field of fields) {
8144
+ const sel = await resolveSelector$1(wc, field.index, field.selector);
8145
+ if (!sel) {
8146
+ results.push(`Skipped: no selector for index=${field.index}`);
8147
+ continue;
8148
+ }
8149
+ const result2 = await setElementValue$1(wc, sel, String(field.value || ""));
8150
+ results.push(result2);
8151
+ }
8152
+ if (args.submit) {
8153
+ const firstSel = await resolveSelector$1(wc, fields[0]?.index, fields[0]?.selector);
8154
+ if (firstSel) {
8155
+ const beforeUrl = wc.getURL();
8156
+ const submitResult = await submitForm$1(wc, { selector: firstSel });
8157
+ await waitForPotentialNavigation$1(wc, beforeUrl);
8158
+ const afterUrl = wc.getURL();
8159
+ results.push(
8160
+ afterUrl !== beforeUrl ? `Submitted → ${afterUrl}` : submitResult
8161
+ );
8162
+ }
8163
+ }
8164
+ return `Filled ${results.length} field(s):
8165
+ ${results.join("\n")}`;
8166
+ }
8167
+ case "login": {
8168
+ if (!wc) return "Error: No active tab";
8169
+ const steps = [];
8170
+ if (typeof args.url === "string" && args.url.trim()) {
8171
+ const id = ctx.tabManager.getActiveTabId();
8172
+ ctx.tabManager.navigateTab(id, args.url);
8173
+ await waitForLoad$1(wc);
8174
+ steps.push(`Navigated to ${wc.getURL()}`);
8175
+ }
8176
+ const userSel = args.username_selector || await wc.executeJavaScript(`
8177
+ (function() {
8178
+ var el = document.querySelector('input[type="email"], input[name="email"], input[name="username"], input[name="user"], input[autocomplete="username"], input[autocomplete="email"], input[type="text"]:not([name="search"]):not([name="q"])');
8179
+ return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
8180
+ })()
8181
+ `);
8182
+ if (!userSel) return "Error: Could not find username/email field. Try providing username_selector.";
8183
+ const passSel = args.password_selector || await wc.executeJavaScript(`
8184
+ (function() {
8185
+ var el = document.querySelector('input[type="password"]');
8186
+ return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
8187
+ })()
8188
+ `);
8189
+ if (!passSel) return "Error: Could not find password field. Try providing password_selector.";
8190
+ const userResult = await setElementValue$1(wc, userSel, String(args.username || ""));
8191
+ steps.push(userResult);
8192
+ const passResult = await setElementValue$1(wc, passSel, String(args.password || ""));
8193
+ steps.push(passResult);
8194
+ const beforeUrl = wc.getURL();
8195
+ if (args.submit_selector) {
8196
+ await clickResolvedSelector$1(wc, args.submit_selector);
8197
+ } else {
8198
+ const clicked = await wc.executeJavaScript(`
8199
+ (function() {
8200
+ var btn = document.querySelector('button[type="submit"], input[type="submit"], form button:not([type="button"])');
8201
+ if (btn) { btn.click(); return true; }
8202
+ var form = document.querySelector('input[type="password"]')?.closest('form');
8203
+ if (form) { form.requestSubmit ? form.requestSubmit() : form.submit(); return true; }
8204
+ return false;
8205
+ })()
8206
+ `);
8207
+ if (!clicked) return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
8208
+ }
8209
+ await waitForPotentialNavigation$1(wc, beforeUrl);
8210
+ const afterUrl = wc.getURL();
8211
+ steps.push(
8212
+ afterUrl !== beforeUrl ? `Submitted → ${afterUrl}` : "Form submitted (same page)"
8213
+ );
8214
+ return `Login flow complete:
8215
+ ${steps.join("\n")}`;
8216
+ }
8217
+ case "search": {
8218
+ if (!wc) return "Error: No active tab";
8219
+ const searchSel = args.selector || await wc.executeJavaScript(`
8220
+ (function() {
8221
+ var el = document.querySelector('input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]');
8222
+ if (!el) {
8223
+ var inputs = document.querySelectorAll('input[type="text"]');
8224
+ for (var i = 0; i < inputs.length; i++) {
8225
+ var form = inputs[i].closest('form');
8226
+ if (form && (form.getAttribute('role') === 'search' || form.action?.includes('search'))) {
8227
+ el = inputs[i];
8228
+ break;
8229
+ }
8230
+ }
8231
+ }
8232
+ return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
8233
+ })()
8234
+ `);
8235
+ if (!searchSel) return "Error: Could not find search input. Try providing a selector.";
8236
+ await setElementValue$1(wc, searchSel, String(args.query || ""));
8237
+ await wc.executeJavaScript(`
8238
+ (function() {
8239
+ var el = document.querySelector(${JSON.stringify(searchSel)});
8240
+ if (el) el.focus();
8241
+ })()
8242
+ `);
8243
+ await sleep$1(50);
8244
+ const beforeUrl = wc.getURL();
8245
+ wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
8246
+ await sleep$1(16);
8247
+ wc.sendInputEvent({ type: "keyUp", keyCode: "Return" });
8248
+ await waitForPotentialNavigation$1(wc, beforeUrl);
8249
+ const afterUrl = wc.getURL();
8250
+ return afterUrl !== beforeUrl ? `Searched "${args.query}" → ${afterUrl}` : `Searched "${args.query}" (same page — results may have loaded dynamically)`;
8251
+ }
8252
+ case "paginate": {
8253
+ if (!wc) return "Error: No active tab";
8254
+ const beforeUrl = wc.getURL();
8255
+ if (args.selector) {
8256
+ return clickResolvedSelector$1(wc, args.selector);
8257
+ }
8258
+ const isNext = args.direction === "next";
8259
+ const clicked = await wc.executeJavaScript(`
8260
+ (function() {
8261
+ var patterns = ${isNext ? '["next", "Next", "›", "»", "→", ">", "Next Page", "Load More"]' : '["prev", "Prev", "Previous", "‹", "«", "←", "<", "Previous Page"]'};
8262
+ var links = document.querySelectorAll('a, button');
8263
+ for (var i = 0; i < links.length; i++) {
8264
+ var el = links[i];
8265
+ var text = (el.textContent || '').trim();
8266
+ var ariaLabel = (el.getAttribute('aria-label') || '').toLowerCase();
8267
+ var rel = (el.getAttribute('rel') || '').toLowerCase();
8268
+ if (rel === '${isNext ? "next" : "prev"}') { el.click(); return true; }
8269
+ for (var j = 0; j < patterns.length; j++) {
8270
+ if (text === patterns[j] || ariaLabel.includes(patterns[j].toLowerCase())) {
8271
+ el.click();
8272
+ return true;
8273
+ }
8274
+ }
8275
+ }
8276
+ return false;
8277
+ })()
8278
+ `);
8279
+ if (!clicked) return `Error: Could not find ${args.direction} pagination control. Try providing a selector.`;
8280
+ await waitForPotentialNavigation$1(wc, beforeUrl);
8281
+ const afterUrl = wc.getURL();
8282
+ return afterUrl !== beforeUrl ? `Paginated ${args.direction} → ${afterUrl}` : `Clicked ${args.direction} (page may have updated dynamically)`;
8283
+ }
7967
8284
  default:
7968
8285
  return `Unknown tool: ${name}`;
7969
8286
  }
7970
8287
  }
7971
8288
  });
7972
- return result + await getPostActionState$1(ctx, name);
8289
+ const flowCtx = ctx.runtime.getFlowContext();
8290
+ return result + await getPostActionState$1(ctx, name) + flowCtx;
7973
8291
  }
7974
8292
  async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime, history) {
7975
8293
  const lowerQuery = query.toLowerCase().trim();
@@ -7981,8 +8299,20 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
7981
8299
  const truncated = pageContent.content.length > 2e4 ? pageContent.content.slice(0, 2e4) + "\n[Content truncated...]" : pageContent.content;
7982
8300
  const runtimeState = runtime.getState();
7983
8301
  const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
8302
+ const activeTabTitle = pageContent.title || "(untitled)";
8303
+ const activeTabUrl = pageContent.url || activeWebContents.getURL();
8304
+ const allTabs = tabManager.getAllStates();
8305
+ const activeTabId = tabManager.getActiveTabId();
8306
+ const tabSummary = allTabs.length > 1 ? `
8307
+ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.title || "New Tab"} (${t.url})`).join(" | ")}` : "";
7984
8308
  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.
7985
8309
 
8310
+ THE USER IS CURRENTLY LOOKING AT:
8311
+ Title: ${activeTabTitle}
8312
+ URL: ${activeTabUrl}${tabSummary}
8313
+
8314
+ When the user says "this page", "this article", "this site", or asks about what they're viewing, they mean the page above. The content below is from that page — answer directly without needing to call read_page or current_tab first.
8315
+
7986
8316
  Current page context:
7987
8317
  ${structuredContext}
7988
8318
 
@@ -8013,7 +8343,9 @@ Instructions:
8013
8343
  - If the page context reports a rate limit, human verification, or access warning, stop using that page and switch to a different source.
8014
8344
  - Reference interactive elements by their index number (shown as [#N] in the listings above).
8015
8345
  - Be concise. Explain what you're doing as you go.
8016
- - For simple questions about the page, just answer directly without using tools.`;
8346
+ - For simple questions about the page, just answer directly without using tools.
8347
+ - You have a highlight tool that visually marks elements on the page for the user. Use it when the user asks you to highlight, mark, or draw attention to specific content. Colors: yellow (default), red (errors), green (success), blue (info), purple (important), orange (warnings).
8348
+ - 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.`;
8017
8349
  const actionCtx = { tabManager, runtime };
8018
8350
  await provider.streamAgentQuery(
8019
8351
  systemPrompt,
@@ -9130,6 +9462,17 @@ async function describeElementForClick(wc, selector) {
9130
9462
  };
9131
9463
  }
9132
9464
  async function clickResolvedSelector(wc, selector) {
9465
+ if (selector.startsWith("__vessel_idx:")) {
9466
+ const idx = Number(selector.slice("__vessel_idx:".length));
9467
+ const beforeUrl2 = wc.getURL();
9468
+ const result = await wc.executeJavaScript(
9469
+ `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`
9470
+ );
9471
+ if (typeof result === "string" && result.startsWith("Error")) return result;
9472
+ await waitForPotentialNavigation(wc, beforeUrl2);
9473
+ const afterUrl2 = wc.getURL();
9474
+ return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
9475
+ }
9133
9476
  const beforeUrl = wc.getURL();
9134
9477
  const elInfo = await describeElementForClick(wc, selector);
9135
9478
  if ("error" in elInfo) return `Error: ${elInfo.error}`;
@@ -9237,6 +9580,10 @@ async function dismissPopup(wc) {
9237
9580
  document.querySelectorAll("dialog, [role='dialog'], [role='alertdialog'], [aria-modal='true']").forEach((el) => {
9238
9581
  if (isVisible(el)) nodes.push(el);
9239
9582
  });
9583
+ // Detect known consent manager containers
9584
+ document.querySelectorAll("#onetrust-consent-sdk, #onetrust-banner-sdk, [id*='onetrust'], [class*='onetrust'], #CybotCookiebotDialog, #truste-consent-track, [id*='cookie-banner'], [id*='consent-banner'], [class*='cookie-consent'], [class*='consent-banner'], [id*='gdpr'], [class*='gdpr']").forEach((el) => {
9585
+ if (el instanceof HTMLElement && isVisible(el)) nodes.push(el);
9586
+ });
9240
9587
  document.querySelectorAll("body *").forEach((el) => {
9241
9588
  if (!(el instanceof HTMLElement) || !isVisible(el)) return;
9242
9589
  const style = window.getComputedStyle(el);
@@ -9266,14 +9613,17 @@ async function dismissPopup(wc) {
9266
9613
  el.textContent ||
9267
9614
  el.getAttribute("value"),
9268
9615
  ).toLowerCase();
9269
- const classText = text(el.className).toLowerCase();
9616
+ const classText = text(typeof el.className === "string" ? el.className : "").toLowerCase();
9270
9617
  const idText = text(el.id).toLowerCase();
9618
+ const combined = classText + " " + idText;
9271
9619
  let score = rooted ? 30 : 0;
9272
9620
  if (/^x$|^×$/.test(label)) score += 120;
9273
- if (/no thanks|no, thanks|not now|maybe later|dismiss|close|skip|cancel|continue without|no thank you/.test(label)) score += 100;
9274
- if (/close|dismiss|modal-close|overlay-close/.test(classText + " " + idText)) score += 90;
9621
+ if (/no thanks|no, thanks|not now|maybe later|dismiss|close|skip|cancel|continue without|no thank you|reject|decline/.test(label)) score += 100;
9622
+ if (/close|dismiss|modal-close|overlay-close/.test(combined)) score += 90;
9623
+ if (/onetrust-close|onetrust-reject|cookie.*close|consent.*close|cookie.*reject|consent.*reject/.test(combined)) score += 110;
9624
+ if (/onetrust-accept|cookie.*accept|consent.*accept/.test(combined)) score += 80;
9275
9625
  if (el.getAttribute("aria-label")) score += 20;
9276
- if (/accept|continue|submit|sign up|subscribe|join|start|next/.test(label)) score -= 80;
9626
+ if (/accept|continue|submit|sign up|subscribe|join|start|next/.test(label) && !/cookie|consent|onetrust/.test(combined)) score -= 80;
9277
9627
  const rect = el.getBoundingClientRect();
9278
9628
  if (rect.top < 120) score += 10;
9279
9629
  if (rect.right > (window.innerWidth || 0) - 120) score += 15;
@@ -9289,13 +9639,25 @@ async function dismissPopup(wc) {
9289
9639
  if (!(el instanceof HTMLElement) || !isVisible(el)) return;
9290
9640
  const candidateSelector = selectorFor(el);
9291
9641
  if (!candidateSelector) return;
9292
- const label = text(
9642
+ var label = text(
9293
9643
  el.getAttribute("aria-label") ||
9294
9644
  el.getAttribute("title") ||
9295
9645
  el.textContent ||
9296
9646
  el.getAttribute("value"),
9297
9647
  );
9298
- if (!label) return;
9648
+ if (!label) {
9649
+ var idLower = (el.id || "").toLowerCase();
9650
+ var classLower = (typeof el.className === "string" ? el.className : "").toLowerCase();
9651
+ var combined = idLower + " " + classLower;
9652
+ if (/onetrust|consent|cookie|banner|gdpr|trustarc|cookiebot/.test(combined)) {
9653
+ label = idLower.includes("accept") ? "Accept cookies"
9654
+ : idLower.includes("reject") ? "Reject cookies"
9655
+ : idLower.includes("close") || classLower.includes("close") ? "Close"
9656
+ : "Consent button";
9657
+ } else {
9658
+ return;
9659
+ }
9660
+ }
9299
9661
  results.push({
9300
9662
  selector: candidateSelector,
9301
9663
  label: label.slice(0, 120),
@@ -9362,7 +9724,11 @@ function isDangerousAction(name) {
9362
9724
  "create_tab",
9363
9725
  "switch_tab",
9364
9726
  "close_tab",
9365
- "restore_checkpoint"
9727
+ "restore_checkpoint",
9728
+ "login",
9729
+ "fill_form",
9730
+ "search",
9731
+ "paginate"
9366
9732
  ].includes(name);
9367
9733
  }
9368
9734
  function getTabByMatch(tabManager, match) {
@@ -9439,7 +9805,8 @@ async function withAction(runtime, tabManager, name, args, executor) {
9439
9805
  executor
9440
9806
  });
9441
9807
  const stateInfo = await getPostActionState(tabManager, name);
9442
- return asTextResponse(result + stateInfo);
9808
+ const flowCtx = runtime.getFlowContext();
9809
+ return asTextResponse(result + stateInfo + flowCtx);
9443
9810
  } catch (error) {
9444
9811
  return asTextResponse(
9445
9812
  `Error: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -9447,6 +9814,12 @@ async function withAction(runtime, tabManager, name, args, executor) {
9447
9814
  }
9448
9815
  }
9449
9816
  async function setElementValue(wc, selector, value) {
9817
+ if (selector.startsWith("__vessel_idx:")) {
9818
+ const idx = Number(selector.slice("__vessel_idx:".length));
9819
+ return wc.executeJavaScript(
9820
+ `window.__vessel?.interactByIndex?.(${idx}, "value", ${JSON.stringify(value)}) || "Error: interactByIndex not available"`
9821
+ );
9822
+ }
9450
9823
  return wc.executeJavaScript(`
9451
9824
  (function() {
9452
9825
  const el = document.querySelector(${JSON.stringify(selector)});
@@ -11794,6 +12167,382 @@ ${JSON.stringify(otherHighlights, null, 2)}`
11794
12167
  );
11795
12168
  }
11796
12169
  );
12170
+ server.registerTool(
12171
+ "vessel_flow_start",
12172
+ {
12173
+ title: "Start Workflow",
12174
+ description: "Begin tracking a multi-step web workflow. Vessel will show progress after every action so you always know where you are in the flow.",
12175
+ inputSchema: {
12176
+ goal: zod.z.string().describe("What this workflow accomplishes (e.g. 'Purchase item from Amazon')"),
12177
+ steps: zod.z.array(zod.z.string()).describe("Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])")
12178
+ }
12179
+ },
12180
+ async ({ goal, steps }) => {
12181
+ const tab = tabManager.getActiveTab();
12182
+ const flow = runtime.startFlow(goal, steps, tab?.view.webContents.getURL());
12183
+ return asTextResponse(
12184
+ `Flow started: ${flow.goal}
12185
+ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
12186
+ );
12187
+ }
12188
+ );
12189
+ server.registerTool(
12190
+ "vessel_flow_advance",
12191
+ {
12192
+ title: "Advance Workflow Step",
12193
+ description: "Mark the current workflow step as done and move to the next one. Call this after completing each step.",
12194
+ inputSchema: {
12195
+ detail: zod.z.string().optional().describe("Brief note about what was accomplished")
12196
+ }
12197
+ },
12198
+ async ({ detail }) => {
12199
+ const flow = runtime.advanceFlow(detail);
12200
+ if (!flow) return asTextResponse("No active flow to advance");
12201
+ const ctx = runtime.getFlowContext();
12202
+ return asTextResponse(`Step completed.${ctx}`);
12203
+ }
12204
+ );
12205
+ server.registerTool(
12206
+ "vessel_flow_status",
12207
+ {
12208
+ title: "Workflow Status",
12209
+ description: "Check the current workflow progress."
12210
+ },
12211
+ async () => {
12212
+ const flow = runtime.getFlowState();
12213
+ if (!flow) return asTextResponse("No active workflow.");
12214
+ return asTextResponse(runtime.getFlowContext());
12215
+ }
12216
+ );
12217
+ server.registerTool(
12218
+ "vessel_flow_end",
12219
+ {
12220
+ title: "End Workflow",
12221
+ description: "Clear the active workflow tracker."
12222
+ },
12223
+ async () => {
12224
+ runtime.clearFlow();
12225
+ return asTextResponse("Workflow ended.");
12226
+ }
12227
+ );
12228
+ server.registerTool(
12229
+ "vessel_suggest",
12230
+ {
12231
+ title: "What Should I Do?",
12232
+ description: "Analyze the current page and return the most relevant tools and suggested next actions. Call this when you're unsure what to do next — it reads the page context and tells you the optimal approach."
12233
+ },
12234
+ async () => {
12235
+ const tab = tabManager.getActiveTab();
12236
+ if (!tab) return asTextResponse("No active tab. Use vessel_navigate to open a page.");
12237
+ const wc = tab.view.webContents;
12238
+ let page;
12239
+ try {
12240
+ page = await extractContent(wc);
12241
+ } catch {
12242
+ return asTextResponse("Could not read page. Try vessel_navigate to a working URL.");
12243
+ }
12244
+ const suggestions = [];
12245
+ suggestions.push(`Page: ${page.title || "(untitled)"}`);
12246
+ suggestions.push(`URL: ${page.url}`);
12247
+ suggestions.push("");
12248
+ const flowCtx = runtime.getFlowContext();
12249
+ if (flowCtx) {
12250
+ suggestions.push(flowCtx);
12251
+ suggestions.push("");
12252
+ }
12253
+ page.url.toLowerCase();
12254
+ const hasPasswordField = page.forms.some(
12255
+ (f) => f.fields.some((el) => el.inputType === "password")
12256
+ );
12257
+ const hasSearchInput = page.interactiveElements.some(
12258
+ (el) => el.inputType === "search" || el.name === "q" || el.name === "query" || (el.placeholder || "").toLowerCase().includes("search")
12259
+ );
12260
+ const formCount = page.forms.length;
12261
+ const totalFields = page.forms.reduce((n, f) => n + f.fields.length, 0);
12262
+ const linkCount = page.interactiveElements.filter((el) => el.type === "link").length;
12263
+ const hasPagination = page.interactiveElements.some(
12264
+ (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»"
12265
+ );
12266
+ const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
12267
+ if (hasOverlays) {
12268
+ suggestions.push("⚠ BLOCKING OVERLAY detected — dismiss it first:");
12269
+ suggestions.push(" → vessel_dismiss_popup or vessel_click on close/accept button");
12270
+ suggestions.push("");
12271
+ }
12272
+ if (hasPasswordField) {
12273
+ suggestions.push("🔑 LOGIN PAGE detected:");
12274
+ suggestions.push(" → vessel_login(username, password) — handles the full flow");
12275
+ suggestions.push(" → Or vessel_fill_form + vessel_submit_form for manual control");
12276
+ } else if (hasSearchInput && linkCount < 10) {
12277
+ suggestions.push("🔍 SEARCH PAGE detected:");
12278
+ suggestions.push(" → vessel_search(query) — finds the box, types, submits");
12279
+ } else if (hasSearchInput && linkCount >= 10) {
12280
+ suggestions.push("📋 SEARCH RESULTS detected:");
12281
+ suggestions.push(" → vessel_click on a result link");
12282
+ if (hasPagination) {
12283
+ suggestions.push(" → vessel_paginate('next') for more results");
12284
+ }
12285
+ } else if (formCount > 0) {
12286
+ suggestions.push(`📝 FORM detected (${totalFields} fields):`);
12287
+ suggestions.push(" → vessel_fill_form(fields) — fill all fields at once");
12288
+ suggestions.push(" → Or vessel_type for individual fields");
12289
+ } else if (hasPagination) {
12290
+ suggestions.push("📄 PAGINATED CONTENT:");
12291
+ suggestions.push(" → vessel_extract_content to read this page");
12292
+ suggestions.push(" → vessel_paginate('next') for the next page");
12293
+ } else if (page.content.length > 3e3 && page.interactiveElements.length < 10) {
12294
+ suggestions.push("📖 ARTICLE/CONTENT page:");
12295
+ suggestions.push(" → vessel_extract_content for readable text");
12296
+ suggestions.push(" → vessel_scroll to see more");
12297
+ } else {
12298
+ suggestions.push("🌐 GENERAL PAGE:");
12299
+ suggestions.push(" → vessel_extract_content to understand the page structure");
12300
+ suggestions.push(" → vessel_click on any element by index");
12301
+ suggestions.push(" → vessel_navigate to go somewhere new");
12302
+ }
12303
+ suggestions.push("");
12304
+ suggestions.push(`Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`);
12305
+ return asTextResponse(suggestions.join("\n"));
12306
+ }
12307
+ );
12308
+ server.registerTool(
12309
+ "vessel_fill_form",
12310
+ {
12311
+ title: "Fill Form",
12312
+ description: "Fill multiple form fields at once. Provide a map of field identifiers to values. Fields are matched by index, name, label, or placeholder. Much faster than calling type for each field individually.",
12313
+ inputSchema: {
12314
+ fields: zod.z.array(
12315
+ zod.z.object({
12316
+ index: zod.z.number().optional().describe("Element index from page content"),
12317
+ selector: zod.z.string().optional().describe("CSS selector fallback"),
12318
+ value: zod.z.string().describe("Value to enter")
12319
+ })
12320
+ ).describe("Fields to fill"),
12321
+ submit: zod.z.boolean().optional().describe("Submit the form after filling (default false)")
12322
+ }
12323
+ },
12324
+ async ({ fields, submit }) => {
12325
+ const tab = tabManager.getActiveTab();
12326
+ if (!tab) return asTextResponse("Error: No active tab");
12327
+ return withAction(
12328
+ runtime,
12329
+ tabManager,
12330
+ "fill_form",
12331
+ { fieldCount: fields.length, submit },
12332
+ async () => {
12333
+ const wc = tab.view.webContents;
12334
+ const results = [];
12335
+ for (const field of fields) {
12336
+ const sel = await resolveSelector(wc, field.index, field.selector);
12337
+ if (!sel) {
12338
+ results.push(`Skipped: no selector for index=${field.index}`);
12339
+ continue;
12340
+ }
12341
+ const result = await setElementValue(wc, sel, field.value);
12342
+ results.push(result);
12343
+ }
12344
+ if (submit) {
12345
+ const firstSel = await resolveSelector(wc, fields[0]?.index, fields[0]?.selector);
12346
+ if (firstSel) {
12347
+ const beforeUrl = wc.getURL();
12348
+ const submitResult = await submitForm(wc, void 0, firstSel);
12349
+ await waitForPotentialNavigation(wc, beforeUrl);
12350
+ const afterUrl = wc.getURL();
12351
+ results.push(
12352
+ afterUrl !== beforeUrl ? `Submitted → ${afterUrl}` : submitResult
12353
+ );
12354
+ }
12355
+ }
12356
+ return `Filled ${results.length} field(s):
12357
+ ${results.join("\n")}`;
12358
+ }
12359
+ );
12360
+ }
12361
+ );
12362
+ server.registerTool(
12363
+ "vessel_login",
12364
+ {
12365
+ title: "Login",
12366
+ description: "Compound action: navigate to a login page, fill credentials, and submit. Handles the full login flow in one call.",
12367
+ inputSchema: {
12368
+ url: zod.z.string().optional().describe("Login page URL (skip if already on login page)"),
12369
+ username: zod.z.string().describe("Username or email"),
12370
+ password: zod.z.string().describe("Password"),
12371
+ username_selector: zod.z.string().optional().describe("CSS selector for username field (auto-detected if omitted)"),
12372
+ password_selector: zod.z.string().optional().describe("CSS selector for password field (auto-detected if omitted)"),
12373
+ submit_selector: zod.z.string().optional().describe("CSS selector for submit button (auto-detected if omitted)")
12374
+ }
12375
+ },
12376
+ async ({ url, username, password, username_selector, password_selector, submit_selector }) => {
12377
+ const tab = tabManager.getActiveTab();
12378
+ if (!tab) return asTextResponse("Error: No active tab");
12379
+ return withAction(
12380
+ runtime,
12381
+ tabManager,
12382
+ "login",
12383
+ { url, username: username.slice(0, 3) + "***" },
12384
+ async () => {
12385
+ const wc = tab.view.webContents;
12386
+ const steps = [];
12387
+ if (url) {
12388
+ const id = tabManager.getActiveTabId();
12389
+ tabManager.navigateTab(id, url);
12390
+ await waitForLoad(wc);
12391
+ steps.push(`Navigated to ${wc.getURL()}`);
12392
+ }
12393
+ const userSel = username_selector || await wc.executeJavaScript(`
12394
+ (function() {
12395
+ var el = document.querySelector('input[type="email"], input[name="email"], input[name="username"], input[name="user"], input[autocomplete="username"], input[autocomplete="email"], input[type="text"]:not([name="search"]):not([name="q"])');
12396
+ return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
12397
+ })()
12398
+ `);
12399
+ if (!userSel) return "Error: Could not find username/email field. Try providing username_selector.";
12400
+ const passSel = password_selector || await wc.executeJavaScript(`
12401
+ (function() {
12402
+ var el = document.querySelector('input[type="password"]');
12403
+ return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
12404
+ })()
12405
+ `);
12406
+ if (!passSel) return "Error: Could not find password field. Try providing password_selector.";
12407
+ const userResult = await setElementValue(wc, userSel, username);
12408
+ steps.push(userResult);
12409
+ const passResult = await setElementValue(wc, passSel, password);
12410
+ steps.push(passResult);
12411
+ const beforeUrl = wc.getURL();
12412
+ if (submit_selector) {
12413
+ await clickResolvedSelector(wc, submit_selector);
12414
+ } else {
12415
+ const clicked = await wc.executeJavaScript(`
12416
+ (function() {
12417
+ var btn = document.querySelector('button[type="submit"], input[type="submit"], form button:not([type="button"])');
12418
+ if (btn) { btn.click(); return true; }
12419
+ var form = document.querySelector('input[type="password"]')?.closest('form');
12420
+ if (form) { form.requestSubmit ? form.requestSubmit() : form.submit(); return true; }
12421
+ return false;
12422
+ })()
12423
+ `);
12424
+ if (!clicked) return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
12425
+ }
12426
+ await waitForPotentialNavigation(wc, beforeUrl);
12427
+ const afterUrl = wc.getURL();
12428
+ steps.push(
12429
+ afterUrl !== beforeUrl ? `Submitted → ${afterUrl}` : "Form submitted (same page)"
12430
+ );
12431
+ return `Login flow complete:
12432
+ ${steps.join("\n")}`;
12433
+ }
12434
+ );
12435
+ }
12436
+ );
12437
+ server.registerTool(
12438
+ "vessel_search",
12439
+ {
12440
+ title: "Search",
12441
+ description: "Compound action: find a search box on the current page, type a query, and submit. Returns the resulting page state.",
12442
+ inputSchema: {
12443
+ query: zod.z.string().describe("Search query text"),
12444
+ selector: zod.z.string().optional().describe("CSS selector for search input (auto-detected if omitted)")
12445
+ }
12446
+ },
12447
+ async ({ query, selector }) => {
12448
+ const tab = tabManager.getActiveTab();
12449
+ if (!tab) return asTextResponse("Error: No active tab");
12450
+ return withAction(
12451
+ runtime,
12452
+ tabManager,
12453
+ "search",
12454
+ { query },
12455
+ async () => {
12456
+ const wc = tab.view.webContents;
12457
+ const searchSel = selector || await wc.executeJavaScript(`
12458
+ (function() {
12459
+ var el = document.querySelector('input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]');
12460
+ if (!el) {
12461
+ var inputs = document.querySelectorAll('input[type="text"]');
12462
+ for (var i = 0; i < inputs.length; i++) {
12463
+ var form = inputs[i].closest('form');
12464
+ if (form && (form.getAttribute('role') === 'search' || form.action?.includes('search'))) {
12465
+ el = inputs[i];
12466
+ break;
12467
+ }
12468
+ }
12469
+ }
12470
+ return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
12471
+ })()
12472
+ `);
12473
+ if (!searchSel) return "Error: Could not find search input. Try providing a selector.";
12474
+ await setElementValue(wc, searchSel, query);
12475
+ await wc.executeJavaScript(`
12476
+ (function() {
12477
+ var el = document.querySelector(${JSON.stringify(searchSel)});
12478
+ if (el) el.focus();
12479
+ })()
12480
+ `);
12481
+ await new Promise((r) => setTimeout(r, 50));
12482
+ const beforeUrl = wc.getURL();
12483
+ wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
12484
+ await new Promise((r) => setTimeout(r, 16));
12485
+ wc.sendInputEvent({ type: "keyUp", keyCode: "Return" });
12486
+ await waitForPotentialNavigation(wc, beforeUrl);
12487
+ const afterUrl = wc.getURL();
12488
+ return afterUrl !== beforeUrl ? `Searched "${query}" → ${afterUrl}` : `Searched "${query}" (same page — results may have loaded dynamically)`;
12489
+ }
12490
+ );
12491
+ }
12492
+ );
12493
+ server.registerTool(
12494
+ "vessel_paginate",
12495
+ {
12496
+ title: "Paginate",
12497
+ description: "Navigate to the next or previous page of results. Auto-detects pagination controls.",
12498
+ inputSchema: {
12499
+ direction: zod.z.enum(["next", "prev"]).describe("Pagination direction"),
12500
+ selector: zod.z.string().optional().describe("CSS selector for the pagination link (auto-detected if omitted)")
12501
+ }
12502
+ },
12503
+ async ({ direction, selector }) => {
12504
+ const tab = tabManager.getActiveTab();
12505
+ if (!tab) return asTextResponse("Error: No active tab");
12506
+ return withAction(
12507
+ runtime,
12508
+ tabManager,
12509
+ "paginate",
12510
+ { direction },
12511
+ async () => {
12512
+ const wc = tab.view.webContents;
12513
+ const beforeUrl = wc.getURL();
12514
+ if (selector) {
12515
+ return clickResolvedSelector(wc, selector);
12516
+ }
12517
+ const isNext = direction === "next";
12518
+ const clicked = await wc.executeJavaScript(`
12519
+ (function() {
12520
+ var patterns = ${isNext ? '["next", "Next", "›", "»", "→", ">", "Next Page", "Load More"]' : '["prev", "Prev", "Previous", "‹", "«", "←", "<", "Previous Page"]'};
12521
+ var links = document.querySelectorAll('a, button');
12522
+ for (var i = 0; i < links.length; i++) {
12523
+ var el = links[i];
12524
+ var text = (el.textContent || '').trim();
12525
+ var ariaLabel = (el.getAttribute('aria-label') || '').toLowerCase();
12526
+ var rel = (el.getAttribute('rel') || '').toLowerCase();
12527
+ if (rel === '${isNext ? "next" : "prev"}') { el.click(); return true; }
12528
+ for (var j = 0; j < patterns.length; j++) {
12529
+ if (text === patterns[j] || ariaLabel.includes(patterns[j].toLowerCase())) {
12530
+ el.click();
12531
+ return true;
12532
+ }
12533
+ }
12534
+ }
12535
+ return false;
12536
+ })()
12537
+ `);
12538
+ if (!clicked) return `Error: Could not find ${direction} pagination control. Try providing a selector.`;
12539
+ await waitForPotentialNavigation(wc, beforeUrl);
12540
+ const afterUrl = wc.getURL();
12541
+ return afterUrl !== beforeUrl ? `Paginated ${direction} → ${afterUrl}` : `Clicked ${direction} (page may have updated dynamically)`;
12542
+ }
12543
+ );
12544
+ }
12545
+ );
11797
12546
  }
11798
12547
  function waitForLoad(wc, timeout = 1e4) {
11799
12548
  return new Promise((resolve) => {
@@ -11843,7 +12592,11 @@ async function resolveSelector(wc, index, selector) {
11843
12592
  `
11844
12593
  );
11845
12594
  if (typeof authoritativeSelector === "string" && authoritativeSelector) {
11846
- return authoritativeSelector;
12595
+ const resolves = await wc.executeJavaScript(
12596
+ `!!document.querySelector(${JSON.stringify(authoritativeSelector)})`
12597
+ );
12598
+ if (resolves) return authoritativeSelector;
12599
+ return `__vessel_idx:${index}`;
11847
12600
  }
11848
12601
  const page = await extractContent(wc);
11849
12602
  const extractedSelector = findSelectorByIndex(page, index);
@@ -12396,7 +13149,8 @@ function sanitizePersistence(persisted) {
12396
13149
  actions: Array.isArray(persisted?.actions) ? persisted.actions.slice(-120) : [],
12397
13150
  checkpoints: Array.isArray(persisted?.checkpoints) ? persisted.checkpoints.slice(-20) : [],
12398
13151
  transcript: [],
12399
- mcpStatus: "stopped"
13152
+ mcpStatus: "stopped",
13153
+ flowState: null
12400
13154
  };
12401
13155
  }
12402
13156
  class AgentRuntime {
@@ -12515,6 +13269,67 @@ class AgentRuntime {
12515
13269
  this.emit();
12516
13270
  return this.getState();
12517
13271
  }
13272
+ // --- Speedee Flow State ---
13273
+ startFlow(goal, steps, startUrl) {
13274
+ const flow = {
13275
+ id: node_crypto.randomUUID(),
13276
+ goal,
13277
+ steps: steps.map((label) => ({ label, status: "pending" })),
13278
+ currentStepIndex: 0,
13279
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
13280
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
13281
+ startUrl
13282
+ };
13283
+ this.state.flowState = flow;
13284
+ this.emit();
13285
+ return clone(flow);
13286
+ }
13287
+ advanceFlow(detail) {
13288
+ const flow = this.state.flowState;
13289
+ if (!flow) return null;
13290
+ const step = flow.steps[flow.currentStepIndex];
13291
+ if (step) {
13292
+ step.status = "done";
13293
+ step.detail = detail;
13294
+ }
13295
+ flow.currentStepIndex = Math.min(flow.currentStepIndex + 1, flow.steps.length);
13296
+ flow.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
13297
+ this.emit();
13298
+ return clone(flow);
13299
+ }
13300
+ failFlowStep(detail) {
13301
+ const flow = this.state.flowState;
13302
+ if (!flow) return null;
13303
+ const step = flow.steps[flow.currentStepIndex];
13304
+ if (step) {
13305
+ step.status = "failed";
13306
+ step.detail = detail;
13307
+ }
13308
+ flow.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
13309
+ this.emit();
13310
+ return clone(flow);
13311
+ }
13312
+ getFlowState() {
13313
+ return this.state.flowState ? clone(this.state.flowState) : null;
13314
+ }
13315
+ clearFlow() {
13316
+ this.state.flowState = null;
13317
+ this.emit();
13318
+ }
13319
+ getFlowContext() {
13320
+ const flow = this.state.flowState;
13321
+ if (!flow) return "";
13322
+ const progress = flow.steps.map((s, i) => {
13323
+ const marker = s.status === "done" ? "✓" : s.status === "failed" ? "✗" : s.status === "skipped" ? "-" : i === flow.currentStepIndex ? "→" : " ";
13324
+ const detail = s.detail ? ` (${s.detail})` : "";
13325
+ return `[${marker}] ${s.label}${detail}`;
13326
+ }).join("\n");
13327
+ return `
13328
+ --- Active Flow ---
13329
+ Goal: ${flow.goal}
13330
+ ${progress}
13331
+ ---`;
13332
+ }
12518
13333
  async runControlledAction({
12519
13334
  source,
12520
13335
  name,