@quanta-intellect/vessel-browser 0.1.11 → 0.1.12

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
@@ -62,25 +62,15 @@ class Tab {
62
62
  isReaderMode: false,
63
63
  adBlockingEnabled: options?.adBlockingEnabled ?? true
64
64
  };
65
- this.view.webContents.on("before-input-event", (event, input) => {
65
+ this.view.webContents.on("before-input-event", (_event, input) => {
66
66
  if (!input.control && !input.meta) return;
67
+ if (input.type !== "keyDown") return;
67
68
  const key = input.key.toLowerCase();
68
69
  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
- }
70
+ if (key === "c") wc.copy();
71
+ else if (key === "v") wc.paste();
72
+ else if (key === "x") wc.cut();
73
+ else if (key === "a") wc.selectAll();
84
74
  });
85
75
  this.setupListeners();
86
76
  if (url) {
@@ -732,26 +722,46 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
732
722
  var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};
733
723
  // Collect matching text nodes first, then wrap — avoids TreeWalker
734
724
  // seeing newly created nodes from surroundContents and re-matching.
735
- var textNodes = [];
736
- var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
737
- acceptNode: function(node) {
738
- var p = node.parentElement;
739
- if (!p) return NodeFilter.FILTER_REJECT;
740
- if (SKIP_TAGS[p.tagName]) return NodeFilter.FILTER_REJECT;
741
- if (p.closest('[data-vessel-highlight]')) return NodeFilter.FILTER_REJECT;
742
- var style = window.getComputedStyle(p);
743
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return NodeFilter.FILTER_REJECT;
744
- if (p.offsetWidth === 0 && p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;
745
- return NodeFilter.FILTER_ACCEPT;
746
- }
747
- });
748
- var node;
749
- while ((node = walker.nextNode())) {
750
- var idx = node.textContent.indexOf(searchText);
751
- if (idx !== -1) {
752
- textNodes.push({ node: node, idx: idx });
753
- if (textNodes.length >= 20) break;
725
+ // Prioritize main content area over nav/sidebar/captions.
726
+ var contentRoots = ['main', 'article', '[role="main"]', '#mw-content-text', '.mw-parser-output', '#content', '.post-content', '.entry-content', '.article-body'];
727
+ var contentRoot = null;
728
+ for (var cr = 0; cr < contentRoots.length; cr++) {
729
+ contentRoot = document.querySelector(contentRoots[cr]);
730
+ if (contentRoot) break;
731
+ }
732
+ var NAV_ANCESTORS = 'nav, aside, footer, header, [role="navigation"], [role="complementary"], .sidebar, .navbox, .infobox, figcaption, .thumbcaption, .mw-jump-link';
733
+
734
+ function collectMatches(root, limit) {
735
+ var matches = [];
736
+ var w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
737
+ acceptNode: function(n) {
738
+ var p = n.parentElement;
739
+ if (!p) return NodeFilter.FILTER_REJECT;
740
+ if (SKIP_TAGS[p.tagName]) return NodeFilter.FILTER_REJECT;
741
+ if (p.closest('[data-vessel-highlight]')) return NodeFilter.FILTER_REJECT;
742
+ if (p.closest(NAV_ANCESTORS)) return NodeFilter.FILTER_REJECT;
743
+ var style = window.getComputedStyle(p);
744
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return NodeFilter.FILTER_REJECT;
745
+ if (p.offsetWidth === 0 && p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;
746
+ return NodeFilter.FILTER_ACCEPT;
747
+ }
748
+ });
749
+ var n;
750
+ while ((n = w.nextNode())) {
751
+ var idx = n.textContent.indexOf(searchText);
752
+ if (idx !== -1) {
753
+ matches.push({ node: n, idx: idx });
754
+ if (matches.length >= limit) break;
755
+ }
754
756
  }
757
+ return matches;
758
+ }
759
+
760
+ // First try: match inside main content area (skip nav/sidebar/captions)
761
+ var textNodes = contentRoot ? collectMatches(contentRoot, 20) : [];
762
+ // Fallback: if no matches in content area, search the whole body
763
+ if (textNodes.length === 0) {
764
+ textNodes = collectMatches(document.body, 20);
755
765
  }
756
766
  var count = 0;
757
767
  var firstMark = null;
@@ -1946,6 +1956,74 @@ function setSetting(key, value) {
1946
1956
  saveSettings();
1947
1957
  return { ...settings };
1948
1958
  }
1959
+ const Channels = {
1960
+ // Tab management
1961
+ TAB_CREATE: "tab:create",
1962
+ TAB_CLOSE: "tab:close",
1963
+ TAB_SWITCH: "tab:switch",
1964
+ TAB_NAVIGATE: "tab:navigate",
1965
+ TAB_BACK: "tab:back",
1966
+ TAB_FORWARD: "tab:forward",
1967
+ TAB_RELOAD: "tab:reload",
1968
+ TAB_STATE_UPDATE: "tab:state-update",
1969
+ // AI
1970
+ AI_QUERY: "ai:query",
1971
+ AI_STREAM_START: "ai:stream-start",
1972
+ AI_STREAM_CHUNK: "ai:stream-chunk",
1973
+ AI_STREAM_END: "ai:stream-end",
1974
+ AI_CANCEL: "ai:cancel",
1975
+ AI_FETCH_MODELS: "ai:fetch-models",
1976
+ AGENT_RUNTIME_GET: "agent-runtime:get",
1977
+ AGENT_RUNTIME_UPDATE: "agent-runtime:update",
1978
+ AGENT_PAUSE: "agent:pause",
1979
+ AGENT_RESUME: "agent:resume",
1980
+ AGENT_SET_APPROVAL_MODE: "agent:set-approval-mode",
1981
+ AGENT_APPROVAL_RESOLVE: "agent:approval-resolve",
1982
+ AGENT_CHECKPOINT_CREATE: "agent:checkpoint-create",
1983
+ AGENT_CHECKPOINT_RESTORE: "agent:checkpoint-restore",
1984
+ AGENT_SESSION_CAPTURE: "agent:session-capture",
1985
+ AGENT_SESSION_RESTORE: "agent:session-restore",
1986
+ // Content
1987
+ CONTENT_EXTRACT: "content:extract",
1988
+ READER_MODE_TOGGLE: "reader:toggle",
1989
+ // UI state
1990
+ SIDEBAR_TOGGLE: "ui:sidebar-toggle",
1991
+ SIDEBAR_RESIZE: "ui:sidebar-resize",
1992
+ SIDEBAR_RESIZE_START: "ui:sidebar-resize-start",
1993
+ SIDEBAR_RESIZE_COMMIT: "ui:sidebar-resize-commit",
1994
+ SIDEBAR_CONTEXT_MENU: "ui:sidebar-context-menu",
1995
+ FOCUS_MODE_TOGGLE: "ui:focus-mode-toggle",
1996
+ SETTINGS_VISIBILITY: "ui:settings-visibility",
1997
+ // Settings
1998
+ SETTINGS_GET: "settings:get",
1999
+ SETTINGS_SET: "settings:set",
2000
+ SETTINGS_UPDATE: "settings:update",
2001
+ SETTINGS_HEALTH_GET: "settings:health:get",
2002
+ // Bookmarks
2003
+ BOOKMARKS_GET: "bookmarks:get",
2004
+ BOOKMARKS_UPDATE: "bookmarks:update",
2005
+ BOOKMARK_SAVE: "bookmarks:save",
2006
+ BOOKMARK_REMOVE: "bookmarks:remove",
2007
+ FOLDER_CREATE: "bookmarks:folder-create",
2008
+ FOLDER_REMOVE: "bookmarks:folder-remove",
2009
+ FOLDER_RENAME: "bookmarks:folder-rename",
2010
+ // Highlights
2011
+ HIGHLIGHT_CAPTURE: "highlights:capture",
2012
+ HIGHLIGHT_CAPTURE_RESULT: "highlights:capture-result",
2013
+ HIGHLIGHT_SELECTION: "vessel:highlight-selection",
2014
+ HIGHLIGHT_NAV_COUNT: "highlights:nav-count",
2015
+ HIGHLIGHT_NAV_SCROLL: "highlights:nav-scroll",
2016
+ HIGHLIGHT_NAV_REMOVE: "highlights:nav-remove",
2017
+ HIGHLIGHT_NAV_CLEAR: "highlights:nav-clear",
2018
+ SIDEBAR_HIGHLIGHT_ACTION: "highlights:sidebar-action",
2019
+ // DevTools panel
2020
+ DEVTOOLS_PANEL_TOGGLE: "devtools-panel:toggle",
2021
+ DEVTOOLS_PANEL_STATE: "devtools-panel:state",
2022
+ // Window controls
2023
+ WINDOW_MINIMIZE: "window:minimize",
2024
+ WINDOW_MAXIMIZE: "window:maximize",
2025
+ WINDOW_CLOSE: "window:close"
2026
+ };
1949
2027
  function enableClipboardShortcuts(view) {
1950
2028
  view.webContents.on("before-input-event", (event, input) => {
1951
2029
  if (!input.control && !input.meta) return;
@@ -1972,6 +2050,102 @@ const CHROME_HEIGHT = 110;
1972
2050
  const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
1973
2051
  const MIN_DEVTOOLS_PANEL = 120;
1974
2052
  const MAX_DEVTOOLS_PANEL = 600;
2053
+ async function getSidebarContextTarget(sidebarView, x, y) {
2054
+ try {
2055
+ return await sidebarView.webContents.executeJavaScript(
2056
+ `(() => {
2057
+ const el = document.elementFromPoint(${x}, ${y});
2058
+ const nav = el && typeof el.closest === "function"
2059
+ ? el.closest(".highlight-nav")
2060
+ : null;
2061
+ const label = nav?.querySelector(".highlight-nav-label")?.textContent?.trim() || "";
2062
+ return {
2063
+ inHighlightNav: !!nav,
2064
+ canRemoveCurrent: /\\d+\\s*\\/\\s*\\d+/.test(label),
2065
+ };
2066
+ })()`,
2067
+ true
2068
+ );
2069
+ } catch {
2070
+ return { inHighlightNav: false, canRemoveCurrent: false };
2071
+ }
2072
+ }
2073
+ async function showSidebarContextMenu(mainWindow, sidebarView, params) {
2074
+ const target = await getSidebarContextTarget(sidebarView, params.x, params.y);
2075
+ const menu = new electron.Menu();
2076
+ if (target.inHighlightNav) {
2077
+ if (target.canRemoveCurrent) {
2078
+ menu.append(
2079
+ new electron.MenuItem({
2080
+ label: "Remove Current Highlight",
2081
+ click: () => sidebarView.webContents.send(
2082
+ Channels.SIDEBAR_HIGHLIGHT_ACTION,
2083
+ "remove-current"
2084
+ )
2085
+ })
2086
+ );
2087
+ }
2088
+ menu.append(
2089
+ new electron.MenuItem({
2090
+ label: "Clear All Highlights",
2091
+ click: () => sidebarView.webContents.send(
2092
+ Channels.SIDEBAR_HIGHLIGHT_ACTION,
2093
+ "clear-all"
2094
+ )
2095
+ })
2096
+ );
2097
+ }
2098
+ if (params.isEditable) {
2099
+ if (menu.items.length > 0) {
2100
+ menu.append(new electron.MenuItem({ type: "separator" }));
2101
+ }
2102
+ menu.append(
2103
+ new electron.MenuItem({
2104
+ role: "undo",
2105
+ enabled: params.editFlags.canUndo
2106
+ })
2107
+ );
2108
+ menu.append(
2109
+ new electron.MenuItem({
2110
+ role: "redo",
2111
+ enabled: params.editFlags.canRedo
2112
+ })
2113
+ );
2114
+ menu.append(new electron.MenuItem({ type: "separator" }));
2115
+ menu.append(
2116
+ new electron.MenuItem({
2117
+ role: "cut",
2118
+ enabled: params.editFlags.canCut
2119
+ })
2120
+ );
2121
+ menu.append(
2122
+ new electron.MenuItem({
2123
+ role: "copy",
2124
+ enabled: params.editFlags.canCopy
2125
+ })
2126
+ );
2127
+ menu.append(
2128
+ new electron.MenuItem({
2129
+ role: "paste",
2130
+ enabled: params.editFlags.canPaste
2131
+ })
2132
+ );
2133
+ menu.append(
2134
+ new electron.MenuItem({
2135
+ role: "selectAll",
2136
+ enabled: params.editFlags.canSelectAll
2137
+ })
2138
+ );
2139
+ } else if (params.selectionText?.trim()) {
2140
+ if (menu.items.length > 0) {
2141
+ menu.append(new electron.MenuItem({ type: "separator" }));
2142
+ }
2143
+ menu.append(new electron.MenuItem({ role: "copy" }));
2144
+ }
2145
+ if (menu.items.length === 0) return;
2146
+ sidebarView.webContents.focus();
2147
+ menu.popup({ window: mainWindow });
2148
+ }
1975
2149
  function getWindowIconPath() {
1976
2150
  const candidates = [
1977
2151
  path.join(electron.app.getAppPath(), "resources", "vessel-icon.png"),
@@ -2009,6 +2183,10 @@ function createMainWindow(onTabStateChange) {
2009
2183
  }
2010
2184
  });
2011
2185
  sidebarView.setBackgroundColor("#00000000");
2186
+ sidebarView.webContents.on("context-menu", (event, params) => {
2187
+ event.preventDefault();
2188
+ void showSidebarContextMenu(mainWindow, sidebarView, params);
2189
+ });
2012
2190
  mainWindow.contentView.addChildView(sidebarView);
2013
2191
  const devtoolsPanelView = new electron.WebContentsView({
2014
2192
  webPreferences: {
@@ -2097,68 +2275,6 @@ function layoutViews(state2) {
2097
2275
  });
2098
2276
  }
2099
2277
  }
2100
- const Channels = {
2101
- // Tab management
2102
- TAB_CREATE: "tab:create",
2103
- TAB_CLOSE: "tab:close",
2104
- TAB_SWITCH: "tab:switch",
2105
- TAB_NAVIGATE: "tab:navigate",
2106
- TAB_BACK: "tab:back",
2107
- TAB_FORWARD: "tab:forward",
2108
- TAB_RELOAD: "tab:reload",
2109
- TAB_STATE_UPDATE: "tab:state-update",
2110
- // AI
2111
- AI_QUERY: "ai:query",
2112
- AI_STREAM_START: "ai:stream-start",
2113
- AI_STREAM_CHUNK: "ai:stream-chunk",
2114
- AI_STREAM_END: "ai:stream-end",
2115
- AI_CANCEL: "ai:cancel",
2116
- AI_FETCH_MODELS: "ai:fetch-models",
2117
- AGENT_RUNTIME_GET: "agent-runtime:get",
2118
- AGENT_RUNTIME_UPDATE: "agent-runtime:update",
2119
- AGENT_PAUSE: "agent:pause",
2120
- AGENT_RESUME: "agent:resume",
2121
- AGENT_SET_APPROVAL_MODE: "agent:set-approval-mode",
2122
- AGENT_APPROVAL_RESOLVE: "agent:approval-resolve",
2123
- AGENT_CHECKPOINT_CREATE: "agent:checkpoint-create",
2124
- AGENT_CHECKPOINT_RESTORE: "agent:checkpoint-restore",
2125
- AGENT_SESSION_CAPTURE: "agent:session-capture",
2126
- AGENT_SESSION_RESTORE: "agent:session-restore",
2127
- // Content
2128
- CONTENT_EXTRACT: "content:extract",
2129
- READER_MODE_TOGGLE: "reader:toggle",
2130
- // UI state
2131
- SIDEBAR_TOGGLE: "ui:sidebar-toggle",
2132
- SIDEBAR_RESIZE: "ui:sidebar-resize",
2133
- SIDEBAR_RESIZE_START: "ui:sidebar-resize-start",
2134
- SIDEBAR_RESIZE_COMMIT: "ui:sidebar-resize-commit",
2135
- FOCUS_MODE_TOGGLE: "ui:focus-mode-toggle",
2136
- SETTINGS_VISIBILITY: "ui:settings-visibility",
2137
- // Settings
2138
- SETTINGS_GET: "settings:get",
2139
- SETTINGS_SET: "settings:set",
2140
- SETTINGS_UPDATE: "settings:update",
2141
- SETTINGS_HEALTH_GET: "settings:health:get",
2142
- // Bookmarks
2143
- BOOKMARKS_GET: "bookmarks:get",
2144
- BOOKMARKS_UPDATE: "bookmarks:update",
2145
- BOOKMARK_SAVE: "bookmarks:save",
2146
- BOOKMARK_REMOVE: "bookmarks:remove",
2147
- FOLDER_CREATE: "bookmarks:folder-create",
2148
- FOLDER_REMOVE: "bookmarks:folder-remove",
2149
- FOLDER_RENAME: "bookmarks:folder-rename",
2150
- // Highlights
2151
- HIGHLIGHT_CAPTURE: "highlights:capture",
2152
- HIGHLIGHT_CAPTURE_RESULT: "highlights:capture-result",
2153
- HIGHLIGHT_SELECTION: "vessel:highlight-selection",
2154
- // DevTools panel
2155
- DEVTOOLS_PANEL_TOGGLE: "devtools-panel:toggle",
2156
- DEVTOOLS_PANEL_STATE: "devtools-panel:state",
2157
- // Window controls
2158
- WINDOW_MINIMIZE: "window:minimize",
2159
- WINDOW_MAXIMIZE: "window:maximize",
2160
- WINDOW_CLOSE: "window:close"
2161
- };
2162
2278
  const SEARCH_ENGINE_HOSTS = [
2163
2279
  "google.",
2164
2280
  "bing.com",
@@ -2756,6 +2872,14 @@ const PRELOAD_EXTRACTION_SCRIPT = String.raw`
2756
2872
  `;
2757
2873
  const DIRECT_EXTRACTION_SCRIPT = String.raw`
2758
2874
  (function() {
2875
+ // Time budget: stop expensive DOM traversals after this many ms so heavy
2876
+ // pages (Newegg, Wikipedia, etc.) don't stall the agent for 30-60s+.
2877
+ var BUDGET_MS = 5000;
2878
+ var _budgetStart = performance.now();
2879
+ function withinBudget() {
2880
+ return (performance.now() - _budgetStart) < BUDGET_MS;
2881
+ }
2882
+
2759
2883
  function getCleanBodyText() {
2760
2884
  var removed = [];
2761
2885
  document
@@ -2913,20 +3037,40 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
2913
3037
  const viewportArea = Math.max(1, viewportWidth() * viewportHeight());
2914
3038
  const overlays = [];
2915
3039
 
2916
- Array.from(document.body.querySelectorAll("*")).forEach((node) => {
3040
+ // Use targeted selectors instead of querySelectorAll("*") to avoid
3041
+ // expensive getComputedStyle/getBoundingClientRect on every DOM element.
3042
+ // On heavy SPAs (e.g. Newegg) the wildcard could hit 10,000+ elements.
3043
+ var candidates = new Set();
3044
+
3045
+ // Semantic overlays: dialogs, modals, aria-modal
3046
+ document.body.querySelectorAll(
3047
+ "dialog, [role='dialog'], [role='alertdialog'], [aria-modal='true']"
3048
+ ).forEach(function(el) { candidates.add(el); });
3049
+
3050
+ // Fixed/sticky elements are the other overlay category — walk only
3051
+ // direct children of body and high-level containers (depth ≤ 3)
3052
+ // since real overlays are almost always near the top of the DOM tree.
3053
+ var MAX_CANDIDATES = 2000;
3054
+ var allElements = document.body.querySelectorAll("*");
3055
+ for (var ci = 0; ci < allElements.length && candidates.size < MAX_CANDIDATES; ci++) {
3056
+ candidates.add(allElements[ci]);
3057
+ }
3058
+
3059
+ candidates.forEach(function(node) {
3060
+ if (!withinBudget()) return;
2917
3061
  if (!(node instanceof HTMLElement)) return;
2918
3062
  if (!visible(node)) return;
2919
3063
 
2920
- const style = window.getComputedStyle(node);
3064
+ var style = window.getComputedStyle(node);
2921
3065
  if (style.pointerEvents === "none") return;
2922
3066
 
2923
- const rect = node.getBoundingClientRect();
3067
+ var rect = node.getBoundingClientRect();
2924
3068
  if (!inViewport(rect)) return;
2925
3069
 
2926
- const type = overlayType(node);
2927
- const dialogLike = type === "dialog" || type === "modal";
2928
- const areaRatio = (rect.width * rect.height) / viewportArea;
2929
- const blocksInteraction = dialogLike ||
3070
+ var type = overlayType(node);
3071
+ var dialogLike = type === "dialog" || type === "modal";
3072
+ var areaRatio = (rect.width * rect.height) / viewportArea;
3073
+ var blocksInteraction = dialogLike ||
2930
3074
  ((style.position === "fixed" || style.position === "sticky") &&
2931
3075
  parseZIndex(style) >= 10 &&
2932
3076
  areaRatio >= 0.3 &&
@@ -2936,17 +3080,17 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
2936
3080
 
2937
3081
  overlays.push({
2938
3082
  element: node,
2939
- type,
3083
+ type: type,
2940
3084
  role: text(node.getAttribute("role")),
2941
3085
  label: overlayLabel(node),
2942
3086
  selector: selectorFor(node),
2943
3087
  text: text(node.textContent)?.slice(0, 160),
2944
- blocksInteraction,
3088
+ blocksInteraction: blocksInteraction,
2945
3089
  zIndex: parseZIndex(style),
2946
3090
  });
2947
3091
  });
2948
3092
 
2949
- return overlays.sort((a, b) => {
3093
+ return overlays.sort(function(a, b) {
2950
3094
  if ((a.blocksInteraction ? 1 : 0) !== (b.blocksInteraction ? 1 : 0)) {
2951
3095
  return (b.blocksInteraction ? 1 : 0) - (a.blocksInteraction ? 1 : 0);
2952
3096
  }
@@ -3203,6 +3347,7 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3203
3347
 
3204
3348
  const navigation = [];
3205
3349
  document.querySelectorAll("nav a[href], [role='navigation'] a[href], header nav a[href]").forEach((el) => {
3350
+ if (!withinBudget()) return;
3206
3351
  const item = serializeInteractive(el, "link");
3207
3352
  if (!item.text || !item.href || item.href.startsWith("#")) return;
3208
3353
  item.context = "nav";
@@ -3218,14 +3363,17 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3218
3363
 
3219
3364
  const interactiveElements = [];
3220
3365
  document.querySelectorAll("button, [role='button'], input[type='submit'], input[type='button']").forEach((el) => {
3366
+ if (!withinBudget()) return;
3221
3367
  interactiveElements.push(serializeInteractive(el, "button"));
3222
3368
  });
3223
3369
  document.querySelectorAll("a[href]").forEach((el) => {
3370
+ if (!withinBudget()) return;
3224
3371
  const item = serializeInteractive(el, "link");
3225
3372
  if (!item.text || !item.href || item.href.startsWith("#") || item.context === "nav") return;
3226
3373
  interactiveElements.push(item);
3227
3374
  });
3228
3375
  document.querySelectorAll("input:not([type='hidden']):not([type='submit']):not([type='button']), select, textarea").forEach((el) => {
3376
+ if (!withinBudget()) return;
3229
3377
  const tag = el.tagName.toLowerCase();
3230
3378
  interactiveElements.push(
3231
3379
  serializeInteractive(el, tag === "select" ? "select" : tag === "textarea" ? "textarea" : "input"),
@@ -3250,7 +3398,7 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3250
3398
  serializeInteractive(el, tag === "select" ? "select" : tag === "textarea" ? "textarea" : "input"),
3251
3399
  );
3252
3400
  });
3253
- Array.from(document.querySelectorAll("button, input[type='submit'], input[type='image']"))
3401
+ Array.from(form.querySelectorAll("button, input[type='submit'], input[type='image']"))
3254
3402
  .filter((el) => isSubmitControlForForm(el, form))
3255
3403
  .forEach((el) => {
3256
3404
  fields.push(serializeInteractive(el, "button"));
@@ -3486,6 +3634,7 @@ const SAFE_EXTRACTION_SCRIPT = String.raw`
3486
3634
  function delay(ms) {
3487
3635
  return new Promise((resolve) => setTimeout(resolve, ms));
3488
3636
  }
3637
+ const EXECUTE_SCRIPT_TIMEOUT_MS = 1500;
3489
3638
  async function waitForDomReady(webContents, timeoutMs = 1500) {
3490
3639
  const deadline = Date.now() + timeoutMs;
3491
3640
  while (Date.now() < deadline) {
@@ -3507,10 +3656,20 @@ async function executeScript(webContents, script) {
3507
3656
  if (webContents.isDestroyed()) {
3508
3657
  return null;
3509
3658
  }
3659
+ let timer = null;
3510
3660
  try {
3511
- return await webContents.executeJavaScript(script);
3661
+ return await Promise.race([
3662
+ webContents.executeJavaScript(script),
3663
+ new Promise((resolve) => {
3664
+ timer = setTimeout(() => resolve(null), EXECUTE_SCRIPT_TIMEOUT_MS);
3665
+ })
3666
+ ]);
3512
3667
  } catch {
3513
3668
  return null;
3669
+ } finally {
3670
+ if (typeof timer !== "undefined" && timer) {
3671
+ clearTimeout(timer);
3672
+ }
3514
3673
  }
3515
3674
  }
3516
3675
  function bestString(values) {
@@ -3591,18 +3750,30 @@ function mergePageContent(candidates, webContents) {
3591
3750
  url: mergedBase.url || webContents.getURL() || ""
3592
3751
  };
3593
3752
  }
3753
+ const EXTRACT_TIMEOUT_MS = 8e3;
3754
+ async function extractContentInner(webContents) {
3755
+ await waitForDomReady(webContents);
3756
+ const [preloadResult, directResult, safeResult] = await Promise.all([
3757
+ executeScript(webContents, PRELOAD_EXTRACTION_SCRIPT),
3758
+ executeScript(webContents, DIRECT_EXTRACTION_SCRIPT),
3759
+ executeScript(webContents, SAFE_EXTRACTION_SCRIPT)
3760
+ ]);
3761
+ return mergePageContent(
3762
+ [preloadResult, directResult, safeResult],
3763
+ webContents
3764
+ );
3765
+ }
3594
3766
  async function extractContent(webContents) {
3595
3767
  try {
3596
- await waitForDomReady(webContents);
3597
- const [preloadResult, directResult, safeResult] = await Promise.all([
3598
- executeScript(webContents, PRELOAD_EXTRACTION_SCRIPT),
3599
- executeScript(webContents, DIRECT_EXTRACTION_SCRIPT),
3600
- executeScript(webContents, SAFE_EXTRACTION_SCRIPT)
3768
+ return await Promise.race([
3769
+ extractContentInner(webContents),
3770
+ new Promise(
3771
+ (_, reject) => setTimeout(
3772
+ () => reject(new Error("extractContent timeout")),
3773
+ EXTRACT_TIMEOUT_MS
3774
+ )
3775
+ )
3601
3776
  ]);
3602
- return mergePageContent(
3603
- [preloadResult, directResult, safeResult],
3604
- webContents
3605
- );
3606
3777
  } catch {
3607
3778
  return {
3608
3779
  ...EMPTY_PAGE_CONTENT,
@@ -3833,6 +4004,10 @@ class AnthropicProvider {
3833
4004
  let iterationsUsed = 0;
3834
4005
  for (let i = 0; i < maxIterations; i++) {
3835
4006
  iterationsUsed = i + 1;
4007
+ const msgTokenEstimate = JSON.stringify(messages).length;
4008
+ const sysTokenEstimate = systemPrompt.length;
4009
+ console.log(`[Vessel Agent] iteration=${i} messages=${messages.length} msgChars=${msgTokenEstimate} sysChars=${sysTokenEstimate} tools=${tools.length}`);
4010
+ const streamStartTime = Date.now();
3836
4011
  const stream = this.client.messages.stream(
3837
4012
  {
3838
4013
  model: this.model,
@@ -3846,40 +4021,56 @@ class AnthropicProvider {
3846
4021
  let textContent = "";
3847
4022
  const toolUseBlocks = [];
3848
4023
  let currentToolUse = null;
3849
- for await (const event of stream) {
3850
- if (event.type === "content_block_start") {
3851
- if (event.content_block.type === "tool_use") {
3852
- currentToolUse = {
3853
- id: event.content_block.id,
3854
- name: event.content_block.name,
3855
- inputJson: ""
3856
- };
3857
- }
3858
- } else if (event.type === "content_block_delta") {
3859
- if (event.delta.type === "text_delta") {
3860
- textContent += event.delta.text;
3861
- onChunk(event.delta.text);
3862
- } else if (event.delta.type === "input_json_delta" && currentToolUse) {
3863
- currentToolUse.inputJson += event.delta.partial_json;
3864
- }
3865
- } else if (event.type === "content_block_stop" && currentToolUse) {
3866
- try {
3867
- toolUseBlocks.push({
3868
- id: currentToolUse.id,
3869
- name: currentToolUse.name,
3870
- input: JSON.parse(currentToolUse.inputJson || "{}")
3871
- });
3872
- } catch {
3873
- toolUseBlocks.push({
3874
- id: currentToolUse.id,
3875
- name: currentToolUse.name,
3876
- input: {}
3877
- });
4024
+ const STREAM_IDLE_TIMEOUT_MS = 3e4;
4025
+ let idleTimer = null;
4026
+ const resetIdleTimer = () => {
4027
+ if (idleTimer) clearTimeout(idleTimer);
4028
+ idleTimer = setTimeout(() => {
4029
+ this.abortController?.abort();
4030
+ }, STREAM_IDLE_TIMEOUT_MS);
4031
+ };
4032
+ resetIdleTimer();
4033
+ try {
4034
+ for await (const event of stream) {
4035
+ resetIdleTimer();
4036
+ if (event.type === "content_block_start") {
4037
+ if (event.content_block.type === "tool_use") {
4038
+ currentToolUse = {
4039
+ id: event.content_block.id,
4040
+ name: event.content_block.name,
4041
+ inputJson: ""
4042
+ };
4043
+ }
4044
+ } else if (event.type === "content_block_delta") {
4045
+ if (event.delta.type === "text_delta") {
4046
+ textContent += event.delta.text;
4047
+ onChunk(event.delta.text);
4048
+ } else if (event.delta.type === "input_json_delta" && currentToolUse) {
4049
+ currentToolUse.inputJson += event.delta.partial_json;
4050
+ }
4051
+ } else if (event.type === "content_block_stop" && currentToolUse) {
4052
+ try {
4053
+ toolUseBlocks.push({
4054
+ id: currentToolUse.id,
4055
+ name: currentToolUse.name,
4056
+ input: JSON.parse(currentToolUse.inputJson || "{}")
4057
+ });
4058
+ } catch {
4059
+ toolUseBlocks.push({
4060
+ id: currentToolUse.id,
4061
+ name: currentToolUse.name,
4062
+ input: {}
4063
+ });
4064
+ }
4065
+ currentToolUse = null;
3878
4066
  }
3879
- currentToolUse = null;
3880
4067
  }
4068
+ } finally {
4069
+ if (idleTimer) clearTimeout(idleTimer);
3881
4070
  }
4071
+ console.log(`[Vessel Agent] stream complete in ${Date.now() - streamStartTime}ms, toolCalls=${toolUseBlocks.length} textLen=${textContent.length}`);
3882
4072
  const finalMessage = await stream.finalMessage();
4073
+ console.log(`[Vessel Agent] finalMessage received, stop_reason=${finalMessage.stop_reason}`);
3883
4074
  const assistantContent = [];
3884
4075
  if (textContent) {
3885
4076
  assistantContent.push({ type: "text", text: textContent });
@@ -3902,7 +4093,15 @@ class AnthropicProvider {
3902
4093
  onChunk(`
3903
4094
  <<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
3904
4095
  `);
3905
- const result = await onToolCall(tb.name, tb.input);
4096
+ let result;
4097
+ const toolStartTime = Date.now();
4098
+ console.log(`[Vessel Agent] executing tool: ${tb.name}`);
4099
+ try {
4100
+ result = await onToolCall(tb.name, tb.input);
4101
+ } catch (toolErr) {
4102
+ result = `Error: Tool execution failed — ${toolErr.message || toolErr}. Try a different approach or call read_page to refresh context.`;
4103
+ }
4104
+ console.log(`[Vessel Agent] tool ${tb.name} completed in ${Date.now() - toolStartTime}ms, resultLen=${result.length}`);
3906
4105
  toolResults.push({
3907
4106
  type: "tool_result",
3908
4107
  tool_use_id: tb.id,
@@ -4091,6 +4290,9 @@ class OpenAICompatProvider {
4091
4290
  let iterationsUsed = 0;
4092
4291
  for (let i = 0; i < maxIterations; i++) {
4093
4292
  iterationsUsed = i + 1;
4293
+ const msgTokenEstimate = JSON.stringify(messages).length;
4294
+ console.log(`[Vessel Agent OpenAI] iteration=${i} messages=${messages.length} msgChars=${msgTokenEstimate} tools=${openAITools.length}`);
4295
+ const streamStartTime = Date.now();
4094
4296
  let textAccum = "";
4095
4297
  const toolCallAccums = {};
4096
4298
  let finishReason = null;
@@ -4125,10 +4327,21 @@ class OpenAICompatProvider {
4125
4327
  }
4126
4328
  }
4127
4329
  }
4330
+ console.log(`[Vessel Agent OpenAI] stream complete in ${Date.now() - streamStartTime}ms, toolCalls=${Object.keys(toolCallAccums).length} textLen=${textAccum.length} finishReason=${finishReason}`);
4128
4331
  const toolCalls = Object.values(toolCallAccums);
4332
+ for (const tc of Object.values(toolCallAccums)) {
4333
+ if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
4334
+ }
4335
+ for (const tc of toolCalls) {
4336
+ try {
4337
+ JSON.parse(tc.argsJson || "{}");
4338
+ } catch {
4339
+ tc.argsJson = "{}";
4340
+ }
4341
+ }
4129
4342
  const assistantMsg = {
4130
4343
  role: "assistant",
4131
- content: textAccum || null,
4344
+ content: textAccum || "",
4132
4345
  ...toolCalls.length > 0 && {
4133
4346
  tool_calls: toolCalls.map((tc) => ({
4134
4347
  id: tc.id,
@@ -4144,12 +4357,29 @@ class OpenAICompatProvider {
4144
4357
  try {
4145
4358
  args = JSON.parse(tc.argsJson || "{}");
4146
4359
  } catch {
4360
+ onChunk(`
4361
+ <<tool:${tc.name}:⚠ invalid args>>
4362
+ `);
4363
+ messages.push({
4364
+ role: "tool",
4365
+ tool_call_id: tc.id,
4366
+ content: `Error: Invalid JSON in tool arguments. Please retry with valid JSON. Raw: ${tc.argsJson}`
4367
+ });
4368
+ continue;
4147
4369
  }
4148
4370
  const argSummary = args.url || args.text || args.direction || "";
4149
4371
  onChunk(`
4150
4372
  <<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
4151
4373
  `);
4152
- const result = await onToolCall(tc.name, args);
4374
+ let result;
4375
+ const toolStartTime = Date.now();
4376
+ console.log(`[Vessel Agent OpenAI] executing tool: ${tc.name}`);
4377
+ try {
4378
+ result = await onToolCall(tc.name, args);
4379
+ } catch (toolErr) {
4380
+ result = `Error: Tool execution failed — ${toolErr.message || toolErr}. Try a different approach or call read_page to refresh context.`;
4381
+ }
4382
+ console.log(`[Vessel Agent OpenAI] tool ${tc.name} completed in ${Date.now() - toolStartTime}ms, resultLen=${result.length}`);
4153
4383
  messages.push({
4154
4384
  role: "tool",
4155
4385
  tool_call_id: tc.id,
@@ -4320,7 +4550,20 @@ function isVisibleToUser(el) {
4320
4550
  }
4321
4551
  function formatInteractiveElements(elements) {
4322
4552
  if (elements.length === 0) return "None";
4323
- const items = limitItems(elements, 50);
4553
+ const sorted = [...elements].sort((a, b) => {
4554
+ const scoreEl = (el) => {
4555
+ let s = 0;
4556
+ if (el.visible === false) s += 100;
4557
+ if (el.inViewport === false) s += 50;
4558
+ if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
4559
+ s += 30;
4560
+ if (el.obscured) s += 20;
4561
+ if (el.type === "link") s += 5;
4562
+ return s;
4563
+ };
4564
+ return scoreEl(a) - scoreEl(b);
4565
+ });
4566
+ const items = limitItems(sorted, 50);
4324
4567
  return items.map((el) => {
4325
4568
  const prefix = el.index ? `[#${el.index}]` : "-";
4326
4569
  const parts = [prefix];
@@ -4486,7 +4729,10 @@ function formatStructuredEntities(entities) {
4486
4729
  if (entity.url && entity.url !== entity.name) {
4487
4730
  lines.push(` url: ${entity.url}`);
4488
4731
  }
4489
- for (const [key, value] of Object.entries(entity.attributes).slice(0, 8)) {
4732
+ for (const [key, value] of Object.entries(entity.attributes).slice(
4733
+ 0,
4734
+ 8
4735
+ )) {
4490
4736
  const rendered = formatStructuredValue(value);
4491
4737
  if (rendered) {
4492
4738
  lines.push(` ${key}: ${rendered}`);
@@ -4529,10 +4775,34 @@ function getHighlightsForPage(url) {
4529
4775
  function formatJsonLd(items) {
4530
4776
  if (!items || items.length === 0) return "";
4531
4777
  const lines = [];
4532
- const SKIP = /* @__PURE__ */ new Set(["@context", "image", "logo", "thumbnail", "potentialAction"]);
4778
+ const SKIP = /* @__PURE__ */ new Set([
4779
+ "@context",
4780
+ "image",
4781
+ "logo",
4782
+ "thumbnail",
4783
+ "potentialAction"
4784
+ ]);
4533
4785
  const TYPE_FIELDS = {
4534
- Recipe: ["name", "url", "description", "recipeYield", "totalTime", "cookTime", "prepTime", "recipeIngredient", "recipeInstructions"],
4535
- Article: ["headline", "name", "url", "datePublished", "dateModified", "author", "description"],
4786
+ Recipe: [
4787
+ "name",
4788
+ "url",
4789
+ "description",
4790
+ "recipeYield",
4791
+ "totalTime",
4792
+ "cookTime",
4793
+ "prepTime",
4794
+ "recipeIngredient",
4795
+ "recipeInstructions"
4796
+ ],
4797
+ Article: [
4798
+ "headline",
4799
+ "name",
4800
+ "url",
4801
+ "datePublished",
4802
+ "dateModified",
4803
+ "author",
4804
+ "description"
4805
+ ],
4536
4806
  Product: ["name", "url", "description", "offers"],
4537
4807
  BreadcrumbList: ["itemListElement"],
4538
4808
  Organization: ["name", "url", "description"]
@@ -4545,9 +4815,11 @@ function formatJsonLd(items) {
4545
4815
  const renderValue = (val, depth = 0) => {
4546
4816
  if (val === null || val === void 0) return "";
4547
4817
  if (typeof val === "string") return val;
4548
- if (typeof val === "number" || typeof val === "boolean") return String(val);
4818
+ if (typeof val === "number" || typeof val === "boolean")
4819
+ return String(val);
4549
4820
  if (Array.isArray(val)) {
4550
- if (depth > 0) return val.map((v) => renderValue(v, depth + 1)).filter(Boolean).join(", ");
4821
+ if (depth > 0)
4822
+ return val.map((v) => renderValue(v, depth + 1)).filter(Boolean).join(", ");
4551
4823
  return val.map((v, i) => {
4552
4824
  const s = renderValue(v, depth + 1);
4553
4825
  return s ? ` ${i + 1}. ${s}` : "";
@@ -4577,6 +4849,24 @@ function formatJsonLd(items) {
4577
4849
  }
4578
4850
  return lines.join("\n");
4579
4851
  }
4852
+ function chooseAgentReadMode(page) {
4853
+ const pageType = detectPageType(page);
4854
+ switch (pageType) {
4855
+ case "SEARCH_RESULTS":
4856
+ case "PAGINATED_LIST":
4857
+ return "results_only";
4858
+ case "LOGIN":
4859
+ case "SEARCH_READY":
4860
+ case "SHOPPING":
4861
+ case "FORM":
4862
+ return "visible_only";
4863
+ case "ARTICLE":
4864
+ return "summary";
4865
+ case "GENERAL":
4866
+ default:
4867
+ return "visible_only";
4868
+ }
4869
+ }
4580
4870
  function normalizeComparable(value) {
4581
4871
  return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
4582
4872
  }
@@ -4621,7 +4911,9 @@ function collectJsonLdEntityItems(input, results = []) {
4621
4911
  const item = input;
4622
4912
  const type = item["@type"];
4623
4913
  const types = Array.isArray(type) ? type : [type];
4624
- const typeNames = types.filter((entry) => typeof entry === "string");
4914
+ const typeNames = types.filter(
4915
+ (entry) => typeof entry === "string"
4916
+ );
4625
4917
  if ((typeof item.name === "string" || typeof item.url === "string") && !typeNames.some(
4626
4918
  (entry) => ["BreadcrumbList", "Organization", "WebSite", "WebPage"].includes(entry)
4627
4919
  )) {
@@ -4666,7 +4958,8 @@ function getResultCandidates(page) {
4666
4958
  score += 4;
4667
4959
  }
4668
4960
  if (element.context === "article") score += 3;
4669
- else if (element.context === "main" || element.context === "content") score += 1;
4961
+ else if (element.context === "main" || element.context === "content")
4962
+ score += 1;
4670
4963
  if (href && pageHost) {
4671
4964
  try {
4672
4965
  if (new URL(href).origin === new URL(pageHost).origin) score += 1;
@@ -4680,7 +4973,9 @@ function getResultCandidates(page) {
4680
4973
  score += 2;
4681
4974
  }
4682
4975
  if (/\b(card|tile|result|rating|review)\b/.test(haystack)) score += 1;
4683
- if (/\b(item|list|row|repo|repository|issue|pull request|event)\b/.test(haystack)) {
4976
+ if (/\b(item|list|row|repo|repository|issue|pull request|event)\b/.test(
4977
+ haystack
4978
+ )) {
4684
4979
  score += 1;
4685
4980
  }
4686
4981
  if (text.length >= 12 && text.split(/\s+/).length >= 2) score += 1;
@@ -4699,7 +4994,9 @@ function getResultCandidates(page) {
4699
4994
  return score >= 4 || score >= 3 && (element.context === "article" || element.context === "main" || element.context === "content");
4700
4995
  }
4701
4996
  return score >= 4 || score >= 3 && element.context === "article";
4702
- }).sort((a, b) => b.score - a.score || (a.element.index ?? 0) - (b.element.index ?? 0));
4997
+ }).sort(
4998
+ (a, b) => b.score - a.score || (a.element.index ?? 0) - (b.element.index ?? 0)
4999
+ );
4703
5000
  const seen = /* @__PURE__ */ new Set();
4704
5001
  return scored.map(({ element }) => element).filter((element) => {
4705
5002
  const key = `${normalizeComparable(element.text || "")}|${normalizeUrlForMatch(element.href) || ""}`;
@@ -4952,7 +5249,9 @@ function buildScopedContext(page, mode) {
4952
5249
  sections.push(`### Likely Search Results (${resultElements.length})`);
4953
5250
  sections.push(formatInteractiveElements(resultElements));
4954
5251
  } else {
4955
- sections.push("No likely primary result links were detected on this page.");
5252
+ sections.push(
5253
+ "No likely primary result links were detected on this page."
5254
+ );
4956
5255
  }
4957
5256
  return sections.join("\n");
4958
5257
  }
@@ -4961,10 +5260,8 @@ function buildScopedContext(page, mode) {
4961
5260
  return buildStructuredContext(page);
4962
5261
  }
4963
5262
  }
4964
- function analyzePageIntent(page) {
4965
- const hints = [];
5263
+ function detectPageType(page) {
4966
5264
  const url = page.url.toLowerCase();
4967
- (page.title || "").toLowerCase();
4968
5265
  const hasPasswordField = page.forms.some(
4969
5266
  (f) => f.fields.some((el) => el.inputType === "password")
4970
5267
  );
@@ -4983,47 +5280,85 @@ function analyzePageIntent(page) {
4983
5280
  const hasPagination = page.interactiveElements.some(
4984
5281
  (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»" || (el.label || "").toLowerCase().includes("next page")
4985
5282
  );
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")}`;
5283
+ if (hasPasswordField) return "LOGIN";
5284
+ if (hasSearchInput && !hasResults) return "SEARCH_READY";
5285
+ if (hasResults && hasSearchInput) return "SEARCH_RESULTS";
5286
+ if (hasCart) return "SHOPPING";
5287
+ if (formCount > 0 && !hasPasswordField) return "FORM";
5288
+ if (hasPagination) return "PAGINATED_LIST";
5289
+ if (page.content.length > 3e3 && page.interactiveElements.length < 10)
5290
+ return "ARTICLE";
5291
+ return "GENERAL";
5019
5292
  }
5020
- function buildStructuredContext(page) {
5021
- const sections = [];
5022
- sections.push("## PAGE STRUCTURE");
5023
- sections.push("");
5024
- sections.push(
5025
- "**User Focus:** This page is from the active tab currently visible to the human user."
5026
- );
5293
+ function analyzePageIntent(page) {
5294
+ const hints = [];
5295
+ const pageType = detectPageType(page);
5296
+ const hasPagination = page.interactiveElements.some(
5297
+ (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»" || (el.label || "").toLowerCase().includes("next page")
5298
+ );
5299
+ switch (pageType) {
5300
+ case "LOGIN": {
5301
+ hints.push("Page type: LOGIN/SIGNUP");
5302
+ hints.push(
5303
+ "Suggested: vessel_login or vessel_fill_form → auto-fills credentials and submits"
5304
+ );
5305
+ const userField = page.forms.flatMap((f) => f.fields).find(
5306
+ (el) => el.inputType === "email" || el.name === "email" || el.name === "username" || el.autocomplete === "username"
5307
+ );
5308
+ if (userField) {
5309
+ hints.push(
5310
+ `Username field: #${userField.index} [${userField.label || userField.name || userField.placeholder || "input"}]`
5311
+ );
5312
+ }
5313
+ break;
5314
+ }
5315
+ case "SEARCH_READY":
5316
+ hints.push("Page type: SEARCH READY");
5317
+ hints.push(
5318
+ "Suggested: vessel_search → auto-finds search box, types query, and submits"
5319
+ );
5320
+ break;
5321
+ case "SEARCH_RESULTS":
5322
+ hints.push("Page type: SEARCH RESULTS");
5323
+ hints.push(
5324
+ "Suggested: click a result link, or vessel_paginate for more results"
5325
+ );
5326
+ if (hasPagination)
5327
+ hints.push("Pagination detected — vessel_paginate available");
5328
+ break;
5329
+ case "SHOPPING":
5330
+ hints.push("Page type: SHOPPING/CHECKOUT");
5331
+ hints.push("Suggested: vessel_fill_form for payment/address fields");
5332
+ break;
5333
+ case "FORM": {
5334
+ const formCount = page.forms.length;
5335
+ const totalFields = page.forms.reduce((n, f) => n + f.fields.length, 0);
5336
+ hints.push(
5337
+ `Page type: FORM (${formCount} form${formCount > 1 ? "s" : ""}, ${totalFields} fields)`
5338
+ );
5339
+ hints.push("Suggested: vessel_fill_form → fill all fields in one call");
5340
+ break;
5341
+ }
5342
+ case "PAGINATED_LIST":
5343
+ hints.push("Page type: PAGINATED LIST");
5344
+ hints.push("Suggested: vessel_paginate to navigate between pages");
5345
+ break;
5346
+ case "ARTICLE":
5347
+ hints.push("Page type: ARTICLE/CONTENT");
5348
+ hints.push("Suggested: vessel_extract_content for readable text");
5349
+ break;
5350
+ }
5351
+ if (hints.length === 0) return "";
5352
+ return `### Page Intent (Speedee)
5353
+ ${hints.join("\n")}`;
5354
+ }
5355
+ function buildStructuredContext(page) {
5356
+ const sections = [];
5357
+ sections.push("## PAGE STRUCTURE");
5358
+ sections.push("");
5359
+ sections.push(
5360
+ "**User Focus:** This page is from the active tab currently visible to the human user."
5361
+ );
5027
5362
  sections.push(`**URL:** ${page.url}`);
5028
5363
  sections.push(`**Title:** ${page.title}`);
5029
5364
  sections.push(`**Viewport:** ${formatViewport(page)}`);
@@ -5151,12 +5486,14 @@ const TOOL_DEFINITIONS = [
5151
5486
  {
5152
5487
  name: "current_tab",
5153
5488
  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."
5489
+ 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.",
5490
+ tier: 0
5155
5491
  },
5156
5492
  {
5157
5493
  name: "list_tabs",
5158
5494
  title: "List Tabs",
5159
- description: "List all open browser tabs with their IDs, titles, and URLs."
5495
+ description: "List all open browser tabs with their IDs, titles, and URLs.",
5496
+ tier: 2
5160
5497
  },
5161
5498
  {
5162
5499
  name: "switch_tab",
@@ -5164,10 +5501,9 @@ const TOOL_DEFINITIONS = [
5164
5501
  description: "Switch to a browser tab by tab ID, or by matching part of the title or URL.",
5165
5502
  inputSchema: {
5166
5503
  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
- )
5170
- }
5504
+ match: zod.z.string().optional().describe("Case-insensitive partial match against tab title or URL")
5505
+ },
5506
+ tier: 2
5171
5507
  },
5172
5508
  {
5173
5509
  name: "create_tab",
@@ -5175,7 +5511,8 @@ const TOOL_DEFINITIONS = [
5175
5511
  description: "Open a new browser tab, optionally navigating to a URL.",
5176
5512
  inputSchema: {
5177
5513
  url: zod.z.string().optional().describe("Optional URL to open")
5178
- }
5514
+ },
5515
+ tier: 2
5179
5516
  },
5180
5517
  // --- Navigation ---
5181
5518
  {
@@ -5184,22 +5521,26 @@ const TOOL_DEFINITIONS = [
5184
5521
  description: "Navigate the browser to a URL.",
5185
5522
  inputSchema: {
5186
5523
  url: zod.z.string().describe("The URL to navigate to")
5187
- }
5524
+ },
5525
+ tier: 0
5188
5526
  },
5189
5527
  {
5190
5528
  name: "go_back",
5191
5529
  title: "Go Back",
5192
- description: "Go back to the previous page in browser history."
5530
+ description: "Go back to the previous page in browser history.",
5531
+ tier: 1
5193
5532
  },
5194
5533
  {
5195
5534
  name: "go_forward",
5196
5535
  title: "Go Forward",
5197
- description: "Go forward in browser history."
5536
+ description: "Go forward in browser history.",
5537
+ tier: 2
5198
5538
  },
5199
5539
  {
5200
5540
  name: "reload",
5201
5541
  title: "Reload",
5202
- description: "Reload the current page."
5542
+ description: "Reload the current page.",
5543
+ tier: 2
5203
5544
  },
5204
5545
  // --- Interaction ---
5205
5546
  {
@@ -5209,7 +5550,8 @@ const TOOL_DEFINITIONS = [
5209
5550
  inputSchema: {
5210
5551
  index: zod.z.number().optional().describe("Element index from the page content listing"),
5211
5552
  selector: zod.z.string().optional().describe("CSS selector as fallback")
5212
- }
5553
+ },
5554
+ tier: 0
5213
5555
  },
5214
5556
  {
5215
5557
  name: "type_text",
@@ -5222,7 +5564,9 @@ const TOOL_DEFINITIONS = [
5222
5564
  mode: zod.z.enum(["default", "keystroke"]).optional().describe(
5223
5565
  '"default" sets value directly. "keystroke" simulates character-by-character key events.'
5224
5566
  )
5225
- }
5567
+ },
5568
+ tier: 0,
5569
+ relevance: ["LOGIN", "FORM", "SEARCH_READY"]
5226
5570
  },
5227
5571
  {
5228
5572
  name: "select_option",
@@ -5233,7 +5577,9 @@ const TOOL_DEFINITIONS = [
5233
5577
  selector: zod.z.string().optional().describe("CSS selector as fallback"),
5234
5578
  label: zod.z.string().optional().describe("Visible option label to match"),
5235
5579
  value: zod.z.string().optional().describe("Option value attribute to match")
5236
- }
5580
+ },
5581
+ tier: 1,
5582
+ relevance: ["FORM", "SHOPPING"]
5237
5583
  },
5238
5584
  {
5239
5585
  name: "submit_form",
@@ -5242,7 +5588,9 @@ const TOOL_DEFINITIONS = [
5242
5588
  inputSchema: {
5243
5589
  index: zod.z.number().optional().describe("Index of a form field or submit button"),
5244
5590
  selector: zod.z.string().optional().describe("Form or submit button selector")
5245
- }
5591
+ },
5592
+ tier: 1,
5593
+ relevance: ["LOGIN", "FORM", "SEARCH_READY", "SHOPPING"]
5246
5594
  },
5247
5595
  {
5248
5596
  name: "press_key",
@@ -5252,7 +5600,8 @@ const TOOL_DEFINITIONS = [
5252
5600
  key: zod.z.string().describe("Keyboard key such as Enter or Escape"),
5253
5601
  index: zod.z.number().optional().describe("Element index to focus first"),
5254
5602
  selector: zod.z.string().optional().describe("CSS selector to focus first")
5255
- }
5603
+ },
5604
+ tier: 1
5256
5605
  },
5257
5606
  {
5258
5607
  name: "scroll",
@@ -5261,7 +5610,9 @@ const TOOL_DEFINITIONS = [
5261
5610
  inputSchema: {
5262
5611
  direction: zod.z.enum(["up", "down"]).describe("Scroll direction"),
5263
5612
  amount: zod.z.number().optional().describe("Pixels to scroll (default 500)")
5264
- }
5613
+ },
5614
+ tier: 0,
5615
+ relevance: ["ARTICLE", "SEARCH_RESULTS", "PAGINATED_LIST"]
5265
5616
  },
5266
5617
  {
5267
5618
  name: "hover",
@@ -5270,7 +5621,8 @@ const TOOL_DEFINITIONS = [
5270
5621
  inputSchema: {
5271
5622
  index: zod.z.number().optional().describe("Element index number"),
5272
5623
  selector: zod.z.string().optional().describe("CSS selector as fallback")
5273
- }
5624
+ },
5625
+ tier: 2
5274
5626
  },
5275
5627
  {
5276
5628
  name: "focus",
@@ -5279,7 +5631,9 @@ const TOOL_DEFINITIONS = [
5279
5631
  inputSchema: {
5280
5632
  index: zod.z.number().optional().describe("Element index number"),
5281
5633
  selector: zod.z.string().optional().describe("CSS selector as fallback")
5282
- }
5634
+ },
5635
+ tier: 2,
5636
+ relevance: ["FORM", "LOGIN"]
5283
5637
  },
5284
5638
  // --- Page & Content ---
5285
5639
  {
@@ -5289,21 +5643,36 @@ const TOOL_DEFINITIONS = [
5289
5643
  inputSchema: {
5290
5644
  enabled: zod.z.boolean().describe("Whether ad blocking should be enabled for the tab"),
5291
5645
  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
- ),
5646
+ match: zod.z.string().optional().describe("Case-insensitive partial match against tab title or URL"),
5295
5647
  reload: zod.z.boolean().optional().describe("Reload the tab after changing (default true)")
5296
- }
5648
+ },
5649
+ tier: 2
5297
5650
  },
5298
5651
  {
5299
5652
  name: "dismiss_popup",
5300
5653
  title: "Dismiss Popup",
5301
- description: "Dismiss a modal, popup, newsletter gate, cookie banner, or overlay using common close/decline actions."
5654
+ description: "Dismiss a modal, popup, newsletter gate, cookie banner, or overlay using common close/decline actions.",
5655
+ tier: 1
5302
5656
  },
5303
5657
  {
5304
5658
  name: "read_page",
5305
5659
  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."
5660
+ description: "Read the current page using a scoped mode. Defaults to a minimal navigation-focused brief; use mode='debug' only when narrower modes are insufficient.",
5661
+ inputSchema: {
5662
+ mode: zod.z.enum([
5663
+ "summary",
5664
+ "interactives_only",
5665
+ "forms_only",
5666
+ "text_only",
5667
+ "visible_only",
5668
+ "results_only",
5669
+ "full",
5670
+ "debug"
5671
+ ]).optional().describe(
5672
+ "Read mode: visible_only/results_only/forms_only/summary/text_only for narrow reads, full/debug for the complete page dump"
5673
+ )
5674
+ },
5675
+ tier: 0
5307
5676
  },
5308
5677
  {
5309
5678
  name: "wait_for",
@@ -5313,7 +5682,8 @@ const TOOL_DEFINITIONS = [
5313
5682
  text: zod.z.string().optional().describe("Text that should appear in the page body"),
5314
5683
  selector: zod.z.string().optional().describe("CSS selector that should match an element"),
5315
5684
  timeoutMs: zod.z.number().optional().describe("Maximum time to wait in milliseconds (default 5000)")
5316
- }
5685
+ },
5686
+ tier: 2
5317
5687
  },
5318
5688
  // --- Checkpoints & Sessions ---
5319
5689
  {
@@ -5323,7 +5693,8 @@ const TOOL_DEFINITIONS = [
5323
5693
  inputSchema: {
5324
5694
  name: zod.z.string().optional().describe("Short checkpoint name"),
5325
5695
  note: zod.z.string().optional().describe("Optional note about why this checkpoint matters")
5326
- }
5696
+ },
5697
+ tier: 2
5327
5698
  },
5328
5699
  {
5329
5700
  name: "restore_checkpoint",
@@ -5332,7 +5703,8 @@ const TOOL_DEFINITIONS = [
5332
5703
  inputSchema: {
5333
5704
  checkpointId: zod.z.string().optional().describe("Exact checkpoint ID"),
5334
5705
  name: zod.z.string().optional().describe("Checkpoint name to match if ID is unknown")
5335
- }
5706
+ },
5707
+ tier: 2
5336
5708
  },
5337
5709
  {
5338
5710
  name: "save_session",
@@ -5340,7 +5712,9 @@ const TOOL_DEFINITIONS = [
5340
5712
  description: "Persist the current browser cookies, localStorage, and tab layout under a reusable session name.",
5341
5713
  inputSchema: {
5342
5714
  name: zod.z.string().describe("Session name such as github-logged-in")
5343
- }
5715
+ },
5716
+ tier: 2,
5717
+ relevance: ["LOGIN"]
5344
5718
  },
5345
5719
  {
5346
5720
  name: "load_session",
@@ -5348,12 +5722,14 @@ const TOOL_DEFINITIONS = [
5348
5722
  description: "Load a previously saved named session, restoring cookies, localStorage, and saved tabs.",
5349
5723
  inputSchema: {
5350
5724
  name: zod.z.string().describe("Previously saved session name")
5351
- }
5725
+ },
5726
+ tier: 2
5352
5727
  },
5353
5728
  {
5354
5729
  name: "list_sessions",
5355
5730
  title: "List Sessions",
5356
- description: "List previously saved named browser sessions with cookie and storage counts."
5731
+ description: "List previously saved named browser sessions with cookie and storage counts.",
5732
+ tier: 2
5357
5733
  },
5358
5734
  {
5359
5735
  name: "delete_session",
@@ -5361,7 +5737,8 @@ const TOOL_DEFINITIONS = [
5361
5737
  description: "Delete a previously saved named browser session.",
5362
5738
  inputSchema: {
5363
5739
  name: zod.z.string().describe("Saved session name to delete")
5364
- }
5740
+ },
5741
+ tier: 2
5365
5742
  },
5366
5743
  // --- Bookmarks ---
5367
5744
  {
@@ -5371,7 +5748,8 @@ const TOOL_DEFINITIONS = [
5371
5748
  inputSchema: {
5372
5749
  folderId: zod.z.string().optional().describe("Exact bookmark folder ID to filter by"),
5373
5750
  folderName: zod.z.string().optional().describe("Exact bookmark folder name to filter by")
5374
- }
5751
+ },
5752
+ tier: 2
5375
5753
  },
5376
5754
  {
5377
5755
  name: "search_bookmarks",
@@ -5379,7 +5757,8 @@ const TOOL_DEFINITIONS = [
5379
5757
  description: "Search bookmarks by title, URL, note, folder name, or folder summary.",
5380
5758
  inputSchema: {
5381
5759
  query: zod.z.string().describe("Search term to match against saved bookmarks")
5382
- }
5760
+ },
5761
+ tier: 2
5383
5762
  },
5384
5763
  {
5385
5764
  name: "create_bookmark_folder",
@@ -5388,7 +5767,8 @@ const TOOL_DEFINITIONS = [
5388
5767
  inputSchema: {
5389
5768
  name: zod.z.string().describe("Folder name to create"),
5390
5769
  summary: zod.z.string().optional().describe("Optional one-sentence summary for this folder")
5391
- }
5770
+ },
5771
+ tier: 2
5392
5772
  },
5393
5773
  {
5394
5774
  name: "save_bookmark",
@@ -5400,12 +5780,15 @@ const TOOL_DEFINITIONS = [
5400
5780
  index: zod.z.number().optional().describe("Element index of a link to bookmark without opening"),
5401
5781
  selector: zod.z.string().optional().describe("CSS selector of a link to bookmark without opening"),
5402
5782
  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."),
5783
+ folderName: zod.z.string().optional().describe(
5784
+ "Folder name to save into. Created automatically if missing."
5785
+ ),
5404
5786
  folderSummary: zod.z.string().optional().describe("Optional summary used if a new folder is created"),
5405
5787
  createFolderIfMissing: zod.z.boolean().optional().describe("Create folderName automatically when it does not exist"),
5406
5788
  note: zod.z.string().optional().describe("Optional note about why the page was saved"),
5407
5789
  onDuplicate: zod.z.enum(["ask", "update", "duplicate"]).optional().describe("How to handle duplicate URLs in the same folder")
5408
- }
5790
+ },
5791
+ tier: 1
5409
5792
  },
5410
5793
  {
5411
5794
  name: "organize_bookmark",
@@ -5423,7 +5806,8 @@ const TOOL_DEFINITIONS = [
5423
5806
  createFolderIfMissing: zod.z.boolean().optional().describe("Create folderName automatically when it does not exist"),
5424
5807
  note: zod.z.string().optional().describe("Optional note"),
5425
5808
  archive: zod.z.boolean().optional().describe('If true, organize into the default "Archive" folder')
5426
- }
5809
+ },
5810
+ tier: 2
5427
5811
  },
5428
5812
  {
5429
5813
  name: "archive_bookmark",
@@ -5436,7 +5820,8 @@ const TOOL_DEFINITIONS = [
5436
5820
  index: zod.z.number().optional().describe("Element index of a link to archive"),
5437
5821
  selector: zod.z.string().optional().describe("CSS selector of a link to archive"),
5438
5822
  note: zod.z.string().optional().describe("Optional note")
5439
- }
5823
+ },
5824
+ tier: 2
5440
5825
  },
5441
5826
  {
5442
5827
  name: "open_bookmark",
@@ -5445,7 +5830,8 @@ const TOOL_DEFINITIONS = [
5445
5830
  inputSchema: {
5446
5831
  bookmarkId: zod.z.string().describe("Exact bookmark ID to open"),
5447
5832
  newTab: zod.z.boolean().optional().describe("Open in a new tab instead of the current tab")
5448
- }
5833
+ },
5834
+ tier: 2
5449
5835
  },
5450
5836
  // --- Highlights ---
5451
5837
  {
@@ -5457,14 +5843,19 @@ const TOOL_DEFINITIONS = [
5457
5843
  selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
5458
5844
  text: zod.z.string().optional().describe("Text to find and highlight on the page (all occurrences)"),
5459
5845
  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)"),
5846
+ durationMs: zod.z.number().optional().describe(
5847
+ "Auto-clear after this many milliseconds (omit for permanent)"
5848
+ ),
5461
5849
  color: zod.z.enum(["yellow", "red", "green", "blue", "purple", "orange"]).optional().describe("Highlight color (default yellow)")
5462
- }
5850
+ },
5851
+ tier: 1,
5852
+ relevance: ["ARTICLE", "SEARCH_RESULTS"]
5463
5853
  },
5464
5854
  {
5465
5855
  name: "clear_highlights",
5466
5856
  title: "Clear Highlights",
5467
- description: "Remove all visual highlights from the current page."
5857
+ description: "Remove all visual highlights from the current page.",
5858
+ tier: 2
5468
5859
  },
5469
5860
  // --- Speedee System: Flow State ---
5470
5861
  {
@@ -5472,11 +5863,14 @@ const TOOL_DEFINITIONS = [
5472
5863
  title: "Start Workflow",
5473
5864
  description: "Begin tracking a multi-step web workflow. Vessel will show progress after every action so you always know where you are.",
5474
5865
  inputSchema: {
5475
- goal: zod.z.string().describe("What this workflow accomplishes (e.g. 'Purchase item from Amazon')"),
5866
+ goal: zod.z.string().describe(
5867
+ "What this workflow accomplishes (e.g. 'Purchase item from Amazon')"
5868
+ ),
5476
5869
  steps: zod.z.array(zod.z.string()).describe(
5477
5870
  "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
5478
5871
  )
5479
- }
5872
+ },
5873
+ tier: 1
5480
5874
  },
5481
5875
  {
5482
5876
  name: "flow_advance",
@@ -5484,23 +5878,27 @@ const TOOL_DEFINITIONS = [
5484
5878
  description: "Mark the current workflow step as done and move to the next one.",
5485
5879
  inputSchema: {
5486
5880
  detail: zod.z.string().optional().describe("Brief note about what was accomplished")
5487
- }
5881
+ },
5882
+ tier: 1
5488
5883
  },
5489
5884
  {
5490
5885
  name: "flow_status",
5491
5886
  title: "Workflow Status",
5492
- description: "Check the current workflow progress."
5887
+ description: "Check the current workflow progress.",
5888
+ tier: 2
5493
5889
  },
5494
5890
  {
5495
5891
  name: "flow_end",
5496
5892
  title: "End Workflow",
5497
- description: "Clear the active workflow tracker."
5893
+ description: "Clear the active workflow tracker.",
5894
+ tier: 2
5498
5895
  },
5499
5896
  // --- Speedee System: Suggestion Engine ---
5500
5897
  {
5501
5898
  name: "suggest",
5502
5899
  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."
5900
+ description: "Analyze the current page and return the most relevant tools and suggested next actions. Call this when unsure what to do.",
5901
+ tier: 1
5504
5902
  },
5505
5903
  // --- Speedee System: Composable Macros ---
5506
5904
  {
@@ -5516,7 +5914,9 @@ const TOOL_DEFINITIONS = [
5516
5914
  })
5517
5915
  ).describe("Fields to fill"),
5518
5916
  submit: zod.z.boolean().optional().describe("Submit the form after filling (default false)")
5519
- }
5917
+ },
5918
+ tier: 1,
5919
+ relevance: ["FORM", "LOGIN", "SHOPPING"]
5520
5920
  },
5521
5921
  {
5522
5922
  name: "login",
@@ -5529,7 +5929,9 @@ const TOOL_DEFINITIONS = [
5529
5929
  username_selector: zod.z.string().optional().describe("CSS selector for username field (auto-detected if omitted)"),
5530
5930
  password_selector: zod.z.string().optional().describe("CSS selector for password field (auto-detected if omitted)"),
5531
5931
  submit_selector: zod.z.string().optional().describe("CSS selector for submit button (auto-detected if omitted)")
5532
- }
5932
+ },
5933
+ tier: 1,
5934
+ relevance: ["LOGIN"]
5533
5935
  },
5534
5936
  {
5535
5937
  name: "search",
@@ -5538,7 +5940,9 @@ const TOOL_DEFINITIONS = [
5538
5940
  inputSchema: {
5539
5941
  query: zod.z.string().describe("Search query text"),
5540
5942
  selector: zod.z.string().optional().describe("CSS selector for search input (auto-detected if omitted)")
5541
- }
5943
+ },
5944
+ tier: 1,
5945
+ relevance: ["SEARCH_READY", "SEARCH_RESULTS"]
5542
5946
  },
5543
5947
  {
5544
5948
  name: "paginate",
@@ -5546,8 +5950,62 @@ const TOOL_DEFINITIONS = [
5546
5950
  description: "Navigate to the next or previous page of results. Auto-detects pagination controls.",
5547
5951
  inputSchema: {
5548
5952
  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)")
5550
- }
5953
+ selector: zod.z.string().optional().describe(
5954
+ "CSS selector for pagination link (auto-detected if omitted)"
5955
+ )
5956
+ },
5957
+ tier: 1,
5958
+ relevance: ["SEARCH_RESULTS", "PAGINATED_LIST"]
5959
+ },
5960
+ // --- Speedee System: Expanded Macros ---
5961
+ {
5962
+ name: "accept_cookies",
5963
+ title: "Accept Cookies",
5964
+ description: "Dismiss cookie consent banners (OneTrust, CookieBot, GDPR popups, etc.). More targeted than dismiss_popup for consent-specific overlays.",
5965
+ tier: 1
5966
+ },
5967
+ {
5968
+ name: "extract_table",
5969
+ title: "Extract Table",
5970
+ description: "Extract a table from the page as structured JSON rows. Returns column headers and cell values.",
5971
+ inputSchema: {
5972
+ index: zod.z.number().optional().describe("Element index of the table to extract"),
5973
+ selector: zod.z.string().optional().describe(
5974
+ "CSS selector for the table (auto-detected if omitted — uses first table)"
5975
+ )
5976
+ },
5977
+ tier: 1,
5978
+ relevance: ["SEARCH_RESULTS", "ARTICLE"]
5979
+ },
5980
+ {
5981
+ name: "scroll_to_element",
5982
+ title: "Scroll To Element",
5983
+ description: "Scroll a specific element into view by index or selector. Useful for navigating to off-screen content.",
5984
+ inputSchema: {
5985
+ index: zod.z.number().optional().describe("Element index to scroll to"),
5986
+ selector: zod.z.string().optional().describe("CSS selector to scroll to"),
5987
+ position: zod.z.enum(["center", "top", "bottom"]).optional().describe(
5988
+ "Where to position the element in the viewport (default center)"
5989
+ )
5990
+ },
5991
+ tier: 1
5992
+ },
5993
+ // --- Navigation Primitives ---
5994
+ {
5995
+ name: "wait_for_navigation",
5996
+ title: "Wait For Navigation",
5997
+ description: "Wait for the current page to finish loading after a click or form submission. Use when you clicked a link and need to wait for the new page before reading it.",
5998
+ inputSchema: {
5999
+ timeoutMs: zod.z.number().optional().describe("Maximum time to wait in milliseconds (default 10000)")
6000
+ },
6001
+ tier: 1
6002
+ },
6003
+ // --- Speedee System: Metrics ---
6004
+ {
6005
+ name: "metrics",
6006
+ title: "Session Metrics",
6007
+ description: "Show performance metrics for this session: total tool calls, average duration, per-tool breakdown, and error rates.",
6008
+ tier: 2
5551
6009
  }
5552
6010
  ];
5553
6011
  function toAnthropicTools(defs) {
@@ -5572,6 +6030,74 @@ function toAnthropicTools(defs) {
5572
6030
  });
5573
6031
  }
5574
6032
  const AGENT_TOOLS = toAnthropicTools(TOOL_DEFINITIONS);
6033
+ const defByName = Object.fromEntries(
6034
+ TOOL_DEFINITIONS.map((d) => [d.name, d])
6035
+ );
6036
+ const CONTEXT_HINTS = {
6037
+ LOGIN: {
6038
+ login: "⚡ LOGIN PAGE DETECTED — ",
6039
+ fill_form: "⚡ Login fields detected — ",
6040
+ type_text: "⚡ Credential fields on page — ",
6041
+ save_session: "💡 Save session after successful login — "
6042
+ },
6043
+ SEARCH_READY: {
6044
+ search: "⚡ SEARCH BOX DETECTED — ",
6045
+ type_text: "⚡ Search input available — "
6046
+ },
6047
+ SEARCH_RESULTS: {
6048
+ paginate: "⚡ PAGINATION DETECTED — ",
6049
+ search: "⚡ Refine search — ",
6050
+ highlight: "💡 Mark interesting results — "
6051
+ },
6052
+ SHOPPING: {
6053
+ fill_form: "⚡ CHECKOUT FIELDS DETECTED — ",
6054
+ select_option: "⚡ Payment/shipping options available — "
6055
+ },
6056
+ FORM: {
6057
+ fill_form: "⚡ FORM DETECTED — ",
6058
+ select_option: "⚡ Dropdown fields on page — ",
6059
+ submit_form: "⚡ Form ready to submit — "
6060
+ },
6061
+ PAGINATED_LIST: {
6062
+ paginate: "⚡ PAGINATION DETECTED — ",
6063
+ scroll: "💡 Scroll to see more — "
6064
+ },
6065
+ ARTICLE: {
6066
+ highlight: "💡 Mark interesting passages — ",
6067
+ save_bookmark: "💡 Save for later — ",
6068
+ scroll: "💡 Long content — scroll to continue — "
6069
+ }
6070
+ };
6071
+ function scoreForContext(toolName, pageType) {
6072
+ const def = defByName[toolName];
6073
+ if (!def) return 500;
6074
+ const tier = def.tier ?? 1;
6075
+ if (tier === 0) return 0;
6076
+ const isRelevant = !def.relevance || def.relevance.includes(pageType);
6077
+ if (tier === 1 && isRelevant) return 10;
6078
+ if (tier === 1 && !isRelevant) return 30;
6079
+ if (tier === 2 && isRelevant) return 20;
6080
+ return 40;
6081
+ }
6082
+ function pruneToolsForContext(tools, pageType) {
6083
+ const ctx = pageType ?? "GENERAL";
6084
+ const hints = CONTEXT_HINTS[ctx] ?? {};
6085
+ const scored = tools.map((tool) => ({
6086
+ tool,
6087
+ score: scoreForContext(tool.name, ctx)
6088
+ }));
6089
+ scored.sort((a, b) => a.score - b.score);
6090
+ return scored.map(({ tool, score }) => {
6091
+ const hint = hints[tool.name];
6092
+ if (hint && score <= 20) {
6093
+ return {
6094
+ ...tool,
6095
+ description: hint + tool.description
6096
+ };
6097
+ }
6098
+ return tool;
6099
+ });
6100
+ }
5575
6101
  function trimText(value) {
5576
6102
  return typeof value === "string" ? value.trim() : "";
5577
6103
  }
@@ -6436,29 +6962,82 @@ function deleteNamedSession(name) {
6436
6962
  fs$1.unlinkSync(filePath);
6437
6963
  return true;
6438
6964
  }
6965
+ const DEFAULT_PAGE_SCRIPT_TIMEOUT_MS = 1500;
6966
+ const QUIET_NAVIGATION_WINDOW_MS = 1200;
6967
+ const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
6968
+ function pageBusyError(action) {
6969
+ return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
6970
+ }
6971
+ function normalizeReadPageMode(mode, pageContent) {
6972
+ if (typeof mode === "string") {
6973
+ const normalized = mode.trim().toLowerCase();
6974
+ if (normalized === "debug") return "debug";
6975
+ if (normalized === "full" || normalized === "summary" || normalized === "interactives_only" || normalized === "forms_only" || normalized === "text_only" || normalized === "visible_only" || normalized === "results_only") {
6976
+ return normalized;
6977
+ }
6978
+ }
6979
+ return pageContent ? chooseAgentReadMode(pageContent) : "visible_only";
6980
+ }
6981
+ async function executePageScript(wc, script, options) {
6982
+ if (wc.isDestroyed()) return null;
6983
+ const timeoutMs = Math.max(
6984
+ 150,
6985
+ options?.timeoutMs ?? DEFAULT_PAGE_SCRIPT_TIMEOUT_MS
6986
+ );
6987
+ let timer = null;
6988
+ try {
6989
+ const result = await Promise.race([
6990
+ wc.executeJavaScript(script, options?.userGesture ?? false),
6991
+ new Promise((resolve) => {
6992
+ timer = setTimeout(() => resolve(PAGE_SCRIPT_TIMEOUT), timeoutMs);
6993
+ })
6994
+ ]);
6995
+ if (result === PAGE_SCRIPT_TIMEOUT) {
6996
+ console.log(
6997
+ `[Vessel pageScript] timed out after ${timeoutMs}ms (${options?.label || "page-script"})`
6998
+ );
6999
+ return PAGE_SCRIPT_TIMEOUT;
7000
+ }
7001
+ return result;
7002
+ } catch {
7003
+ return null;
7004
+ } finally {
7005
+ if (timer) {
7006
+ clearTimeout(timer);
7007
+ }
7008
+ }
7009
+ }
6439
7010
  function waitForLoad$1(wc, timeout = 5e3) {
6440
7011
  return new Promise((resolve) => {
6441
7012
  let finished = false;
7013
+ console.log(
7014
+ `[Vessel waitForLoad] started, isLoading=${wc.isLoading()}, timeout=${timeout}`
7015
+ );
6442
7016
  const cleanup = () => {
6443
7017
  wc.removeListener("did-finish-load", onLoadEvent);
6444
7018
  wc.removeListener("did-stop-loading", onLoadEvent);
6445
7019
  wc.removeListener("did-fail-load", onLoadEvent);
6446
7020
  };
6447
- const finish = () => {
7021
+ const finish = (reason) => {
6448
7022
  if (finished) return;
6449
7023
  finished = true;
7024
+ console.log(`[Vessel waitForLoad] finished: ${reason}`);
6450
7025
  clearTimeout(timer);
6451
7026
  cleanup();
6452
7027
  resolve();
6453
7028
  };
6454
7029
  const onLoadEvent = () => {
6455
- if (!wc.isLoading()) {
6456
- finish();
7030
+ const loading = wc.isLoading();
7031
+ console.log(
7032
+ `[Vessel waitForLoad] load event fired, isLoading=${loading}`
7033
+ );
7034
+ if (!loading) {
7035
+ finish("load event");
6457
7036
  }
6458
7037
  };
6459
- const timer = setTimeout(finish, timeout);
7038
+ const timer = setTimeout(() => finish("timeout"), timeout);
6460
7039
  if (!wc.isLoading()) {
6461
- finish();
7040
+ finish("already loaded");
6462
7041
  return;
6463
7042
  }
6464
7043
  wc.on("did-finish-load", onLoadEvent);
@@ -6466,41 +7045,70 @@ function waitForLoad$1(wc, timeout = 5e3) {
6466
7045
  wc.on("did-fail-load", onLoadEvent);
6467
7046
  });
6468
7047
  }
6469
- function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 4e3) {
7048
+ function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
6470
7049
  return new Promise((resolve) => {
6471
7050
  let done = false;
7051
+ let waitingForLoad = false;
7052
+ const beforeTitle = wc.getTitle();
6472
7053
  const finish = () => {
6473
7054
  if (done) return;
6474
7055
  done = true;
6475
7056
  clearTimeout(timer);
7057
+ clearInterval(poller);
6476
7058
  wc.removeListener("did-start-loading", onStart);
6477
7059
  wc.removeListener("did-navigate", onNavigate);
6478
7060
  wc.removeListener("did-navigate-in-page", onNavigateInPage);
7061
+ wc.removeListener("did-stop-loading", onNativeChange);
7062
+ wc.removeListener("page-title-updated", onNativeChange);
6479
7063
  resolve();
6480
7064
  };
6481
- const onStart = () => {
6482
- wc.removeListener("did-navigate", onNavigate);
6483
- wc.once("did-navigate", () => {
6484
- void waitForLoad$1(wc, timeout).then(finish);
6485
- });
7065
+ const finishAfterLoad = () => {
7066
+ if (waitingForLoad) return;
7067
+ waitingForLoad = true;
6486
7068
  void waitForLoad$1(wc, timeout).then(finish);
6487
7069
  };
7070
+ const onNativeChange = () => {
7071
+ if (wc.isLoading()) {
7072
+ finishAfterLoad();
7073
+ return;
7074
+ }
7075
+ if (wc.getURL() !== beforeUrl || wc.getTitle() !== beforeTitle) {
7076
+ finish();
7077
+ }
7078
+ };
7079
+ const onStart = () => {
7080
+ finishAfterLoad();
7081
+ };
6488
7082
  const onNavigate = () => {
6489
- void waitForLoad$1(wc, timeout).then(finish);
7083
+ finishAfterLoad();
6490
7084
  };
6491
7085
  const onNavigateInPage = () => finish();
6492
- const timer = setTimeout(finish, timeout);
6493
- if (wc.getURL() !== beforeUrl || wc.isLoading()) {
6494
- void waitForLoad$1(wc, timeout).then(finish);
7086
+ const timer = setTimeout(
7087
+ finish,
7088
+ Math.min(timeout, QUIET_NAVIGATION_WINDOW_MS)
7089
+ );
7090
+ const poller = setInterval(onNativeChange, 100);
7091
+ if (wc.getURL() !== beforeUrl || wc.getTitle() !== beforeTitle || wc.isLoading()) {
7092
+ onNativeChange();
6495
7093
  return;
6496
7094
  }
6497
7095
  wc.once("did-start-loading", onStart);
6498
7096
  wc.once("did-navigate", onNavigate);
6499
7097
  wc.once("did-navigate-in-page", onNavigateInPage);
7098
+ wc.on("did-stop-loading", onNativeChange);
7099
+ wc.on("page-title-updated", onNativeChange);
6500
7100
  });
6501
7101
  }
7102
+ function getPostNavSummary(wc) {
7103
+ const title = wc.getTitle();
7104
+ return title ? `
7105
+ Page title: ${title}` : "";
7106
+ }
6502
7107
  async function scrollPage$1(wc, deltaY) {
6503
- const getScrollY = () => wc.executeJavaScript(`
7108
+ const getScrollY = async () => {
7109
+ const scrollY = await executePageScript(
7110
+ wc,
7111
+ `
6504
7112
  (function() {
6505
7113
  return Math.max(
6506
7114
  window.scrollY || 0,
@@ -6510,9 +7118,28 @@ async function scrollPage$1(wc, deltaY) {
6510
7118
  document.body?.scrollTop || 0,
6511
7119
  );
6512
7120
  })()
6513
- `);
7121
+ `,
7122
+ {
7123
+ label: "read scroll position"
7124
+ }
7125
+ );
7126
+ return typeof scrollY === "number" ? scrollY : 0;
7127
+ };
6514
7128
  const beforeY = await getScrollY();
6515
- await wc.executeJavaScript(`window.scrollBy(0, ${deltaY})`);
7129
+ const scrolled = await executePageScript(
7130
+ wc,
7131
+ `window.scrollBy(0, ${deltaY})`,
7132
+ {
7133
+ label: "scroll page"
7134
+ }
7135
+ );
7136
+ if (scrolled === PAGE_SCRIPT_TIMEOUT) {
7137
+ return {
7138
+ beforeY,
7139
+ afterY: beforeY,
7140
+ movedY: 0
7141
+ };
7142
+ }
6516
7143
  await sleep$1(100);
6517
7144
  const afterY = await getScrollY();
6518
7145
  return {
@@ -6525,7 +7152,9 @@ function sleep$1(ms) {
6525
7152
  return new Promise((resolve) => setTimeout(resolve, ms));
6526
7153
  }
6527
7154
  async function clickElement$1(wc, selector) {
6528
- const target = await wc.executeJavaScript(`
7155
+ const target = await executePageScript(
7156
+ wc,
7157
+ `
6529
7158
  (async function() {
6530
7159
  function matchesTarget(candidate, el) {
6531
7160
  return !!candidate && (candidate === el || el.contains(candidate) || candidate.contains(el));
@@ -6589,7 +7218,15 @@ async function clickElement$1(wc, selector) {
6589
7218
  hiddenWindow: document.visibilityState !== "visible",
6590
7219
  };
6591
7220
  })()
6592
- `);
7221
+ `,
7222
+ {
7223
+ timeoutMs: 2e3,
7224
+ label: "resolve click target"
7225
+ }
7226
+ );
7227
+ if (target === PAGE_SCRIPT_TIMEOUT) {
7228
+ return pageBusyError("click");
7229
+ }
6593
7230
  if (!target || typeof target !== "object") {
6594
7231
  return "Error: Could not resolve click target";
6595
7232
  }
@@ -6619,7 +7256,9 @@ async function clickElement$1(wc, selector) {
6619
7256
  return target.obstructed ? "Clicked via pointer events (target may be partially obstructed)" : "Clicked via pointer events";
6620
7257
  }
6621
7258
  async function activateElement$1(wc, selector) {
6622
- const activated = await wc.executeJavaScript(`
7259
+ const activated = await executePageScript(
7260
+ wc,
7261
+ `
6623
7262
  (function() {
6624
7263
  const el = document.querySelector(${JSON.stringify(selector)});
6625
7264
  if (!el) return { error: "Element not found" };
@@ -6632,7 +7271,14 @@ async function activateElement$1(wc, selector) {
6632
7271
  }
6633
7272
  return { error: "Element is not clickable" };
6634
7273
  })()
6635
- `);
7274
+ `,
7275
+ {
7276
+ label: "activate element"
7277
+ }
7278
+ );
7279
+ if (activated === PAGE_SCRIPT_TIMEOUT) {
7280
+ return pageBusyError("activate");
7281
+ }
6636
7282
  if (!activated || typeof activated !== "object") {
6637
7283
  return "Error: Could not activate element";
6638
7284
  }
@@ -6642,7 +7288,9 @@ async function activateElement$1(wc, selector) {
6642
7288
  return "Activated element via DOM click";
6643
7289
  }
6644
7290
  async function describeElementForClick$1(wc, selector) {
6645
- const result = await wc.executeJavaScript(`
7291
+ const result = await executePageScript(
7292
+ wc,
7293
+ `
6646
7294
  (function() {
6647
7295
  const el = document.querySelector(${JSON.stringify(selector)});
6648
7296
  if (!el) return { error: "Element not found" };
@@ -6653,7 +7301,14 @@ async function describeElementForClick$1(wc, selector) {
6653
7301
  href: anchor instanceof HTMLAnchorElement ? anchor.href : undefined,
6654
7302
  };
6655
7303
  })()
6656
- `);
7304
+ `,
7305
+ {
7306
+ label: "describe element"
7307
+ }
7308
+ );
7309
+ if (result === PAGE_SCRIPT_TIMEOUT) {
7310
+ return { error: "Page is still busy" };
7311
+ }
6657
7312
  if (!result || typeof result !== "object") {
6658
7313
  return { error: "Element not found" };
6659
7314
  }
@@ -6669,9 +7324,36 @@ async function clickResolvedSelector$1(wc, selector) {
6669
7324
  if (selector.startsWith("__vessel_idx:")) {
6670
7325
  const idx = Number(selector.slice("__vessel_idx:".length));
6671
7326
  const beforeUrl2 = wc.getURL();
6672
- const result = await wc.executeJavaScript(
6673
- `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`
7327
+ const result = await executePageScript(
7328
+ wc,
7329
+ `window.__vessel?.interactByIndex?.(${idx}, "click") || "Error: interactByIndex not available"`,
7330
+ {
7331
+ label: "shadow click by index"
7332
+ }
7333
+ );
7334
+ if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
7335
+ if (typeof result === "string" && result.startsWith("Error")) return result;
7336
+ await waitForPotentialNavigation$1(wc, beforeUrl2);
7337
+ const afterUrl2 = wc.getURL();
7338
+ return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
7339
+ }
7340
+ if (selector.includes(" >>> ")) {
7341
+ const beforeUrl2 = wc.getURL();
7342
+ const result = await executePageScript(
7343
+ wc,
7344
+ `
7345
+ (function() {
7346
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
7347
+ if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
7348
+ if (el instanceof HTMLElement) { el.focus(); el.click(); }
7349
+ return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase());
7350
+ })()
7351
+ `,
7352
+ {
7353
+ label: "shadow click selector"
7354
+ }
6674
7355
  );
7356
+ if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
6675
7357
  if (typeof result === "string" && result.startsWith("Error")) return result;
6676
7358
  await waitForPotentialNavigation$1(wc, beforeUrl2);
6677
7359
  const afterUrl2 = wc.getURL();
@@ -6710,7 +7392,9 @@ async function dismissPopup$1(wc) {
6710
7392
  (overlay) => overlay.blocksInteraction
6711
7393
  ).length;
6712
7394
  const initialDormant = before.dormantOverlays.length;
6713
- const candidates = await wc.executeJavaScript(`
7395
+ const candidates = await executePageScript(
7396
+ wc,
7397
+ `
6714
7398
  (function() {
6715
7399
  function text(value) {
6716
7400
  const trimmed = value == null ? "" : String(value).trim();
@@ -6889,7 +7573,15 @@ async function dismissPopup$1(wc) {
6889
7573
  .sort((a, b) => b.score - a.score)
6890
7574
  .slice(0, 8);
6891
7575
  })()
6892
- `);
7576
+ `,
7577
+ {
7578
+ timeoutMs: 2e3,
7579
+ label: "inspect popup candidates"
7580
+ }
7581
+ );
7582
+ if (candidates === PAGE_SCRIPT_TIMEOUT) {
7583
+ return pageBusyError("dismiss_popup");
7584
+ }
6893
7585
  if (Array.isArray(candidates)) {
6894
7586
  for (const candidate of candidates) {
6895
7587
  if (!candidate || typeof candidate !== "object" || typeof candidate.selector !== "string") {
@@ -6924,7 +7616,8 @@ async function dismissPopup$1(wc) {
6924
7616
  async function resolveSelector$1(wc, index, selector) {
6925
7617
  if (selector) return selector;
6926
7618
  if (index == null) return null;
6927
- const authoritativeSelector = await wc.executeJavaScript(
7619
+ const authoritativeSelector = await executePageScript(
7620
+ wc,
6928
7621
  `
6929
7622
  (function() {
6930
7623
  return window.__vessel?.getElementSelector
@@ -6934,16 +7627,23 @@ async function resolveSelector$1(wc, index, selector) {
6934
7627
  `
6935
7628
  );
6936
7629
  if (typeof authoritativeSelector === "string" && authoritativeSelector) {
6937
- const resolves = await wc.executeJavaScript(
7630
+ if (authoritativeSelector.includes(" >>> ")) {
7631
+ const resolves2 = await executePageScript(
7632
+ wc,
7633
+ `!!window.__vessel?.resolveShadowSelector?.(${JSON.stringify(authoritativeSelector)})`
7634
+ );
7635
+ if (resolves2) return authoritativeSelector;
7636
+ return `__vessel_idx:${index}`;
7637
+ }
7638
+ const resolves = await executePageScript(
7639
+ wc,
6938
7640
  `!!document.querySelector(${JSON.stringify(authoritativeSelector)})`
6939
7641
  );
6940
7642
  if (resolves) return authoritativeSelector;
6941
7643
  return `__vessel_idx:${index}`;
6942
7644
  }
6943
- const page = await extractContent(wc);
6944
- const extractedSelector = findSelectorByIndex(page, index);
6945
- if (extractedSelector) return extractedSelector;
6946
- return wc.executeJavaScript(
7645
+ const fallbackSelector = await executePageScript(
7646
+ wc,
6947
7647
  `
6948
7648
  (function() {
6949
7649
  // Final fallback: replicate the legacy extraction order.
@@ -7019,6 +7719,13 @@ async function resolveSelector$1(wc, index, selector) {
7019
7719
  })()
7020
7720
  `
7021
7721
  );
7722
+ if (typeof fallbackSelector === "string" && fallbackSelector) {
7723
+ return fallbackSelector;
7724
+ }
7725
+ const page = await extractContent(wc);
7726
+ const extractedSelector = findSelectorByIndex(page, index);
7727
+ if (extractedSelector) return extractedSelector;
7728
+ return null;
7022
7729
  }
7023
7730
  function getTabByMatch$1(tabManager, match) {
7024
7731
  if (!match) return null;
@@ -7049,11 +7756,38 @@ function isDangerousAction$1(name) {
7049
7756
  async function setElementValue$1(wc, selector, value) {
7050
7757
  if (selector.startsWith("__vessel_idx:")) {
7051
7758
  const idx = Number(selector.slice("__vessel_idx:".length));
7052
- return wc.executeJavaScript(
7759
+ const result2 = await executePageScript(
7760
+ wc,
7053
7761
  `window.__vessel?.interactByIndex?.(${idx}, "value", ${JSON.stringify(value)}) || "Error: interactByIndex not available"`
7054
7762
  );
7763
+ return result2 === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result2 || "Error: interactByIndex not available";
7055
7764
  }
7056
- return wc.executeJavaScript(`
7765
+ if (selector.includes(" >>> ")) {
7766
+ const result2 = await executePageScript(
7767
+ wc,
7768
+ `
7769
+ (function() {
7770
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
7771
+ if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
7772
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return "Error[not-input]: Element is not a text input";
7773
+ var proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
7774
+ var desc = Object.getOwnPropertyDescriptor(proto, "value");
7775
+ if (desc && desc.set) { desc.set.call(el, ${JSON.stringify(value)}); } else { el.value = ${JSON.stringify(value)}; }
7776
+ el.focus();
7777
+ el.dispatchEvent(new Event("input", { bubbles: true }));
7778
+ el.dispatchEvent(new Event("change", { bubbles: true }));
7779
+ return "Typed into: " + (el.getAttribute("aria-label") || el.placeholder || el.name || "input");
7780
+ })()
7781
+ `,
7782
+ {
7783
+ label: "type text in shadow input"
7784
+ }
7785
+ );
7786
+ return result2 === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result2 || "Error: Could not type into element";
7787
+ }
7788
+ const result = await executePageScript(
7789
+ wc,
7790
+ `
7057
7791
  (function() {
7058
7792
  const el = document.querySelector(${JSON.stringify(selector)});
7059
7793
  if (!el) return 'Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh.';
@@ -7086,10 +7820,17 @@ async function setElementValue$1(wc, selector, value) {
7086
7820
  (el.getAttribute('aria-label') || el.placeholder || el.name || 'input') +
7087
7821
  ' = ' + (el.type === 'password' ? '[hidden]' : String(el.value).slice(0, 80));
7088
7822
  })()
7089
- `);
7823
+ `,
7824
+ {
7825
+ label: "type text"
7826
+ }
7827
+ );
7828
+ return result === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result || "Error: Could not type into element";
7090
7829
  }
7091
7830
  async function typeKeystroke$1(wc, selector, value) {
7092
- return wc.executeJavaScript(`
7831
+ const result = await executePageScript(
7832
+ wc,
7833
+ `
7093
7834
  (async function() {
7094
7835
  const el = document.querySelector(${JSON.stringify(selector)});
7095
7836
  if (!el) return 'Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh.';
@@ -7126,7 +7867,13 @@ async function typeKeystroke$1(wc, selector, value) {
7126
7867
  (el.getAttribute('aria-label') || el.placeholder || el.name || 'input') +
7127
7868
  ' = ' + (el.type === 'password' ? '[hidden]' : String(el.value).slice(0, 80));
7128
7869
  })()
7129
- `);
7870
+ `,
7871
+ {
7872
+ timeoutMs: 2e3,
7873
+ label: "type keystrokes"
7874
+ }
7875
+ );
7876
+ return result === PAGE_SCRIPT_TIMEOUT ? pageBusyError("type_text") : result || "Error: Could not type into element";
7130
7877
  }
7131
7878
  async function hoverElement$1(wc, selector) {
7132
7879
  const pos = await wc.executeJavaScript(`
@@ -7152,7 +7899,8 @@ async function hoverElement$1(wc, selector) {
7152
7899
  if ("error" in pos && typeof pos.error === "string") return pos.error;
7153
7900
  const x = typeof pos.x === "number" ? pos.x : null;
7154
7901
  const y = typeof pos.y === "number" ? pos.y : null;
7155
- if (x == null || y == null) return "Error: Could not resolve hover coordinates";
7902
+ if (x == null || y == null)
7903
+ return "Error: Could not resolve hover coordinates";
7156
7904
  wc.sendInputEvent({ type: "mouseMove", x, y });
7157
7905
  const label = typeof pos.label === "string" ? pos.label : "element";
7158
7906
  return `Hovered: ${label}`;
@@ -7183,7 +7931,9 @@ async function waitForCondition$1(wc, args) {
7183
7931
  }
7184
7932
  const startedAt = Date.now();
7185
7933
  while (Date.now() - startedAt < timeoutMs) {
7186
- const result = await wc.executeJavaScript(`
7934
+ const result = await executePageScript(
7935
+ wc,
7936
+ `
7187
7937
  (function() {
7188
7938
  var selector = ${JSON.stringify(selector)};
7189
7939
  var text = ${JSON.stringify(text)};
@@ -7197,7 +7947,14 @@ async function waitForCondition$1(wc, args) {
7197
7947
  if (text && document.body && document.body.innerText && document.body.innerText.includes(text)) return 'text';
7198
7948
  return '';
7199
7949
  })()
7200
- `);
7950
+ `,
7951
+ {
7952
+ label: "wait_for probe"
7953
+ }
7954
+ );
7955
+ if (result === PAGE_SCRIPT_TIMEOUT) {
7956
+ return pageBusyError("wait_for");
7957
+ }
7201
7958
  if (result === "selector") {
7202
7959
  return `Matched selector ${selector}`;
7203
7960
  }
@@ -7281,7 +8038,9 @@ ${formatFolderStatus$1()}`;
7281
8038
  async function selectOption$1(wc, args) {
7282
8039
  const selector = await resolveSelector$1(wc, args.index, args.selector);
7283
8040
  if (!selector) return "Error: No select element index or selector provided";
7284
- return wc.executeJavaScript(`
8041
+ const result = await executePageScript(
8042
+ wc,
8043
+ `
7285
8044
  (function() {
7286
8045
  const el = document.querySelector(${JSON.stringify(selector)});
7287
8046
  if (!(el instanceof HTMLSelectElement)) {
@@ -7305,13 +8064,20 @@ async function selectOption$1(wc, args) {
7305
8064
  el.dispatchEvent(new Event('change', { bubbles: true }));
7306
8065
  return 'Selected: ' + ((option.textContent || option.value).trim().slice(0, 100));
7307
8066
  })()
7308
- `);
8067
+ `,
8068
+ {
8069
+ label: "select option"
8070
+ }
8071
+ );
8072
+ return result === PAGE_SCRIPT_TIMEOUT ? pageBusyError("select_option") : result || "Error: Could not select option";
7309
8073
  }
7310
8074
  async function submitForm$1(wc, args) {
7311
8075
  const beforeUrl = wc.getURL();
7312
8076
  let selector = await resolveSelector$1(wc, args.index, args.selector);
7313
8077
  if (!selector) {
7314
- selector = await wc.executeJavaScript(`
8078
+ const discoveredSelector = await executePageScript(
8079
+ wc,
8080
+ `
7315
8081
  (function() {
7316
8082
  var forms = document.querySelectorAll('form');
7317
8083
  for (var i = 0; i < forms.length; i++) {
@@ -7321,10 +8087,20 @@ async function submitForm$1(wc, args) {
7321
8087
  }
7322
8088
  return forms.length > 0 ? 'form' : null;
7323
8089
  })()
7324
- `);
8090
+ `,
8091
+ {
8092
+ label: "discover form"
8093
+ }
8094
+ );
8095
+ if (discoveredSelector === PAGE_SCRIPT_TIMEOUT) {
8096
+ return pageBusyError("submit_form");
8097
+ }
8098
+ selector = discoveredSelector || null;
7325
8099
  if (!selector) return "Error: No form found on the page";
7326
8100
  }
7327
- const formInfo = await wc.executeJavaScript(`
8101
+ const formInfo = await executePageScript(
8102
+ wc,
8103
+ `
7328
8104
  (function() {
7329
8105
  const target = document.querySelector(${JSON.stringify(selector)});
7330
8106
  if (!target) return { error: 'Target not found' };
@@ -7418,7 +8194,18 @@ async function submitForm$1(wc, args) {
7418
8194
  form.submit();
7419
8195
  return { submitted: true, method };
7420
8196
  })()
7421
- `);
8197
+ `,
8198
+ {
8199
+ timeoutMs: 2e3,
8200
+ label: "submit form"
8201
+ }
8202
+ );
8203
+ if (formInfo === PAGE_SCRIPT_TIMEOUT) {
8204
+ return pageBusyError("submit_form");
8205
+ }
8206
+ if (!formInfo || typeof formInfo !== "object") {
8207
+ return "Error: Could not inspect form";
8208
+ }
7422
8209
  if (formInfo.error) return formInfo.error;
7423
8210
  if (formInfo.found && formInfo.method === "GET") {
7424
8211
  const url = new URL(formInfo.action);
@@ -7433,7 +8220,37 @@ async function submitForm$1(wc, args) {
7433
8220
  if (formInfo.submitted) {
7434
8221
  await waitForPotentialNavigation$1(wc, beforeUrl);
7435
8222
  const afterUrl = wc.getURL();
7436
- return afterUrl !== beforeUrl ? `Submitted form via ${formInfo.method} -> ${afterUrl}` : `Submitted form via ${formInfo.method}`;
8223
+ if (afterUrl !== beforeUrl) {
8224
+ return `Submitted form via ${formInfo.method} -> ${afterUrl}`;
8225
+ }
8226
+ await executePageScript(
8227
+ wc,
8228
+ `
8229
+ (function() {
8230
+ var active = document.activeElement;
8231
+ if (!active || active === document.body) {
8232
+ var inputs = document.querySelectorAll('input[type="text"], input[type="search"], input:not([type])');
8233
+ for (var i = 0; i < inputs.length; i++) {
8234
+ if (inputs[i].value) { active = inputs[i]; active.focus(); break; }
8235
+ }
8236
+ }
8237
+ if (active && active !== document.body) {
8238
+ active.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true, cancelable: true }));
8239
+ active.dispatchEvent(new KeyboardEvent('keypress', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true, cancelable: true }));
8240
+ active.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true, cancelable: true }));
8241
+ }
8242
+ })()
8243
+ `,
8244
+ {
8245
+ label: "submit form enter fallback"
8246
+ }
8247
+ );
8248
+ wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
8249
+ await new Promise((r) => setTimeout(r, 50));
8250
+ wc.sendInputEvent({ type: "keyUp", keyCode: "Return" });
8251
+ await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
8252
+ const finalUrl = wc.getURL();
8253
+ return finalUrl !== beforeUrl ? `Submitted form (Enter fallback) -> ${finalUrl}` : `Submitted form via ${formInfo.method} (page may have updated dynamically)`;
7437
8254
  }
7438
8255
  return "Submitted form";
7439
8256
  }
@@ -7441,30 +8258,52 @@ async function pressKey$1(wc, args) {
7441
8258
  const key = typeof args.key === "string" ? args.key.trim() : "";
7442
8259
  if (!key) return "Error: No key provided";
7443
8260
  const selector = await resolveSelector$1(wc, args.index, args.selector);
7444
- return wc.executeJavaScript(`
8261
+ const focusResult = await executePageScript(
8262
+ wc,
8263
+ `
7445
8264
  (function() {
7446
- const key = ${JSON.stringify(key)};
7447
8265
  const selector = ${JSON.stringify(selector)};
7448
8266
  const target =
7449
8267
  selector ? document.querySelector(selector) : document.activeElement;
7450
8268
  if (!target || !(target instanceof HTMLElement)) {
7451
- return selector ? 'Target not found' : 'No focused element';
7452
- }
7453
- target.focus();
7454
- const eventInit = { key, bubbles: true, cancelable: true };
7455
- target.dispatchEvent(new KeyboardEvent('keydown', eventInit));
7456
- target.dispatchEvent(new KeyboardEvent('keypress', eventInit));
7457
- const tag = target.tagName;
7458
- const type = target instanceof HTMLInputElement ? target.type : '';
7459
- if (key === 'Enter' &&
7460
- typeof target.click === 'function' &&
7461
- (tag === 'BUTTON' || (tag === 'INPUT' && (type === 'submit' || type === 'button')))) {
7462
- target.click();
8269
+ return { error: selector ? 'Target not found' : 'No focused element' };
7463
8270
  }
7464
- target.dispatchEvent(new KeyboardEvent('keyup', eventInit));
7465
- return 'Pressed key: ' + key;
8271
+ target.focus({ preventScroll: false });
8272
+ return {
8273
+ ok: true,
8274
+ label:
8275
+ target.getAttribute('aria-label') ||
8276
+ target.getAttribute('name') ||
8277
+ target.getAttribute('placeholder') ||
8278
+ target.textContent?.trim().slice(0, 60) ||
8279
+ target.tagName.toLowerCase(),
8280
+ };
7466
8281
  })()
7467
- `);
8282
+ `,
8283
+ {
8284
+ label: "focus before key press"
8285
+ }
8286
+ );
8287
+ if (focusResult === PAGE_SCRIPT_TIMEOUT) {
8288
+ return pageBusyError("press_key");
8289
+ }
8290
+ if (!focusResult || typeof focusResult !== "object") {
8291
+ return "Error: Could not prepare key press";
8292
+ }
8293
+ if ("error" in focusResult && typeof focusResult.error === "string") {
8294
+ return focusResult.error;
8295
+ }
8296
+ wc.focus();
8297
+ const normalizedKey = key.length === 1 ? key : key[0].toUpperCase() + key.slice(1);
8298
+ const electronKeyCode = normalizedKey === "Enter" ? "Return" : normalizedKey === "ArrowUp" ? "Up" : normalizedKey === "ArrowDown" ? "Down" : normalizedKey === "ArrowLeft" ? "Left" : normalizedKey === "ArrowRight" ? "Right" : normalizedKey;
8299
+ wc.sendInputEvent({ type: "keyDown", keyCode: electronKeyCode });
8300
+ if (key.length === 1) {
8301
+ wc.sendInputEvent({ type: "char", keyCode: key });
8302
+ }
8303
+ await sleep$1(16);
8304
+ wc.sendInputEvent({ type: "keyUp", keyCode: electronKeyCode });
8305
+ const label = "label" in focusResult && typeof focusResult.label === "string" ? focusResult.label : null;
8306
+ return label ? `Pressed key: ${key} on ${label}` : `Pressed key: ${key}`;
7468
8307
  }
7469
8308
  async function getPostActionState$1(ctx, name) {
7470
8309
  const tab = ctx.tabManager.getActiveTab();
@@ -7483,7 +8322,13 @@ async function getPostActionState$1(ctx, name) {
7483
8322
  "search",
7484
8323
  "paginate"
7485
8324
  ];
7486
- const interactActions = ["type_text", "select_option", "hover", "focus", "fill_form"];
8325
+ const interactActions = [
8326
+ "type_text",
8327
+ "select_option",
8328
+ "hover",
8329
+ "focus",
8330
+ "fill_form"
8331
+ ];
7487
8332
  const tabActions = [
7488
8333
  "create_tab",
7489
8334
  "switch_tab",
@@ -7491,33 +8336,11 @@ async function getPostActionState$1(ctx, name) {
7491
8336
  "load_session"
7492
8337
  ];
7493
8338
  if (navActions.includes(name)) {
7494
- let warning = "";
7495
- try {
7496
- const page = await extractContent(wc);
7497
- const issue = getRecoverableAccessIssue(page);
7498
- if (issue) {
7499
- const blockedUrl = wc.getURL();
7500
- const canRecover = [
7501
- "navigate",
7502
- "open_bookmark",
7503
- "click",
7504
- "submit_form",
7505
- "reload",
7506
- "press_key"
7507
- ].includes(name) && tab.canGoBack();
7508
- if (canRecover && tab.goBack()) {
7509
- await waitForLoad$1(wc);
7510
- warning = `
7511
- [warning: ${issue.summary} ${issue.recommendation ?? ""} Automatically returned to ${wc.getURL()} after landing on ${blockedUrl}.]`;
7512
- } else {
7513
- warning = `
7514
- [warning: ${issue.summary} ${issue.recommendation ?? ""}${tab.canGoBack() ? "" : " No previous page was available for automatic recovery."}]`;
7515
- }
7516
- }
7517
- } catch {
8339
+ if (wc.isLoading()) {
8340
+ await waitForLoad$1(wc);
7518
8341
  }
7519
- return `${warning}
7520
- [state: url=${wc.getURL()}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]`;
8342
+ return `
8343
+ [state: url=${wc.getURL()}, title=${JSON.stringify(wc.getTitle() || "")}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]`;
7521
8344
  }
7522
8345
  if (interactActions.includes(name)) {
7523
8346
  return `
@@ -7534,7 +8357,69 @@ async function getPostActionState$1(ctx, name) {
7534
8357
  }
7535
8358
  return "";
7536
8359
  }
8360
+ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
8361
+ "current_tab",
8362
+ "list_tabs",
8363
+ "switch_tab",
8364
+ "create_tab",
8365
+ "navigate",
8366
+ "go_back",
8367
+ "go_forward",
8368
+ "reload",
8369
+ "click",
8370
+ "type_text",
8371
+ "select_option",
8372
+ "submit_form",
8373
+ "press_key",
8374
+ "scroll",
8375
+ "hover",
8376
+ "focus",
8377
+ "set_ad_blocking",
8378
+ "dismiss_popup",
8379
+ "read_page",
8380
+ "wait_for",
8381
+ "create_checkpoint",
8382
+ "restore_checkpoint",
8383
+ "save_session",
8384
+ "load_session",
8385
+ "list_sessions",
8386
+ "delete_session",
8387
+ "list_bookmarks",
8388
+ "search_bookmarks",
8389
+ "create_bookmark_folder",
8390
+ "save_bookmark",
8391
+ "organize_bookmark",
8392
+ "archive_bookmark",
8393
+ "open_bookmark",
8394
+ "highlight",
8395
+ "clear_highlights",
8396
+ "flow_start",
8397
+ "flow_advance",
8398
+ "flow_status",
8399
+ "flow_end",
8400
+ "suggest",
8401
+ "fill_form",
8402
+ "login",
8403
+ "search",
8404
+ "paginate",
8405
+ "accept_cookies",
8406
+ "extract_table",
8407
+ "scroll_to_element",
8408
+ "metrics",
8409
+ "wait_for_navigation"
8410
+ ]);
7537
8411
  async function executeAction(name, args, ctx) {
8412
+ if (!KNOWN_TOOLS.has(name)) {
8413
+ for (const known of KNOWN_TOOLS) {
8414
+ if (name.startsWith(known) && name.length > known.length) {
8415
+ const remaining = name.slice(known.length);
8416
+ const otherTools = [...KNOWN_TOOLS].filter(
8417
+ (t) => remaining.includes(t)
8418
+ );
8419
+ return `Error: It looks like you tried to call multiple tools at once (${known}, ${otherTools.join(", ")}). Please call them one at a time — send one tool call per message.`;
8420
+ }
8421
+ }
8422
+ }
7538
8423
  const tab = ctx.tabManager.getActiveTab();
7539
8424
  const tabId = ctx.tabManager.getActiveTabId();
7540
8425
  if (!tab && ![
@@ -7617,6 +8502,7 @@ async function executeAction(name, args, ctx) {
7617
8502
  const created = ctx.tabManager.getActiveTab();
7618
8503
  if (created) {
7619
8504
  await waitForLoad$1(created.view.webContents);
8505
+ return `Created tab ${createdId}${getPostNavSummary(created.view.webContents)}`;
7620
8506
  }
7621
8507
  return `Created tab ${createdId}`;
7622
8508
  }
@@ -7624,7 +8510,7 @@ async function executeAction(name, args, ctx) {
7624
8510
  if (!wc || !tabId) return "Error: No active tab";
7625
8511
  ctx.tabManager.navigateTab(tabId, args.url);
7626
8512
  await waitForLoad$1(wc);
7627
- return `Navigated to ${wc.getURL()}`;
8513
+ return `Navigated to ${wc.getURL()}${getPostNavSummary(wc)}`;
7628
8514
  }
7629
8515
  case "go_back": {
7630
8516
  if (!tab || !wc || !tabId) return "Error: No active tab";
@@ -7635,7 +8521,7 @@ async function executeAction(name, args, ctx) {
7635
8521
  ctx.tabManager.goBack(tabId);
7636
8522
  await waitForLoad$1(wc);
7637
8523
  const afterUrl = wc.getURL();
7638
- return afterUrl !== beforeUrl ? `Went back to ${afterUrl}` : `Back action completed but page stayed on ${afterUrl}`;
8524
+ return afterUrl !== beforeUrl ? `Went back to ${afterUrl}${getPostNavSummary(wc)}` : `Back action completed but page stayed on ${afterUrl}`;
7639
8525
  }
7640
8526
  case "go_forward": {
7641
8527
  if (!tab || !wc || !tabId) return "Error: No active tab";
@@ -7646,7 +8532,7 @@ async function executeAction(name, args, ctx) {
7646
8532
  ctx.tabManager.goForward(tabId);
7647
8533
  await waitForLoad$1(wc);
7648
8534
  const afterUrl = wc.getURL();
7649
- return afterUrl !== beforeUrl ? `Went forward to ${afterUrl}` : `Forward action completed but page stayed on ${afterUrl}`;
8535
+ return afterUrl !== beforeUrl ? `Went forward to ${afterUrl}${getPostNavSummary(wc)}` : `Forward action completed but page stayed on ${afterUrl}`;
7650
8536
  }
7651
8537
  case "reload": {
7652
8538
  if (!wc || !tabId) return "Error: No active tab";
@@ -7750,28 +8636,110 @@ async function executeAction(name, args, ctx) {
7750
8636
  }
7751
8637
  case "read_page": {
7752
8638
  if (!wc) return "Error: No active tab";
7753
- const content = await extractContent(wc);
7754
- const liveSelectionSection = formatLiveSelectionSection(
7755
- await captureLiveHighlightSnapshot(
7756
- wc,
7757
- getHighlightsForUrl(content.url)
7758
- )
8639
+ console.log("[Vessel read_page] starting extraction with 6s timeout");
8640
+ let content = null;
8641
+ try {
8642
+ content = await Promise.race([
8643
+ extractContent(wc),
8644
+ new Promise(
8645
+ (resolve) => setTimeout(() => {
8646
+ console.log("[Vessel read_page] timeout fired, falling back");
8647
+ resolve(null);
8648
+ }, 6e3)
8649
+ )
8650
+ ]);
8651
+ } catch {
8652
+ content = null;
8653
+ }
8654
+ console.log(
8655
+ `[Vessel read_page] extraction result: ${content ? `content=${content.content.length}` : "null (timeout)"}`
7759
8656
  );
7760
- const structured = buildStructuredContext(content);
7761
- const truncated = content.content.length > 2e4 ? content.content.slice(0, 2e4) + "\n[Content truncated...]" : content.content;
7762
- const livePrefix = liveSelectionSection ? `${liveSelectionSection}
8657
+ if (content) {
8658
+ const liveSelectionSection = formatLiveSelectionSection(
8659
+ await captureLiveHighlightSnapshot(
8660
+ wc,
8661
+ getHighlightsForUrl(content.url)
8662
+ )
8663
+ );
8664
+ const livePrefix = liveSelectionSection ? `${liveSelectionSection}
7763
8665
 
7764
8666
  ` : "";
7765
- return `${livePrefix}${structured}
8667
+ const requestedMode = normalizeReadPageMode(args.mode, content);
8668
+ if (requestedMode === "debug" || requestedMode === "full") {
8669
+ const structured = buildStructuredContext(content);
8670
+ const truncated = content.content.length > 2e4 ? content.content.slice(0, 2e4) + "\n[Content truncated...]" : content.content;
8671
+ return `${livePrefix}[read_page mode=debug]
8672
+
8673
+ ${structured}
7766
8674
 
7767
8675
  ## PAGE CONTENT
7768
8676
 
7769
8677
  ${truncated}`;
8678
+ }
8679
+ const scoped = buildScopedContext(content, requestedMode);
8680
+ return [
8681
+ livePrefix ? livePrefix.trimEnd() : "",
8682
+ `[read_page mode=${requestedMode}]`,
8683
+ "",
8684
+ scoped,
8685
+ "",
8686
+ `Need more detail? Escalate with read_page(mode="debug") only if the narrow modes are insufficient.`
8687
+ ].filter(Boolean).join("\n\n");
8688
+ }
8689
+ const title = wc.getTitle() || "(untitled)";
8690
+ const url = wc.getURL();
8691
+ return [
8692
+ `# ${title}`,
8693
+ `URL: ${url}`,
8694
+ "",
8695
+ "[Page content extraction timed out — the page JS thread is busy.]",
8696
+ "[Use the search tool to search the site, or type_text/click to interact directly.]",
8697
+ "[You can retry read_page in a few seconds once the page finishes loading.]"
8698
+ ].join("\n");
7770
8699
  }
7771
8700
  case "wait_for": {
7772
8701
  if (!wc) return "Error: No active tab";
7773
8702
  return waitForCondition$1(wc, args);
7774
8703
  }
8704
+ case "wait_for_navigation": {
8705
+ if (!wc) return "Error: No active tab";
8706
+ const timeout = typeof args.timeoutMs === "number" ? args.timeoutMs : 1e4;
8707
+ const beforeUrl = wc.getURL();
8708
+ if (wc.isLoading()) {
8709
+ await new Promise((resolve) => {
8710
+ const timer = setTimeout(resolve, timeout);
8711
+ wc.once("did-stop-loading", () => {
8712
+ clearTimeout(timer);
8713
+ resolve();
8714
+ });
8715
+ });
8716
+ } else {
8717
+ await new Promise((resolve) => {
8718
+ let navigated = false;
8719
+ const timer = setTimeout(
8720
+ () => {
8721
+ if (!navigated) resolve();
8722
+ },
8723
+ Math.min(timeout, 2e3)
8724
+ );
8725
+ wc.once("did-start-loading", () => {
8726
+ navigated = true;
8727
+ clearTimeout(timer);
8728
+ const loadTimer = setTimeout(resolve, timeout);
8729
+ wc.once("did-stop-loading", () => {
8730
+ clearTimeout(loadTimer);
8731
+ resolve();
8732
+ });
8733
+ });
8734
+ });
8735
+ }
8736
+ const afterUrl = wc.getURL();
8737
+ const title = wc.getTitle();
8738
+ if (afterUrl !== beforeUrl) {
8739
+ return `Navigation complete: ${title} (${afterUrl})`;
8740
+ }
8741
+ return `Page loaded: ${title} (${afterUrl})`;
8742
+ }
7775
8743
  case "create_checkpoint": {
7776
8744
  const checkpoint = ctx.runtime.createCheckpoint(args.name, args.note);
7777
8745
  return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
@@ -8031,12 +8999,26 @@ ${truncated}`;
8031
8999
  case "highlight": {
8032
9000
  if (!wc) return "Error: No active tab";
8033
9001
  const selector = await resolveSelector$1(wc, args.index, args.selector);
9002
+ const highlightColor = args.color || "yellow";
9003
+ const url = wc.getURL();
9004
+ if (url && url !== "about:blank") {
9005
+ const highlightText = typeof args.text === "string" ? args.text : void 0;
9006
+ addHighlight(
9007
+ url,
9008
+ typeof selector === "string" ? selector : void 0,
9009
+ highlightText,
9010
+ typeof args.label === "string" ? args.label : void 0,
9011
+ highlightColor,
9012
+ "agent"
9013
+ );
9014
+ }
8034
9015
  return highlightOnPage(
8035
9016
  wc,
8036
9017
  selector,
8037
9018
  args.text,
8038
9019
  args.label,
8039
- args.durationMs
9020
+ args.durationMs,
9021
+ highlightColor
8040
9022
  );
8041
9023
  }
8042
9024
  case "clear_highlights": {
@@ -8047,7 +9029,8 @@ ${truncated}`;
8047
9029
  case "flow_start": {
8048
9030
  const goal = typeof args.goal === "string" ? args.goal : "";
8049
9031
  const steps = Array.isArray(args.steps) ? args.steps.map(String) : [];
8050
- if (!goal || steps.length === 0) return "Error: goal and steps are required";
9032
+ if (!goal || steps.length === 0)
9033
+ return "Error: goal and steps are required";
8051
9034
  const flow = ctx.runtime.startFlow(goal, steps, wc?.getURL());
8052
9035
  return `Flow started: ${flow.goal}
8053
9036
  ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`;
@@ -8092,47 +9075,70 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
8092
9075
  (el) => el.inputType === "search" || el.name === "q" || el.name === "query" || (el.placeholder || "").toLowerCase().includes("search")
8093
9076
  );
8094
9077
  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;
9078
+ const totalFields = page.forms.reduce(
9079
+ (n, f) => n + f.fields.length,
9080
+ 0
9081
+ );
9082
+ const linkCount = page.interactiveElements.filter(
9083
+ (el) => el.type === "link"
9084
+ ).length;
8097
9085
  const hasPagination = page.interactiveElements.some(
8098
9086
  (el) => (el.text || "").toLowerCase() === "next" || el.text === "›" || el.text === "»"
8099
9087
  );
8100
9088
  const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
8101
9089
  if (hasOverlays) {
8102
9090
  suggestions.push("BLOCKING OVERLAY detected — dismiss it first:");
8103
- suggestions.push(" → dismiss_popup or click on close/accept button");
9091
+ suggestions.push(
9092
+ " → dismiss_popup or click on close/accept button"
9093
+ );
8104
9094
  suggestions.push("");
8105
9095
  }
8106
9096
  if (hasPasswordField) {
8107
9097
  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");
9098
+ suggestions.push(
9099
+ " → login(username, password) handles the full flow"
9100
+ );
9101
+ suggestions.push(
9102
+ " → Or fill_form + submit_form for manual control"
9103
+ );
8110
9104
  } else if (hasSearchInput && linkCount < 10) {
8111
9105
  suggestions.push("SEARCH PAGE detected:");
8112
- suggestions.push(" → search(query) — finds the box, types, submits");
9106
+ suggestions.push(
9107
+ " → search(query) — finds the box, types, submits"
9108
+ );
8113
9109
  } else if (hasSearchInput && linkCount >= 10) {
8114
9110
  suggestions.push("SEARCH RESULTS detected:");
8115
9111
  suggestions.push(" → click on a result link");
8116
- if (hasPagination) suggestions.push(" → paginate('next') for more results");
9112
+ if (hasPagination)
9113
+ suggestions.push(" → paginate('next') for more results");
8117
9114
  } else if (formCount > 0) {
8118
9115
  suggestions.push(`FORM detected (${totalFields} fields):`);
8119
9116
  suggestions.push(" → fill_form(fields) — fill all fields at once");
8120
9117
  } else if (hasPagination) {
8121
9118
  suggestions.push("PAGINATED CONTENT:");
8122
- suggestions.push(" → read_page to read this page");
9119
+ suggestions.push(
9120
+ " → read_page(mode='results_only') to inspect likely results"
9121
+ );
8123
9122
  suggestions.push(" → paginate('next') for the next page");
8124
9123
  } else if (page.content.length > 3e3 && page.interactiveElements.length < 10) {
8125
9124
  suggestions.push("ARTICLE/CONTENT page:");
8126
- suggestions.push(" → read_page for readable text");
9125
+ suggestions.push(" → read_page(mode='summary') for a fast brief");
9126
+ suggestions.push(
9127
+ " → read_page(mode='text_only') for readable text"
9128
+ );
8127
9129
  suggestions.push(" → scroll to see more");
8128
9130
  } else {
8129
9131
  suggestions.push("GENERAL PAGE:");
8130
- suggestions.push(" → read_page to understand the page structure");
9132
+ suggestions.push(
9133
+ " → read_page(mode='visible_only') to inspect active controls"
9134
+ );
8131
9135
  suggestions.push(" → click on any element by index");
8132
9136
  suggestions.push(" → navigate to go somewhere new");
8133
9137
  }
8134
9138
  suggestions.push("");
8135
- suggestions.push(`Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`);
9139
+ suggestions.push(
9140
+ `Available: ${page.interactiveElements.length} interactive elements, ${formCount} forms, ${linkCount} links`
9141
+ );
8136
9142
  return suggestions.join("\n");
8137
9143
  }
8138
9144
  case "fill_form": {
@@ -8146,11 +9152,19 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
8146
9152
  results.push(`Skipped: no selector for index=${field.index}`);
8147
9153
  continue;
8148
9154
  }
8149
- const result2 = await setElementValue$1(wc, sel, String(field.value || ""));
9155
+ const result2 = await setElementValue$1(
9156
+ wc,
9157
+ sel,
9158
+ String(field.value || "")
9159
+ );
8150
9160
  results.push(result2);
8151
9161
  }
8152
9162
  if (args.submit) {
8153
- const firstSel = await resolveSelector$1(wc, fields[0]?.index, fields[0]?.selector);
9163
+ const firstSel = await resolveSelector$1(
9164
+ wc,
9165
+ fields[0]?.index,
9166
+ fields[0]?.selector
9167
+ );
8154
9168
  if (firstSel) {
8155
9169
  const beforeUrl = wc.getURL();
8156
9170
  const submitResult = await submitForm$1(wc, { selector: firstSel });
@@ -8173,29 +9187,53 @@ ${results.join("\n")}`;
8173
9187
  await waitForLoad$1(wc);
8174
9188
  steps.push(`Navigated to ${wc.getURL()}`);
8175
9189
  }
8176
- const userSel = args.username_selector || await wc.executeJavaScript(`
9190
+ const userSel = args.username_selector || await executePageScript(
9191
+ wc,
9192
+ `
8177
9193
  (function() {
8178
9194
  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
9195
  return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
8180
9196
  })()
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(`
9197
+ `,
9198
+ {
9199
+ label: "find username field"
9200
+ }
9201
+ );
9202
+ if (!userSel)
9203
+ return "Error: Could not find username/email field. Try providing username_selector.";
9204
+ const passSel = args.password_selector || await executePageScript(
9205
+ wc,
9206
+ `
8184
9207
  (function() {
8185
9208
  var el = document.querySelector('input[type="password"]');
8186
9209
  return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
8187
9210
  })()
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 || ""));
9211
+ `,
9212
+ {
9213
+ label: "find password field"
9214
+ }
9215
+ );
9216
+ if (!passSel)
9217
+ return "Error: Could not find password field. Try providing password_selector.";
9218
+ const userResult = await setElementValue$1(
9219
+ wc,
9220
+ userSel,
9221
+ String(args.username || "")
9222
+ );
8191
9223
  steps.push(userResult);
8192
- const passResult = await setElementValue$1(wc, passSel, String(args.password || ""));
9224
+ const passResult = await setElementValue$1(
9225
+ wc,
9226
+ passSel,
9227
+ String(args.password || "")
9228
+ );
8193
9229
  steps.push(passResult);
8194
9230
  const beforeUrl = wc.getURL();
8195
9231
  if (args.submit_selector) {
8196
9232
  await clickResolvedSelector$1(wc, args.submit_selector);
8197
9233
  } else {
8198
- const clicked = await wc.executeJavaScript(`
9234
+ const clicked = await executePageScript(
9235
+ wc,
9236
+ `
8199
9237
  (function() {
8200
9238
  var btn = document.querySelector('button[type="submit"], input[type="submit"], form button:not([type="button"])');
8201
9239
  if (btn) { btn.click(); return true; }
@@ -8203,8 +9241,16 @@ ${results.join("\n")}`;
8203
9241
  if (form) { form.requestSubmit ? form.requestSubmit() : form.submit(); return true; }
8204
9242
  return false;
8205
9243
  })()
8206
- `);
8207
- if (!clicked) return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
9244
+ `,
9245
+ {
9246
+ label: "submit login form"
9247
+ }
9248
+ );
9249
+ if (clicked === PAGE_SCRIPT_TIMEOUT) {
9250
+ return pageBusyError("login");
9251
+ }
9252
+ if (!clicked)
9253
+ return steps.join("\n") + "\nWarning: Could not find submit button. Credentials filled but form not submitted.";
8208
9254
  }
8209
9255
  await waitForPotentialNavigation$1(wc, beforeUrl);
8210
9256
  const afterUrl = wc.getURL();
@@ -8212,42 +9258,137 @@ ${results.join("\n")}`;
8212
9258
  afterUrl !== beforeUrl ? `Submitted → ${afterUrl}` : "Form submitted (same page)"
8213
9259
  );
8214
9260
  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;
9261
+ ${steps.join("\n")}`;
9262
+ }
9263
+ case "search": {
9264
+ if (!wc) return "Error: No active tab";
9265
+ const query = String(args.query || "");
9266
+ if (!query) return "Error: No search query provided.";
9267
+ const searchInfo = args.selector ? { selector: args.selector, formAction: null, formMethod: null } : await executePageScript(
9268
+ wc,
9269
+ `
9270
+ (function() {
9271
+ var SELECTORS = 'input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]';
9272
+ function find() {
9273
+ var el = document.querySelector(SELECTORS);
9274
+ if (!el) {
9275
+ var inputs = document.querySelectorAll('input[type="text"]');
9276
+ for (var i = 0; i < inputs.length; i++) {
9277
+ var form = inputs[i].closest('form');
9278
+ if (form && (form.getAttribute('role') === 'search' || (form.action && form.action.includes('search')))) {
9279
+ el = inputs[i];
9280
+ break;
9281
+ }
8229
9282
  }
8230
9283
  }
9284
+ if (!el) return null;
9285
+ var sel = el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null;
9286
+ var form = el.closest('form');
9287
+ return {
9288
+ selector: sel,
9289
+ formAction: form ? form.action : null,
9290
+ formMethod: form ? (form.method || 'GET').toUpperCase() : null,
9291
+ inputName: el.name || 'q',
9292
+ };
8231
9293
  }
8232
- return el ? (el.id ? '#' + CSS.escape(el.id) : el.name ? 'input[name="' + el.name + '"]' : null) : null;
9294
+ return new Promise(function(resolve) {
9295
+ var result = find();
9296
+ if (result) { resolve(result); return; }
9297
+ var attempts = 0;
9298
+ var timer = setInterval(function() {
9299
+ result = find();
9300
+ if (result || ++attempts >= 20) {
9301
+ clearInterval(timer);
9302
+ resolve(result);
9303
+ }
9304
+ }, 250);
9305
+ });
8233
9306
  })()
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(`
9307
+ `,
9308
+ {
9309
+ timeoutMs: 1800,
9310
+ label: "find search input"
9311
+ }
9312
+ );
9313
+ if (searchInfo === PAGE_SCRIPT_TIMEOUT) {
9314
+ return pageBusyError("search");
9315
+ }
9316
+ if (!searchInfo?.selector)
9317
+ return "Error: Could not find search input. Try providing a selector.";
9318
+ const fillResult = await setElementValue$1(
9319
+ wc,
9320
+ searchInfo.selector,
9321
+ query
9322
+ );
9323
+ if (fillResult.startsWith("Error:")) {
9324
+ return fillResult;
9325
+ }
9326
+ await sleep$1(100);
9327
+ const focusResult = await executePageScript(
9328
+ wc,
9329
+ `
8238
9330
  (function() {
8239
- var el = document.querySelector(${JSON.stringify(searchSel)});
9331
+ var el = document.querySelector(${JSON.stringify(searchInfo.selector)});
8240
9332
  if (el) el.focus();
8241
9333
  })()
8242
- `);
9334
+ `,
9335
+ {
9336
+ label: "focus search input"
9337
+ }
9338
+ );
9339
+ if (focusResult === PAGE_SCRIPT_TIMEOUT) {
9340
+ return pageBusyError("search");
9341
+ }
8243
9342
  await sleep$1(50);
8244
9343
  const beforeUrl = wc.getURL();
8245
9344
  wc.sendInputEvent({ type: "keyDown", keyCode: "Return" });
8246
9345
  await sleep$1(16);
8247
9346
  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)`;
9347
+ await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
9348
+ let afterUrl = wc.getURL();
9349
+ if (afterUrl !== beforeUrl) {
9350
+ return `Searched "${query}" → ${afterUrl}`;
9351
+ }
9352
+ const clickedSubmit = await executePageScript(
9353
+ wc,
9354
+ `
9355
+ (function() {
9356
+ var form = document.querySelector(${JSON.stringify(searchInfo.selector)})?.closest('form');
9357
+ if (!form) return false;
9358
+ var btn = form.querySelector('button[type="submit"], input[type="submit"], button:not([type])');
9359
+ if (btn) { btn.click(); return true; }
9360
+ // Try any button in the form
9361
+ var anyBtn = form.querySelector('button');
9362
+ if (anyBtn) { anyBtn.click(); return true; }
9363
+ return false;
9364
+ })()
9365
+ `,
9366
+ {
9367
+ label: "click search submit"
9368
+ }
9369
+ );
9370
+ if (clickedSubmit === PAGE_SCRIPT_TIMEOUT) {
9371
+ return pageBusyError("search");
9372
+ }
9373
+ if (clickedSubmit) {
9374
+ await waitForPotentialNavigation$1(wc, beforeUrl, 3e3);
9375
+ afterUrl = wc.getURL();
9376
+ if (afterUrl !== beforeUrl) {
9377
+ return `Searched "${query}" (via submit button) → ${afterUrl}`;
9378
+ }
9379
+ }
9380
+ if (searchInfo.formAction && searchInfo.formMethod === "GET") {
9381
+ try {
9382
+ const url = new URL(searchInfo.formAction);
9383
+ url.searchParams.set(searchInfo.inputName || "q", query);
9384
+ wc.loadURL(url.toString());
9385
+ await waitForPotentialNavigation$1(wc, beforeUrl);
9386
+ afterUrl = wc.getURL();
9387
+ return `Searched "${query}" (via direct URL) → ${afterUrl}`;
9388
+ } catch {
9389
+ }
9390
+ }
9391
+ return `Searched "${query}" (same page — results may have loaded dynamically)`;
8251
9392
  }
8252
9393
  case "paginate": {
8253
9394
  if (!wc) return "Error: No active tab";
@@ -8256,7 +9397,9 @@ ${steps.join("\n")}`;
8256
9397
  return clickResolvedSelector$1(wc, args.selector);
8257
9398
  }
8258
9399
  const isNext = args.direction === "next";
8259
- const clicked = await wc.executeJavaScript(`
9400
+ const clicked = await executePageScript(
9401
+ wc,
9402
+ `
8260
9403
  (function() {
8261
9404
  var patterns = ${isNext ? '["next", "Next", "›", "»", "→", ">", "Next Page", "Load More"]' : '["prev", "Prev", "Previous", "‹", "«", "←", "<", "Previous Page"]'};
8262
9405
  var links = document.querySelectorAll('a, button');
@@ -8275,12 +9418,156 @@ ${steps.join("\n")}`;
8275
9418
  }
8276
9419
  return false;
8277
9420
  })()
8278
- `);
8279
- if (!clicked) return `Error: Could not find ${args.direction} pagination control. Try providing a selector.`;
9421
+ `,
9422
+ {
9423
+ label: "paginate"
9424
+ }
9425
+ );
9426
+ if (clicked === PAGE_SCRIPT_TIMEOUT) {
9427
+ return pageBusyError("paginate");
9428
+ }
9429
+ if (!clicked)
9430
+ return `Error: Could not find ${args.direction} pagination control. Try providing a selector.`;
8280
9431
  await waitForPotentialNavigation$1(wc, beforeUrl);
8281
9432
  const afterUrl = wc.getURL();
8282
9433
  return afterUrl !== beforeUrl ? `Paginated ${args.direction} → ${afterUrl}` : `Clicked ${args.direction} (page may have updated dynamically)`;
8283
9434
  }
9435
+ case "accept_cookies": {
9436
+ if (!wc) return "Error: No active tab";
9437
+ const dismissed = await executePageScript(
9438
+ wc,
9439
+ `
9440
+ (function() {
9441
+ // Common cookie consent selectors — OneTrust, CookieBot, GDPR banners
9442
+ var selectors = [
9443
+ '#onetrust-accept-btn-handler',
9444
+ '#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
9445
+ '[data-cookiefirst-action="accept"]',
9446
+ '.cookie-consent-accept-all',
9447
+ '#accept-cookies',
9448
+ '.cc-accept',
9449
+ '.cc-btn.cc-allow',
9450
+ '[aria-label="Accept cookies"]',
9451
+ '[aria-label="Accept all cookies"]',
9452
+ '[data-testid="cookie-accept"]',
9453
+ ];
9454
+ // Also try text-matching on buttons
9455
+ var textPatterns = ['accept all', 'accept cookies', 'allow all', 'allow cookies', 'agree', 'got it', 'ok', 'i agree', 'consent'];
9456
+ for (var i = 0; i < selectors.length; i++) {
9457
+ var el = document.querySelector(selectors[i]);
9458
+ if (el && el instanceof HTMLElement) { el.click(); return "Dismissed cookie banner via: " + selectors[i]; }
9459
+ }
9460
+ var buttons = document.querySelectorAll('button, a[role="button"], [type="submit"]');
9461
+ for (var j = 0; j < buttons.length; j++) {
9462
+ var btn = buttons[j];
9463
+ var text = (btn.textContent || '').trim().toLowerCase();
9464
+ for (var k = 0; k < textPatterns.length; k++) {
9465
+ if (text === textPatterns[k] || text.startsWith(textPatterns[k])) {
9466
+ btn.click();
9467
+ return "Dismissed cookie banner via text match: " + text;
9468
+ }
9469
+ }
9470
+ }
9471
+ return null;
9472
+ })()
9473
+ `,
9474
+ {
9475
+ label: "accept cookies"
9476
+ }
9477
+ );
9478
+ if (dismissed === PAGE_SCRIPT_TIMEOUT) {
9479
+ return pageBusyError("accept_cookies");
9480
+ }
9481
+ return dismissed || "No cookie consent banner detected. Try dismiss_popup for other overlays.";
9482
+ }
9483
+ case "extract_table": {
9484
+ if (!wc) return "Error: No active tab";
9485
+ const selector = args.selector ? args.selector : args.index != null ? await resolveSelector$1(wc, args.index, void 0) : null;
9486
+ const tableJson = await wc.executeJavaScript(`
9487
+ (function() {
9488
+ var table = ${selector ? `document.querySelector(${JSON.stringify(selector)})` : "document.querySelector('table')"};
9489
+ if (!table) return null;
9490
+ var headers = [];
9491
+ var headerRow = table.querySelector('thead tr') || table.querySelector('tr');
9492
+ if (headerRow) {
9493
+ headerRow.querySelectorAll('th, td').forEach(function(cell) {
9494
+ headers.push(cell.textContent.trim());
9495
+ });
9496
+ }
9497
+ var rows = [];
9498
+ var bodyRows = table.querySelectorAll('tbody tr');
9499
+ if (bodyRows.length === 0) bodyRows = table.querySelectorAll('tr');
9500
+ bodyRows.forEach(function(tr, idx) {
9501
+ if (idx === 0 && headers.length > 0 && !table.querySelector('thead')) return;
9502
+ var row = {};
9503
+ tr.querySelectorAll('td, th').forEach(function(cell, ci) {
9504
+ var key = headers[ci] || ("col_" + ci);
9505
+ row[key] = cell.textContent.trim();
9506
+ });
9507
+ if (Object.keys(row).length > 0) rows.push(row);
9508
+ });
9509
+ return { headers: headers, rows: rows, rowCount: rows.length };
9510
+ })()
9511
+ `);
9512
+ if (!tableJson) return "Error: No table found on the page.";
9513
+ return `Extracted table (${tableJson.rowCount} rows):
9514
+ ${JSON.stringify(tableJson, null, 2)}`;
9515
+ }
9516
+ case "metrics": {
9517
+ const m = ctx.runtime.getMetrics();
9518
+ const lines = [
9519
+ `Session Metrics:`,
9520
+ ` Total actions: ${m.totalActions}`,
9521
+ ` Completed: ${m.completedActions}`,
9522
+ ` Failed: ${m.failedActions}`,
9523
+ ` Average duration: ${m.averageDurationMs}ms`,
9524
+ ``,
9525
+ `Tool breakdown:`
9526
+ ];
9527
+ for (const [name2, stats] of Object.entries(m.toolBreakdown)) {
9528
+ lines.push(
9529
+ ` ${name2}: ${stats.count} calls, avg ${stats.avgMs}ms${stats.errors > 0 ? `, ${stats.errors} errors` : ""}`
9530
+ );
9531
+ }
9532
+ return lines.join("\n");
9533
+ }
9534
+ case "scroll_to_element": {
9535
+ if (!wc) return "Error: No active tab";
9536
+ const sel = await resolveSelector$1(wc, args.index, args.selector);
9537
+ if (!sel)
9538
+ return "Error: Provide an index or selector for the element to scroll to.";
9539
+ const block = args.position === "top" ? "start" : args.position === "bottom" ? "end" : "center";
9540
+ if (sel.startsWith("__vessel_idx:")) {
9541
+ const idx = Number(sel.slice("__vessel_idx:".length));
9542
+ return wc.executeJavaScript(`
9543
+ (function() {
9544
+ var el = window.__vessel?.interactByIndex && Object.values(window.__vessel)[2];
9545
+ var ref = (function() { try { return document.querySelector('[data-vessel-idx="${idx}"]'); } catch(e) { return null; } })();
9546
+ if (!ref) return "Error: Element not found";
9547
+ ref.scrollIntoView({ behavior: "smooth", block: "${block}" });
9548
+ return "Scrolled to element #${idx}";
9549
+ })()
9550
+ `);
9551
+ }
9552
+ if (sel.includes(" >>> ")) {
9553
+ return wc.executeJavaScript(`
9554
+ (function() {
9555
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(sel)});
9556
+ if (!el) return "Error: Shadow DOM element not found";
9557
+ el.scrollIntoView({ behavior: "smooth", block: "${block}" });
9558
+ return "Scrolled to shadow DOM element";
9559
+ })()
9560
+ `);
9561
+ }
9562
+ return wc.executeJavaScript(`
9563
+ (function() {
9564
+ var el = document.querySelector(${JSON.stringify(sel)});
9565
+ if (!el) return "Error: Element not found";
9566
+ el.scrollIntoView({ behavior: "smooth", block: "${block}" });
9567
+ return "Scrolled to element";
9568
+ })()
9569
+ `);
9570
+ }
8284
9571
  default:
8285
9572
  return `Unknown tool: ${name}`;
8286
9573
  }
@@ -8294,9 +9581,17 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
8294
9581
  const isSummarize = lowerQuery.startsWith("summarize") || lowerQuery.startsWith("tldr") || lowerQuery === "summary";
8295
9582
  if (provider.streamAgentQuery && tabManager && activeWebContents && runtime) {
8296
9583
  try {
9584
+ const extractStart = Date.now();
8297
9585
  const pageContent = await extractContent(activeWebContents);
8298
- const structuredContext = buildStructuredContext(pageContent);
8299
- const truncated = pageContent.content.length > 2e4 ? pageContent.content.slice(0, 2e4) + "\n[Content truncated...]" : pageContent.content;
9586
+ console.log(
9587
+ `[Vessel Agent] initial extractContent completed in ${Date.now() - extractStart}ms, contentLen=${pageContent.content.length}`
9588
+ );
9589
+ const pageType = detectPageType(pageContent);
9590
+ const defaultReadMode = chooseAgentReadMode(pageContent);
9591
+ const structuredContext = buildScopedContext(
9592
+ pageContent,
9593
+ defaultReadMode
9594
+ );
8300
9595
  const runtimeState = runtime.getState();
8301
9596
  const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
8302
9597
  const activeTabTitle = pageContent.title || "(untitled)";
@@ -8311,13 +9606,14 @@ THE USER IS CURRENTLY LOOKING AT:
8311
9606
  Title: ${activeTabTitle}
8312
9607
  URL: ${activeTabUrl}${tabSummary}
8313
9608
 
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.
9609
+ When the user says "this page", "this article", "this site", or asks about what they're viewing, they mean the page above. The context below is from that page — answer directly without needing to call read_page or current_tab first.
8315
9610
 
8316
9611
  Current page context:
8317
- ${structuredContext}
9612
+ This brief is intentionally minimal and filtered for speed. It omits most page text and low-value chrome unless you explicitly ask for more.
9613
+ Default brief mode: ${defaultReadMode}
9614
+ Detected page type: ${pageType}
8318
9615
 
8319
- Page content:
8320
- ${truncated}
9616
+ ${structuredContext}
8321
9617
 
8322
9618
  Supervisor state:
8323
9619
  - paused: ${runtimeState.supervisor.paused ? "yes" : "no"}
@@ -8336,7 +9632,11 @@ Instructions:
8336
9632
  - Create a checkpoint before risky multi-step flows or before leaving an important state.
8337
9633
  - Use save_session after completing a login flow you may need again later, and load_session to resume that authenticated state in future runs.
8338
9634
  - Prefer select_option for dropdowns and submit_form for forms instead of guessing with clicks.
8339
- - After clicking or navigating, use read_page to see the updated content.
9635
+ - After navigating to a new site, DO NOT call read_page immediately. Instead, act on what you already know: use the search tool to search the site, type_text to enter queries in search bars, or click on known navigation patterns. You know what major sites look like — use that knowledge. Only call read_page if you're genuinely stuck and need to discover unfamiliar page structure.
9636
+ - The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
9637
+ - Escalate page reads progressively: read_page(mode="visible_only"), read_page(mode="results_only"), read_page(mode="forms_only"), read_page(mode="summary"), or read_page(mode="text_only") depending on what you need.
9638
+ - Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
9639
+ - After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. Do not jump straight to read_page(mode="debug").
8340
9640
  - If the user says they highlighted or selected text, use read_page before falling back to screenshots because it includes active selection and visible unsaved highlights.
8341
9641
  - If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.
8342
9642
  - For broad discovery tasks, prefer direct sources, official sites, venue directories, and site-specific search over generic search engines, which often rate-limit automated browser traffic.
@@ -8344,13 +9644,19 @@ Instructions:
8344
9644
  - Reference interactive elements by their index number (shown as [#N] in the listings above).
8345
9645
  - Be concise. Explain what you're doing as you go.
8346
9646
  - 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.`;
9647
+ - VISUAL AWARENESS: The human is watching the browser alongside this chat. Highlights are your pointing finger they show the user exactly what you're looking at on the page. Use them proactively: highlight key findings, important elements, errors, or anything you're referencing. Don't wait to be asked. If you mention something specific on the page, highlight it. Colors: yellow (default/attention), red (errors/warnings), green (success/good), blue (info/neutral), purple (important/notable), orange (caution). Clear highlights when moving to a new topic or page.
9648
+ - 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.
9649
+ - Call one tool at a time unless you are certain your provider supports parallel tool calls. Sequential calls are more reliable.
9650
+ - MINIMIZE TOOL CALLS: Every tool call takes time and costs a round trip. Be efficient. Don't use flow_start/flow_advance for simple multi-step tasks — just do the work. Don't call read_page after navigating — use search or type_text directly. Don't retry failed tools with slight variations — if search fails, go straight to type_text + press_key Enter, don't try read_page in between. The fastest path is usually: navigate → search → wait_for or read_page(mode="results_only") → click.
9651
+ - ACT, DON'T HEDGE: You have a full browser — you can navigate to any website, see live content, search, click, add to cart, fill forms, and interact with real pages in real time. Never claim you "don't have access" to a website's inventory, pricing, or content. If the user asks you to go somewhere and do something, start doing it immediately. Don't ask for permission to do what the user just asked you to do — that's redundant and frustrating. Jump straight into action.
9652
+ - USE YOUR KNOWLEDGE: You have broad, practical knowledge about technology, products, cooking, travel, finance, and countless other domains. When the user asks for recommendations, GIVE them — don't deflect to Reddit, YouTubers, or other sources. You know enough to recommend PC parts, suggest restaurants, pick a good laptop, or advise on most consumer decisions. Make a clear recommendation, explain your reasoning briefly, and then execute. If there's genuine ambiguity (e.g. AMD vs Intel is preference-dependent), state your pick and why, then ask only the questions that would actually change your recommendation. Never refuse a recommendation by claiming you're "not an expert" — the user chose to ask you, so help them.
9653
+ - NEVER USE EMOJIS unless the user uses them first.`;
8349
9654
  const actionCtx = { tabManager, runtime };
9655
+ const contextualTools = pruneToolsForContext(AGENT_TOOLS, pageType);
8350
9656
  await provider.streamAgentQuery(
8351
9657
  systemPrompt,
8352
9658
  query,
8353
- AGENT_TOOLS,
9659
+ contextualTools,
8354
9660
  onChunk,
8355
9661
  (name, args) => executeAction(name, args, actionCtx),
8356
9662
  onEnd,
@@ -8375,7 +9681,13 @@ Instructions:
8375
9681
  } else {
8376
9682
  prompt = buildGeneralPrompt(query);
8377
9683
  }
8378
- await provider.streamQuery(prompt.system, prompt.user, onChunk, onEnd, history);
9684
+ await provider.streamQuery(
9685
+ prompt.system,
9686
+ prompt.user,
9687
+ onChunk,
9688
+ onEnd,
9689
+ history
9690
+ );
8379
9691
  }
8380
9692
  const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
8381
9693
  const DEFAULT_NOTE_FOLDER = "Vessel/Research";
@@ -9473,6 +10785,21 @@ async function clickResolvedSelector(wc, selector) {
9473
10785
  const afterUrl2 = wc.getURL();
9474
10786
  return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
9475
10787
  }
10788
+ if (selector.includes(" >>> ")) {
10789
+ const beforeUrl2 = wc.getURL();
10790
+ const result = await wc.executeJavaScript(`
10791
+ (function() {
10792
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
10793
+ if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
10794
+ if (el instanceof HTMLElement) { el.focus(); el.click(); }
10795
+ return "Clicked: " + (el.getAttribute("aria-label") || el.textContent?.trim().slice(0, 60) || el.tagName.toLowerCase());
10796
+ })()
10797
+ `);
10798
+ if (typeof result === "string" && result.startsWith("Error")) return result;
10799
+ await waitForPotentialNavigation(wc, beforeUrl2);
10800
+ const afterUrl2 = wc.getURL();
10801
+ return afterUrl2 !== beforeUrl2 ? `${result} -> ${afterUrl2}` : result;
10802
+ }
9476
10803
  const beforeUrl = wc.getURL();
9477
10804
  const elInfo = await describeElementForClick(wc, selector);
9478
10805
  if ("error" in elInfo) return `Error: ${elInfo.error}`;
@@ -9820,6 +11147,22 @@ async function setElementValue(wc, selector, value) {
9820
11147
  `window.__vessel?.interactByIndex?.(${idx}, "value", ${JSON.stringify(value)}) || "Error: interactByIndex not available"`
9821
11148
  );
9822
11149
  }
11150
+ if (selector.includes(" >>> ")) {
11151
+ return wc.executeJavaScript(`
11152
+ (function() {
11153
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(selector)});
11154
+ if (!el) return "Error[stale-index]: Shadow DOM element not found — call read_page to refresh.";
11155
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return "Error[not-input]: Element is not a text input";
11156
+ var proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
11157
+ var desc = Object.getOwnPropertyDescriptor(proto, "value");
11158
+ if (desc && desc.set) { desc.set.call(el, ${JSON.stringify(value)}); } else { el.value = ${JSON.stringify(value)}; }
11159
+ el.focus();
11160
+ el.dispatchEvent(new Event("input", { bubbles: true }));
11161
+ el.dispatchEvent(new Event("change", { bubbles: true }));
11162
+ return "Typed into: " + (el.getAttribute("aria-label") || el.placeholder || el.name || "input");
11163
+ })()
11164
+ `);
11165
+ }
9823
11166
  return wc.executeJavaScript(`
9824
11167
  (function() {
9825
11168
  const el = document.querySelector(${JSON.stringify(selector)});
@@ -10264,6 +11607,65 @@ function registerTools(server, tabManager, runtime) {
10264
11607
  ]
10265
11608
  })
10266
11609
  );
11610
+ server.registerResource(
11611
+ "vessel-recommended-tools",
11612
+ "vessel://context/recommended-tools",
11613
+ {
11614
+ title: "Recommended Tools for Current Page",
11615
+ description: "Context-aware tool recommendations based on the current page type (login, search, form, article, etc.). Returns tools sorted by relevance with contextual hints.",
11616
+ mimeType: "application/json"
11617
+ },
11618
+ async () => {
11619
+ const activeTab = tabManager.getActiveTab();
11620
+ let pageType = "GENERAL";
11621
+ let pageUrl = "";
11622
+ let pageTitle = "";
11623
+ if (activeTab) {
11624
+ try {
11625
+ const wc = activeTab.view.webContents;
11626
+ pageUrl = wc.getURL();
11627
+ pageTitle = wc.getTitle();
11628
+ const page = await extractContent(wc);
11629
+ pageType = detectPageType(page);
11630
+ } catch {
11631
+ }
11632
+ }
11633
+ const scored = TOOL_DEFINITIONS.map((def) => {
11634
+ const tier = def.tier ?? 1;
11635
+ const isRelevant = !def.relevance || def.relevance.includes(pageType);
11636
+ let score;
11637
+ if (tier === 0) score = 0;
11638
+ else if (tier === 1 && isRelevant) score = 10;
11639
+ else if (tier === 2 && isRelevant) score = 20;
11640
+ else if (tier === 1) score = 30;
11641
+ else score = 40;
11642
+ return {
11643
+ name: `vessel_${def.name}`,
11644
+ title: def.title,
11645
+ description: def.description,
11646
+ tier,
11647
+ relevance: isRelevant ? "high" : "low",
11648
+ score
11649
+ };
11650
+ });
11651
+ scored.sort((a, b) => a.score - b.score);
11652
+ const result = {
11653
+ pageType,
11654
+ pageUrl,
11655
+ pageTitle,
11656
+ recommended: scored.filter((t) => t.score <= 20).map(({ name, title, description, relevance }) => ({ name, title, description, relevance })),
11657
+ available: scored.filter((t) => t.score > 20).map(({ name, title, relevance }) => ({ name, title, relevance }))
11658
+ };
11659
+ return {
11660
+ contents: [
11661
+ {
11662
+ uri: "vessel://context/recommended-tools",
11663
+ text: JSON.stringify(result, null, 2)
11664
+ }
11665
+ ]
11666
+ };
11667
+ }
11668
+ );
10267
11669
  server.registerTool(
10268
11670
  "vessel_current_tab",
10269
11671
  {
@@ -12543,6 +13945,240 @@ ${steps.join("\n")}`;
12543
13945
  );
12544
13946
  }
12545
13947
  );
13948
+ server.registerTool(
13949
+ "vessel_accept_cookies",
13950
+ {
13951
+ title: "Accept Cookies",
13952
+ description: "Dismiss cookie consent banners (OneTrust, CookieBot, GDPR popups, etc.).",
13953
+ inputSchema: zod.z.object({})
13954
+ },
13955
+ async () => {
13956
+ return withAction(
13957
+ tabManager,
13958
+ runtime,
13959
+ "vessel_accept_cookies",
13960
+ {},
13961
+ async (wc) => {
13962
+ const dismissed = await wc.executeJavaScript(`
13963
+ (function() {
13964
+ var selectors = [
13965
+ '#onetrust-accept-btn-handler',
13966
+ '#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll',
13967
+ '[data-cookiefirst-action="accept"]',
13968
+ '.cookie-consent-accept-all',
13969
+ '#accept-cookies',
13970
+ '.cc-accept',
13971
+ '.cc-btn.cc-allow',
13972
+ '[aria-label="Accept cookies"]',
13973
+ '[aria-label="Accept all cookies"]',
13974
+ '[data-testid="cookie-accept"]',
13975
+ ];
13976
+ var textPatterns = ['accept all', 'accept cookies', 'allow all', 'allow cookies', 'agree', 'got it', 'ok', 'i agree', 'consent'];
13977
+ for (var i = 0; i < selectors.length; i++) {
13978
+ var el = document.querySelector(selectors[i]);
13979
+ if (el && el instanceof HTMLElement) { el.click(); return "Dismissed cookie banner via: " + selectors[i]; }
13980
+ }
13981
+ var buttons = document.querySelectorAll('button, a[role="button"], [type="submit"]');
13982
+ for (var j = 0; j < buttons.length; j++) {
13983
+ var btn = buttons[j];
13984
+ var text = (btn.textContent || '').trim().toLowerCase();
13985
+ for (var k = 0; k < textPatterns.length; k++) {
13986
+ if (text === textPatterns[k] || text.startsWith(textPatterns[k])) {
13987
+ btn.click();
13988
+ return "Dismissed cookie banner via text match: " + text;
13989
+ }
13990
+ }
13991
+ }
13992
+ return null;
13993
+ })()
13994
+ `);
13995
+ return dismissed || "No cookie consent banner detected. Try vessel_dismiss_popup for other overlays.";
13996
+ }
13997
+ );
13998
+ }
13999
+ );
14000
+ server.registerTool(
14001
+ "vessel_extract_table",
14002
+ {
14003
+ title: "Extract Table",
14004
+ description: "Extract a table from the page as structured JSON rows with headers.",
14005
+ inputSchema: zod.z.object({
14006
+ index: zod.z.number().optional().describe("Element index of the table"),
14007
+ selector: zod.z.string().optional().describe("CSS selector for the table")
14008
+ })
14009
+ },
14010
+ async ({ index, selector: rawSelector }) => {
14011
+ return withAction(
14012
+ tabManager,
14013
+ runtime,
14014
+ "vessel_extract_table",
14015
+ { index, selector: rawSelector },
14016
+ async (wc) => {
14017
+ const sel = rawSelector || (index != null ? await resolveSelector(wc, index) : null);
14018
+ const tableJson = await wc.executeJavaScript(`
14019
+ (function() {
14020
+ var table = ${sel ? `document.querySelector(${JSON.stringify(sel)})` : "document.querySelector('table')"};
14021
+ if (!table) return null;
14022
+ var headers = [];
14023
+ var headerRow = table.querySelector('thead tr') || table.querySelector('tr');
14024
+ if (headerRow) {
14025
+ headerRow.querySelectorAll('th, td').forEach(function(cell) {
14026
+ headers.push(cell.textContent.trim());
14027
+ });
14028
+ }
14029
+ var rows = [];
14030
+ var bodyRows = table.querySelectorAll('tbody tr');
14031
+ if (bodyRows.length === 0) bodyRows = table.querySelectorAll('tr');
14032
+ bodyRows.forEach(function(tr, idx) {
14033
+ if (idx === 0 && headers.length > 0 && !table.querySelector('thead')) return;
14034
+ var row = {};
14035
+ tr.querySelectorAll('td, th').forEach(function(cell, ci) {
14036
+ var key = headers[ci] || ("col_" + ci);
14037
+ row[key] = cell.textContent.trim();
14038
+ });
14039
+ if (Object.keys(row).length > 0) rows.push(row);
14040
+ });
14041
+ return { headers: headers, rows: rows, rowCount: rows.length };
14042
+ })()
14043
+ `);
14044
+ if (!tableJson) return "Error: No table found on the page.";
14045
+ return `Extracted table (${tableJson.rowCount} rows):
14046
+ ${JSON.stringify(tableJson, null, 2)}`;
14047
+ }
14048
+ );
14049
+ }
14050
+ );
14051
+ server.registerTool(
14052
+ "vessel_scroll_to_element",
14053
+ {
14054
+ title: "Scroll To Element",
14055
+ description: "Scroll a specific element into view by index or selector.",
14056
+ inputSchema: zod.z.object({
14057
+ index: zod.z.number().optional().describe("Element index to scroll to"),
14058
+ selector: zod.z.string().optional().describe("CSS selector to scroll to"),
14059
+ position: zod.z.enum(["center", "top", "bottom"]).optional().describe("Viewport position (default center)")
14060
+ })
14061
+ },
14062
+ async ({ index, selector: rawSelector, position }) => {
14063
+ return withAction(
14064
+ tabManager,
14065
+ runtime,
14066
+ "vessel_scroll_to_element",
14067
+ { index, selector: rawSelector, position },
14068
+ async (wc) => {
14069
+ const sel = rawSelector || (index != null ? await resolveSelector(wc, index) : null);
14070
+ if (!sel) return "Error: Provide an index or selector.";
14071
+ const block = position === "top" ? "start" : position === "bottom" ? "end" : "center";
14072
+ if (sel.startsWith("__vessel_idx:")) {
14073
+ const idx = Number(sel.slice("__vessel_idx:".length));
14074
+ return wc.executeJavaScript(`
14075
+ (function() {
14076
+ var refs = window.__vessel;
14077
+ if (!refs || !refs.interactByIndex) return "Error: __vessel not available";
14078
+ // Use stored ref directly
14079
+ var el = document.querySelector('[data-vessel-idx="${idx}"]');
14080
+ if (!el) return "Error: Element #${idx} not found";
14081
+ el.scrollIntoView({ behavior: "smooth", block: "${block}" });
14082
+ return "Scrolled to element #${idx}";
14083
+ })()
14084
+ `);
14085
+ }
14086
+ if (sel.includes(" >>> ")) {
14087
+ return wc.executeJavaScript(`
14088
+ (function() {
14089
+ var el = window.__vessel?.resolveShadowSelector?.(${JSON.stringify(sel)});
14090
+ if (!el) return "Error: Shadow DOM element not found";
14091
+ el.scrollIntoView({ behavior: "smooth", block: "${block}" });
14092
+ return "Scrolled to shadow DOM element";
14093
+ })()
14094
+ `);
14095
+ }
14096
+ return wc.executeJavaScript(`
14097
+ (function() {
14098
+ var el = document.querySelector(${JSON.stringify(sel)});
14099
+ if (!el) return "Error: Element not found";
14100
+ el.scrollIntoView({ behavior: "smooth", block: "${block}" });
14101
+ return "Scrolled to element";
14102
+ })()
14103
+ `);
14104
+ }
14105
+ );
14106
+ }
14107
+ );
14108
+ server.registerTool(
14109
+ "vessel_wait_for_navigation",
14110
+ {
14111
+ title: "Wait For Navigation",
14112
+ description: "Wait for the current page to finish loading after a click or form submission.",
14113
+ inputSchema: zod.z.object({
14114
+ timeoutMs: zod.z.number().optional().describe("Max wait in milliseconds (default 10000)")
14115
+ })
14116
+ },
14117
+ async ({ timeoutMs }) => {
14118
+ return withAction(
14119
+ tabManager,
14120
+ runtime,
14121
+ "vessel_wait_for_navigation",
14122
+ { timeoutMs },
14123
+ async (wc) => {
14124
+ const timeout = timeoutMs || 1e4;
14125
+ const beforeUrl = wc.getURL();
14126
+ if (wc.isLoading()) {
14127
+ await new Promise((resolve) => {
14128
+ const timer = setTimeout(resolve, timeout);
14129
+ wc.once("did-stop-loading", () => {
14130
+ clearTimeout(timer);
14131
+ resolve();
14132
+ });
14133
+ });
14134
+ } else {
14135
+ await new Promise((resolve) => {
14136
+ let navigated = false;
14137
+ const timer = setTimeout(() => {
14138
+ if (!navigated) resolve();
14139
+ }, Math.min(timeout, 2e3));
14140
+ wc.once("did-start-loading", () => {
14141
+ navigated = true;
14142
+ clearTimeout(timer);
14143
+ const loadTimer = setTimeout(resolve, timeout);
14144
+ wc.once("did-stop-loading", () => {
14145
+ clearTimeout(loadTimer);
14146
+ resolve();
14147
+ });
14148
+ });
14149
+ });
14150
+ }
14151
+ const afterUrl = wc.getURL();
14152
+ const title = wc.getTitle();
14153
+ return afterUrl !== beforeUrl ? `Navigation complete: ${title} (${afterUrl})` : `Page loaded: ${title} (${afterUrl})`;
14154
+ }
14155
+ );
14156
+ }
14157
+ );
14158
+ server.registerTool(
14159
+ "vessel_metrics",
14160
+ {
14161
+ title: "Session Metrics",
14162
+ description: "Show performance metrics: total tool calls, average duration, per-tool breakdown.",
14163
+ inputSchema: zod.z.object({})
14164
+ },
14165
+ async () => {
14166
+ const m = runtime.getMetrics();
14167
+ const lines = [
14168
+ `Session Metrics:`,
14169
+ ` Total actions: ${m.totalActions}`,
14170
+ ` Completed: ${m.completedActions}`,
14171
+ ` Failed: ${m.failedActions}`,
14172
+ ` Average duration: ${m.averageDurationMs}ms`,
14173
+ ``,
14174
+ `Tool breakdown:`
14175
+ ];
14176
+ for (const [name, stats] of Object.entries(m.toolBreakdown)) {
14177
+ lines.push(` ${name}: ${stats.count} calls, avg ${stats.avgMs}ms${stats.errors > 0 ? `, ${stats.errors} errors` : ""}`);
14178
+ }
14179
+ return asTextResponse(lines.join("\n"));
14180
+ }
14181
+ );
12546
14182
  }
12547
14183
  function waitForLoad(wc, timeout = 1e4) {
12548
14184
  return new Promise((resolve) => {
@@ -12592,6 +14228,13 @@ async function resolveSelector(wc, index, selector) {
12592
14228
  `
12593
14229
  );
12594
14230
  if (typeof authoritativeSelector === "string" && authoritativeSelector) {
14231
+ if (authoritativeSelector.includes(" >>> ")) {
14232
+ const resolves2 = await wc.executeJavaScript(
14233
+ `!!window.__vessel?.resolveShadowSelector?.(${JSON.stringify(authoritativeSelector)})`
14234
+ );
14235
+ if (resolves2) return authoritativeSelector;
14236
+ return `__vessel_idx:${index}`;
14237
+ }
12595
14238
  const resolves = await wc.executeJavaScript(
12596
14239
  `!!document.querySelector(${JSON.stringify(authoritativeSelector)})`
12597
14240
  );
@@ -13091,6 +14734,112 @@ function registerIpcHandlers(windowState, runtime) {
13091
14734
  } catch {
13092
14735
  }
13093
14736
  });
14737
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_COUNT, () => {
14738
+ const tab = tabManager.getActiveTab();
14739
+ if (!tab) return 0;
14740
+ const wc = tab.view.webContents;
14741
+ if (wc.isDestroyed()) return 0;
14742
+ try {
14743
+ return wc.executeJavaScript(
14744
+ `document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text').length`
14745
+ );
14746
+ } catch {
14747
+ return 0;
14748
+ }
14749
+ });
14750
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_SCROLL, (_, index) => {
14751
+ const tab = tabManager.getActiveTab();
14752
+ if (!tab) return false;
14753
+ const wc = tab.view.webContents;
14754
+ if (wc.isDestroyed()) return false;
14755
+ try {
14756
+ return wc.executeJavaScript(`
14757
+ (function() {
14758
+ var highlights = document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text');
14759
+ if (${index} < 0 || ${index} >= highlights.length) return false;
14760
+ // Remove focus ring from all highlights
14761
+ highlights.forEach(function(h) { h.style.removeProperty('outline'); h.style.removeProperty('outline-offset'); });
14762
+ var target = highlights[${index}];
14763
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' });
14764
+ // Add focus ring to current highlight
14765
+ target.style.setProperty('outline', '2px solid rgba(255, 255, 255, 0.9)', 'important');
14766
+ target.style.setProperty('outline-offset', '2px', 'important');
14767
+ return true;
14768
+ })()
14769
+ `);
14770
+ } catch {
14771
+ return false;
14772
+ }
14773
+ });
14774
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, (_, index) => {
14775
+ const tab = tabManager.getActiveTab();
14776
+ if (!tab) return false;
14777
+ const wc = tab.view.webContents;
14778
+ if (wc.isDestroyed()) return false;
14779
+ try {
14780
+ return wc.executeJavaScript(`
14781
+ (function() {
14782
+ var highlights = document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text');
14783
+ if (${index} < 0 || ${index} >= highlights.length) return false;
14784
+ var el = highlights[${index}];
14785
+ // Remove associated label if any
14786
+ document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) {
14787
+ if (b.__vesselAnchor === el) b.remove();
14788
+ });
14789
+ // Unwrap text highlights, remove class from element highlights
14790
+ if (el.tagName === 'MARK' && el.classList.contains('__vessel-highlight-text')) {
14791
+ var parent = el.parentNode;
14792
+ while (el.firstChild) parent.insertBefore(el.firstChild, el);
14793
+ parent.removeChild(el);
14794
+ parent.normalize();
14795
+ } else {
14796
+ el.classList.remove('__vessel-highlight');
14797
+ el.style.removeProperty('background');
14798
+ el.style.removeProperty('outline-color');
14799
+ el.style.removeProperty('box-shadow');
14800
+ el.style.removeProperty('outline');
14801
+ el.style.removeProperty('outline-offset');
14802
+ }
14803
+ return true;
14804
+ })()
14805
+ `);
14806
+ } catch {
14807
+ return false;
14808
+ }
14809
+ });
14810
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, () => {
14811
+ const tab = tabManager.getActiveTab();
14812
+ if (!tab) return false;
14813
+ const wc = tab.view.webContents;
14814
+ if (wc.isDestroyed()) return false;
14815
+ try {
14816
+ return wc.executeJavaScript(`
14817
+ (function() {
14818
+ // Remove all labels
14819
+ document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) { b.remove(); });
14820
+ // Unwrap text highlights
14821
+ document.querySelectorAll('.__vessel-highlight-text').forEach(function(mark) {
14822
+ var parent = mark.parentNode;
14823
+ while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
14824
+ parent.removeChild(mark);
14825
+ parent.normalize();
14826
+ });
14827
+ // Remove element highlights
14828
+ document.querySelectorAll('.__vessel-highlight').forEach(function(el) {
14829
+ el.classList.remove('__vessel-highlight');
14830
+ el.style.removeProperty('background');
14831
+ el.style.removeProperty('outline-color');
14832
+ el.style.removeProperty('box-shadow');
14833
+ el.style.removeProperty('outline');
14834
+ el.style.removeProperty('outline-offset');
14835
+ });
14836
+ return true;
14837
+ })()
14838
+ `);
14839
+ } catch {
14840
+ return false;
14841
+ }
14842
+ });
13094
14843
  electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => {
13095
14844
  windowState.uiState.devtoolsPanelOpen = !windowState.uiState.devtoolsPanelOpen;
13096
14845
  layoutViews(windowState);
@@ -13355,6 +15104,7 @@ ${progress}
13355
15104
  mode: "replace"
13356
15105
  });
13357
15106
  const approvalReason = this.getApprovalReason(dangerous);
15107
+ console.log(`[Vessel Runtime] action=${name} dangerous=${dangerous} approvalReason=${approvalReason} mode=${this.state.supervisor.approvalMode}`);
13358
15108
  if (approvalReason) {
13359
15109
  this.publishTranscript({
13360
15110
  source,
@@ -13364,7 +15114,9 @@ ${progress}
13364
15114
  streamId: transcriptStreamId,
13365
15115
  mode: "replace"
13366
15116
  });
15117
+ console.log(`[Vessel Runtime] awaiting approval for ${name}...`);
13367
15118
  const approved = await this.awaitApproval(action, approvalReason);
15119
+ console.log(`[Vessel Runtime] approval result for ${name}: ${approved}`);
13368
15120
  if (!approved) {
13369
15121
  this.publishTranscript({
13370
15122
  source,
@@ -13499,13 +15251,44 @@ ${progress}
13499
15251
  this.emit();
13500
15252
  }
13501
15253
  finishAction(actionId, status, resultSummary, error) {
15254
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
15255
+ const action = this.state.actions.find((a) => a.id === actionId);
15256
+ const durationMs = action ? new Date(finishedAt).getTime() - new Date(action.startedAt).getTime() : void 0;
13502
15257
  this.updateAction(actionId, {
13503
15258
  status,
13504
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
15259
+ finishedAt,
15260
+ durationMs,
13505
15261
  resultSummary,
13506
15262
  error
13507
15263
  });
13508
15264
  }
15265
+ /** Aggregate metrics for all completed actions in this session. */
15266
+ getMetrics() {
15267
+ const completed = this.state.actions.filter((a) => a.status === "completed");
15268
+ const failed = this.state.actions.filter((a) => a.status === "error");
15269
+ const durations = completed.filter((a) => a.durationMs != null).map((a) => a.durationMs);
15270
+ const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
15271
+ const toolBreakdown = {};
15272
+ for (const action of this.state.actions) {
15273
+ const name = action.name;
15274
+ if (!toolBreakdown[name]) toolBreakdown[name] = { count: 0, totalMs: 0, avgMs: 0, errors: 0 };
15275
+ toolBreakdown[name].count++;
15276
+ if (action.durationMs != null) toolBreakdown[name].totalMs += action.durationMs;
15277
+ if (action.status === "error") toolBreakdown[name].errors++;
15278
+ }
15279
+ for (const entry of Object.values(toolBreakdown)) {
15280
+ entry.avgMs = entry.count > 0 ? Math.round(entry.totalMs / entry.count) : 0;
15281
+ }
15282
+ return {
15283
+ totalActions: this.state.actions.length,
15284
+ completedActions: completed.length,
15285
+ failedActions: failed.length,
15286
+ averageDurationMs: Math.round(avgDuration),
15287
+ toolBreakdown: Object.fromEntries(
15288
+ Object.entries(toolBreakdown).map(([k, v]) => [k, { count: v.count, avgMs: v.avgMs, errors: v.errors }])
15289
+ )
15290
+ };
15291
+ }
13509
15292
  getApprovalReason(dangerous) {
13510
15293
  if (this.state.supervisor.paused) {
13511
15294
  return "Agent execution is paused";