@quanta-intellect/vessel-browser 0.1.13 → 0.1.15

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
@@ -8,11 +8,125 @@ const Anthropic = require("@anthropic-ai/sdk");
8
8
  const OpenAI = require("openai");
9
9
  const zod = require("zod");
10
10
  const path$1 = require("node:path");
11
- const node_crypto = require("node:crypto");
11
+ const crypto$1 = require("node:crypto");
12
12
  const http = require("node:http");
13
13
  const os = require("node:os");
14
14
  const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
15
15
  const streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
16
+ const defaults = {
17
+ defaultUrl: "https://start.duckduckgo.com",
18
+ theme: "dark",
19
+ sidebarWidth: 340,
20
+ mcpPort: 3100,
21
+ autoRestoreSession: true,
22
+ clearBookmarksOnLaunch: false,
23
+ obsidianVaultPath: "",
24
+ approvalMode: "confirm-dangerous",
25
+ agentTranscriptMode: "summary",
26
+ chatProvider: null,
27
+ maxToolIterations: 200,
28
+ domainPolicy: { allowedDomains: [], blockedDomains: [] },
29
+ downloadPath: ""
30
+ };
31
+ const SETTABLE_KEYS = new Set(Object.keys(defaults));
32
+ let settings = null;
33
+ let settingsIssues = [];
34
+ function getSettingsPath() {
35
+ return path.join(electron.app.getPath("userData"), "vessel-settings.json");
36
+ }
37
+ function getSettingsLoadIssues() {
38
+ return settingsIssues.map((issue) => ({ ...issue }));
39
+ }
40
+ function sanitizePort(value) {
41
+ const parsed = Number(value);
42
+ if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) {
43
+ return parsed;
44
+ }
45
+ settingsIssues.push({
46
+ code: "settings-invalid-mcp-port",
47
+ severity: "warning",
48
+ title: "Invalid MCP port in settings",
49
+ detail: `Expected an integer between 1 and 65535 but found ${JSON.stringify(value)}.`,
50
+ action: `Using default port ${defaults.mcpPort} instead.`
51
+ });
52
+ return defaults.mcpPort;
53
+ }
54
+ function loadSettings() {
55
+ if (settings) return settings;
56
+ settingsIssues = [];
57
+ try {
58
+ const raw = fs.readFileSync(getSettingsPath(), "utf-8");
59
+ const parsed = JSON.parse(raw);
60
+ delete parsed.apiKey;
61
+ delete parsed.provider;
62
+ settings = {
63
+ ...defaults,
64
+ ...parsed,
65
+ mcpPort: sanitizePort(parsed.mcpPort ?? defaults.mcpPort),
66
+ agentTranscriptMode: parsed.agentTranscriptMode === "off" || parsed.agentTranscriptMode === "summary" || parsed.agentTranscriptMode === "full" ? parsed.agentTranscriptMode : parsed.showAgentTranscript === false ? "off" : defaults.agentTranscriptMode
67
+ };
68
+ } catch (error) {
69
+ if (fs.existsSync(getSettingsPath())) {
70
+ settingsIssues.push({
71
+ code: "settings-read-failed",
72
+ severity: "warning",
73
+ title: "Could not read Vessel settings",
74
+ detail: error instanceof Error ? error.message : "Unknown settings error.",
75
+ action: "Falling back to built-in defaults for this launch."
76
+ });
77
+ }
78
+ settings = { ...defaults };
79
+ }
80
+ return settings;
81
+ }
82
+ function saveSettings() {
83
+ try {
84
+ fs.mkdirSync(path.dirname(getSettingsPath()), { recursive: true });
85
+ fs.writeFileSync(getSettingsPath(), JSON.stringify(settings, null, 2));
86
+ } catch (err) {
87
+ console.error("[Vessel] Failed to save settings:", err);
88
+ }
89
+ }
90
+ function setSetting(key, value) {
91
+ loadSettings();
92
+ if (key === "mcpPort") {
93
+ settings.mcpPort = sanitizePort(value);
94
+ } else {
95
+ settings[key] = value;
96
+ }
97
+ saveSettings();
98
+ return { ...settings };
99
+ }
100
+ function checkDomainPolicy(url) {
101
+ if (!url || url.startsWith("about:")) return null;
102
+ const settings2 = loadSettings();
103
+ const policy = settings2.domainPolicy;
104
+ if (policy.allowedDomains.length === 0 && policy.blockedDomains.length === 0) {
105
+ return null;
106
+ }
107
+ let hostname;
108
+ try {
109
+ hostname = new URL(url).hostname.toLowerCase();
110
+ } catch {
111
+ return null;
112
+ }
113
+ if (policy.allowedDomains.length > 0) {
114
+ const allowed = policy.allowedDomains.some(
115
+ (d) => matchesDomain(hostname, d.toLowerCase())
116
+ );
117
+ return allowed ? null : `Navigation blocked by domain policy: ${hostname} is not in the allowed domains list.`;
118
+ }
119
+ if (policy.blockedDomains.length > 0) {
120
+ const blocked = policy.blockedDomains.some(
121
+ (d) => matchesDomain(hostname, d.toLowerCase())
122
+ );
123
+ return blocked ? `Navigation blocked by domain policy: ${hostname} is in the blocked domains list.` : null;
124
+ }
125
+ return null;
126
+ }
127
+ function matchesDomain(hostname, policyDomain) {
128
+ return hostname === policyDomain || hostname.endsWith("." + policyDomain);
129
+ }
16
130
  const MAX_CUSTOM_HISTORY = 50;
17
131
  class Tab {
18
132
  id;
@@ -60,7 +174,8 @@ class Tab {
60
174
  canGoBack: false,
61
175
  canGoForward: false,
62
176
  isReaderMode: false,
63
- adBlockingEnabled: options?.adBlockingEnabled ?? true
177
+ adBlockingEnabled: options?.adBlockingEnabled ?? true,
178
+ role: options?.role
64
179
  };
65
180
  this.view.webContents.on("before-input-event", (_event, input) => {
66
181
  if (!input.control && !input.meta) return;
@@ -250,7 +365,13 @@ class Tab {
250
365
  url = `https://duckduckgo.com/?q=${encodeURIComponent(url)}`;
251
366
  }
252
367
  }
368
+ if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
369
+ return `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`;
370
+ }
371
+ const policyError = checkDomainPolicy(url);
372
+ if (policyError) return policyError;
253
373
  this.view.webContents.loadURL(url);
374
+ return null;
254
375
  }
255
376
  goBack() {
256
377
  const previousUrl = this.urlHistory.pop();
@@ -388,31 +509,35 @@ class Tab {
388
509
  this.view.webContents.close();
389
510
  }
390
511
  }
391
- let state$2 = null;
392
- const listeners$1 = /* @__PURE__ */ new Set();
512
+ let state$3 = null;
513
+ const listeners$2 = /* @__PURE__ */ new Set();
393
514
  function getHighlightsPath() {
394
515
  return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
395
516
  }
396
- function load$1() {
397
- if (state$2) return state$2;
517
+ function load$2() {
518
+ if (state$3) return state$3;
398
519
  try {
399
520
  const raw = fs.readFileSync(getHighlightsPath(), "utf-8");
400
521
  const parsed = JSON.parse(raw);
401
- state$2 = {
522
+ state$3 = {
402
523
  highlights: Array.isArray(parsed.highlights) ? parsed.highlights : []
403
524
  };
404
525
  } catch {
405
- state$2 = { highlights: [] };
526
+ state$3 = { highlights: [] };
406
527
  }
407
- return state$2;
528
+ return state$3;
408
529
  }
409
- function save$1() {
410
- fs.writeFileSync(getHighlightsPath(), JSON.stringify(state$2, null, 2), "utf-8");
530
+ function save$2() {
531
+ try {
532
+ fs.writeFileSync(getHighlightsPath(), JSON.stringify(state$3, null, 2), "utf-8");
533
+ } catch (err) {
534
+ console.error("[Vessel] Failed to save highlights:", err);
535
+ }
411
536
  }
412
- function emit$1() {
413
- if (!state$2) return;
414
- const snapshot = { highlights: [...state$2.highlights] };
415
- for (const listener of listeners$1) {
537
+ function emit$2() {
538
+ if (!state$3) return;
539
+ const snapshot = { highlights: [...state$3.highlights] };
540
+ for (const listener of listeners$2) {
416
541
  listener(snapshot);
417
542
  }
418
543
  }
@@ -425,17 +550,17 @@ function normalizeUrl(rawUrl) {
425
550
  return rawUrl;
426
551
  }
427
552
  }
428
- function getState$1() {
429
- load$1();
430
- return { highlights: [...state$2.highlights] };
553
+ function getState$2() {
554
+ load$2();
555
+ return { highlights: [...state$3.highlights] };
431
556
  }
432
557
  function getHighlightsForUrl(url) {
433
- load$1();
558
+ load$2();
434
559
  const normalized = normalizeUrl(url);
435
- return state$2.highlights.filter((h) => h.url === normalized);
560
+ return state$3.highlights.filter((h) => h.url === normalized);
436
561
  }
437
562
  function addHighlight(url, selector, text, label, color, source) {
438
- load$1();
563
+ load$2();
439
564
  const highlight = {
440
565
  id: crypto.randomUUID(),
441
566
  url: normalizeUrl(url),
@@ -446,45 +571,45 @@ function addHighlight(url, selector, text, label, color, source) {
446
571
  source: source || void 0,
447
572
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
448
573
  };
449
- state$2.highlights.push(highlight);
450
- save$1();
451
- emit$1();
574
+ state$3.highlights.push(highlight);
575
+ save$2();
576
+ emit$2();
452
577
  return highlight;
453
578
  }
454
579
  function removeHighlight(id) {
455
- load$1();
456
- const index = state$2.highlights.findIndex((h) => h.id === id);
580
+ load$2();
581
+ const index = state$3.highlights.findIndex((h) => h.id === id);
457
582
  if (index === -1) return null;
458
- const [removed] = state$2.highlights.splice(index, 1);
459
- save$1();
460
- emit$1();
583
+ const [removed] = state$3.highlights.splice(index, 1);
584
+ save$2();
585
+ emit$2();
461
586
  return removed;
462
587
  }
463
588
  function findHighlightByText(url, text) {
464
- load$1();
589
+ load$2();
465
590
  const normalized = normalizeUrl(url);
466
- return state$2.highlights.find(
591
+ return state$3.highlights.find(
467
592
  (h) => h.url === normalized && h.text && h.text === text
468
593
  ) ?? null;
469
594
  }
470
595
  function updateHighlightColor(id, color) {
471
- load$1();
472
- const highlight = state$2.highlights.find((h) => h.id === id);
596
+ load$2();
597
+ const highlight = state$3.highlights.find((h) => h.id === id);
473
598
  if (!highlight) return null;
474
599
  highlight.color = color;
475
- save$1();
476
- emit$1();
600
+ save$2();
601
+ emit$2();
477
602
  return highlight;
478
603
  }
479
604
  function clearHighlightsForUrl(url) {
480
- load$1();
605
+ load$2();
481
606
  const normalized = normalizeUrl(url);
482
- const before = state$2.highlights.length;
483
- state$2.highlights = state$2.highlights.filter((h) => h.url !== normalized);
484
- const removed = before - state$2.highlights.length;
607
+ const before = state$3.highlights.length;
608
+ state$3.highlights = state$3.highlights.filter((h) => h.url !== normalized);
609
+ const removed = before - state$3.highlights.length;
485
610
  if (removed > 0) {
486
- save$1();
487
- emit$1();
611
+ save$2();
612
+ emit$2();
488
613
  }
489
614
  return removed;
490
615
  }
@@ -710,7 +835,8 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
710
835
  if (text) {
711
836
  return wc.executeJavaScript(`
712
837
  (function() {
713
- var searchText = ${JSON.stringify(text)};
838
+ var searchText = (${JSON.stringify(text)} || '').trim();
839
+ var foldedSearchText = searchText.toLowerCase();
714
840
  var solidColor = ${JSON.stringify(c.solid)};
715
841
  var bgColor = ${JSON.stringify(c.bg)};
716
842
  var labelBg = ${JSON.stringify(c.label)};
@@ -748,7 +874,11 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
748
874
  });
749
875
  var n;
750
876
  while ((n = w.nextNode())) {
751
- var idx = n.textContent.indexOf(searchText);
877
+ var haystack = n.textContent || '';
878
+ var idx = haystack.indexOf(searchText);
879
+ if (idx === -1 && foldedSearchText) {
880
+ idx = haystack.toLowerCase().indexOf(foldedSearchText);
881
+ }
752
882
  if (idx !== -1) {
753
883
  matches.push({ node: n, idx: idx });
754
884
  if (matches.length >= limit) break;
@@ -819,6 +949,172 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
819
949
  }
820
950
  return "Error: No element or text to highlight";
821
951
  }
952
+ async function highlightBatchOnPage(wc, entries) {
953
+ if (entries.length === 0) return;
954
+ const serialized = entries.filter((e) => e.selector || e.text).map((e) => ({
955
+ selector: e.selector ?? null,
956
+ text: e.text ?? null,
957
+ label: e.label ?? null,
958
+ color: resolveColor(e.color)
959
+ }));
960
+ if (serialized.length === 0) return;
961
+ await wc.executeJavaScript(`
962
+ (function() {
963
+ if (!document.getElementById('__vessel-highlight-styles')) {
964
+ var s = document.createElement('style');
965
+ s.id = '__vessel-highlight-styles';
966
+ s.textContent = ${JSON.stringify(VESSEL_HIGHLIGHT_CSS)};
967
+ document.head.appendChild(s);
968
+ }
969
+ var entries = ${JSON.stringify(serialized)};
970
+ var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};
971
+ var contentRoots = ['main', 'article', '[role="main"]', '#mw-content-text', '.mw-parser-output', '#content', '.post-content', '.entry-content', '.article-body'];
972
+ var NAV_ANCESTORS = 'nav, aside, footer, header, [role="navigation"], [role="complementary"], .sidebar, .navbox, .infobox, figcaption, .thumbcaption, .mw-jump-link';
973
+ var contentRoot = null;
974
+ for (var cr = 0; cr < contentRoots.length; cr++) {
975
+ contentRoot = document.querySelector(contentRoots[cr]);
976
+ if (contentRoot) break;
977
+ }
978
+
979
+ function collectMatches(root, searchText, foldedSearchText, limit) {
980
+ var matches = [];
981
+ var w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
982
+ acceptNode: function(n) {
983
+ var p = n.parentElement;
984
+ if (!p) return NodeFilter.FILTER_REJECT;
985
+ if (SKIP_TAGS[p.tagName]) return NodeFilter.FILTER_REJECT;
986
+ if (p.closest('[data-vessel-highlight]')) return NodeFilter.FILTER_REJECT;
987
+ if (p.closest(NAV_ANCESTORS)) return NodeFilter.FILTER_REJECT;
988
+ var style = window.getComputedStyle(p);
989
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return NodeFilter.FILTER_REJECT;
990
+ if (p.offsetWidth === 0 && p.offsetHeight === 0) return NodeFilter.FILTER_REJECT;
991
+ return NodeFilter.FILTER_ACCEPT;
992
+ }
993
+ });
994
+ var n;
995
+ while ((n = w.nextNode())) {
996
+ var haystack = n.textContent || '';
997
+ var idx = haystack.indexOf(searchText);
998
+ if (idx === -1 && foldedSearchText) {
999
+ idx = haystack.toLowerCase().indexOf(foldedSearchText);
1000
+ }
1001
+ if (idx !== -1) {
1002
+ matches.push({ node: n, idx: idx });
1003
+ if (matches.length >= limit) break;
1004
+ }
1005
+ }
1006
+ return matches;
1007
+ }
1008
+
1009
+ for (var e = 0; e < entries.length; e++) {
1010
+ var entry = entries[e];
1011
+ var c = entry.color;
1012
+ if (entry.text) {
1013
+ var searchText = (entry.text || '').trim();
1014
+ var foldedSearchText = searchText.toLowerCase();
1015
+ var textNodes = contentRoot ? collectMatches(contentRoot, searchText, foldedSearchText, 20) : [];
1016
+ if (textNodes.length === 0) {
1017
+ textNodes = collectMatches(document.body, searchText, foldedSearchText, 20);
1018
+ }
1019
+ for (var i = 0; i < textNodes.length; i++) {
1020
+ try {
1021
+ var match = textNodes[i];
1022
+ var range = document.createRange();
1023
+ range.setStart(match.node, match.idx);
1024
+ range.setEnd(match.node, match.idx + searchText.length);
1025
+ var mark = document.createElement('mark');
1026
+ mark.className = '__vessel-highlight-text';
1027
+ mark.style.setProperty('background', c.bg, 'important');
1028
+ mark.style.setProperty('border-bottom-color', c.solid, 'important');
1029
+ mark.setAttribute('data-vessel-highlight', 'true');
1030
+ range.surroundContents(mark);
1031
+ } catch (_e) {}
1032
+ }
1033
+ } else if (entry.selector) {
1034
+ try {
1035
+ var el = document.querySelector(entry.selector);
1036
+ if (el) {
1037
+ el.classList.add('__vessel-highlight');
1038
+ el.style.setProperty('background', c.bg, 'important');
1039
+ el.style.setProperty('outline-color', c.solid, 'important');
1040
+ el.style.setProperty('box-shadow', '0 0 8px ' + c.glow, 'important');
1041
+ }
1042
+ } catch (_e) {}
1043
+ }
1044
+ }
1045
+ })()
1046
+ `);
1047
+ }
1048
+ const HIGHLIGHT_SELECTOR = "'.__vessel-highlight, .__vessel-highlight-text'";
1049
+ async function getHighlightCount(wc) {
1050
+ return wc.executeJavaScript(
1051
+ `document.querySelectorAll(${HIGHLIGHT_SELECTOR}).length`
1052
+ );
1053
+ }
1054
+ async function scrollToHighlight(wc, index) {
1055
+ const safeIndex = Math.floor(Number(index));
1056
+ return wc.executeJavaScript(`
1057
+ (function() {
1058
+ var highlights = document.querySelectorAll(${HIGHLIGHT_SELECTOR});
1059
+ if (${safeIndex} < 0 || ${safeIndex} >= highlights.length) return false;
1060
+ highlights.forEach(function(h) { h.style.removeProperty('outline'); h.style.removeProperty('outline-offset'); });
1061
+ var target = highlights[${safeIndex}];
1062
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' });
1063
+ target.style.setProperty('outline', '2px solid rgba(255, 255, 255, 0.9)', 'important');
1064
+ target.style.setProperty('outline-offset', '2px', 'important');
1065
+ return true;
1066
+ })()
1067
+ `);
1068
+ }
1069
+ async function removeHighlightAtIndex(wc, index) {
1070
+ const safeIndex = Math.floor(Number(index));
1071
+ return wc.executeJavaScript(`
1072
+ (function() {
1073
+ var highlights = document.querySelectorAll(${HIGHLIGHT_SELECTOR});
1074
+ if (${safeIndex} < 0 || ${safeIndex} >= highlights.length) return false;
1075
+ var el = highlights[${safeIndex}];
1076
+ document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) {
1077
+ if (b.__vesselAnchor === el) b.remove();
1078
+ });
1079
+ if (el.tagName === 'MARK' && el.classList.contains('__vessel-highlight-text')) {
1080
+ var parent = el.parentNode;
1081
+ while (el.firstChild) parent.insertBefore(el.firstChild, el);
1082
+ parent.removeChild(el);
1083
+ parent.normalize();
1084
+ } else {
1085
+ el.classList.remove('__vessel-highlight');
1086
+ el.style.removeProperty('background');
1087
+ el.style.removeProperty('outline-color');
1088
+ el.style.removeProperty('box-shadow');
1089
+ el.style.removeProperty('outline');
1090
+ el.style.removeProperty('outline-offset');
1091
+ }
1092
+ return true;
1093
+ })()
1094
+ `);
1095
+ }
1096
+ async function clearAllHighlightElements(wc) {
1097
+ return wc.executeJavaScript(`
1098
+ (function() {
1099
+ document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) { b.remove(); });
1100
+ document.querySelectorAll('.__vessel-highlight-text').forEach(function(mark) {
1101
+ var parent = mark.parentNode;
1102
+ while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
1103
+ parent.removeChild(mark);
1104
+ parent.normalize();
1105
+ });
1106
+ document.querySelectorAll('.__vessel-highlight').forEach(function(el) {
1107
+ el.classList.remove('__vessel-highlight');
1108
+ el.style.removeProperty('background');
1109
+ el.style.removeProperty('outline-color');
1110
+ el.style.removeProperty('box-shadow');
1111
+ el.style.removeProperty('outline');
1112
+ el.style.removeProperty('outline-offset');
1113
+ });
1114
+ return true;
1115
+ })()
1116
+ `);
1117
+ }
822
1118
  async function clearHighlights(wc) {
823
1119
  return wc.executeJavaScript(`
824
1120
  (function() {
@@ -843,6 +1139,134 @@ async function clearHighlights(wc) {
843
1139
  })()
844
1140
  `);
845
1141
  }
1142
+ const MAX_HIGHLIGHT_TEXT = 5e3;
1143
+ async function captureSelectionHighlight(wc) {
1144
+ if (wc.isDestroyed()) {
1145
+ return { success: false, message: "Tab is not available" };
1146
+ }
1147
+ const url = wc.getURL();
1148
+ if (!url || url === "about:blank") {
1149
+ return { success: false, message: "No page loaded" };
1150
+ }
1151
+ const selectedText = await wc.executeJavaScript(`
1152
+ (function() {
1153
+ var sel = window.getSelection();
1154
+ return sel ? sel.toString().trim() : '';
1155
+ })()
1156
+ `);
1157
+ if (!selectedText) {
1158
+ return { success: false, message: "No text selected" };
1159
+ }
1160
+ return persistHighlight(url, selectedText);
1161
+ }
1162
+ async function persistAndMarkHighlight(wc, text) {
1163
+ if (wc.isDestroyed()) {
1164
+ return { success: false, message: "Tab is not available" };
1165
+ }
1166
+ const url = wc.getURL();
1167
+ if (!url || url === "about:blank") {
1168
+ return { success: false, message: "No page loaded" };
1169
+ }
1170
+ const result = persistHighlight(url, text);
1171
+ return result;
1172
+ }
1173
+ function persistHighlight(url, text) {
1174
+ const capped = text.length > MAX_HIGHLIGHT_TEXT ? text.slice(0, MAX_HIGHLIGHT_TEXT) : text;
1175
+ const highlight = addHighlight(
1176
+ url,
1177
+ void 0,
1178
+ capped,
1179
+ void 0,
1180
+ "yellow",
1181
+ "user"
1182
+ );
1183
+ return { success: true, text: capped, id: highlight.id };
1184
+ }
1185
+ const MAX_HISTORY_ENTRIES = 5e3;
1186
+ let state$2 = null;
1187
+ const listeners$1 = /* @__PURE__ */ new Set();
1188
+ function getHistoryPath() {
1189
+ return path.join(electron.app.getPath("userData"), "vessel-history.json");
1190
+ }
1191
+ function load$1() {
1192
+ if (state$2) return state$2;
1193
+ try {
1194
+ const raw = fs.readFileSync(getHistoryPath(), "utf-8");
1195
+ const parsed = JSON.parse(raw);
1196
+ state$2 = {
1197
+ entries: Array.isArray(parsed.entries) ? parsed.entries : []
1198
+ };
1199
+ } catch {
1200
+ state$2 = { entries: [] };
1201
+ }
1202
+ return state$2;
1203
+ }
1204
+ function save$1() {
1205
+ try {
1206
+ fs.mkdirSync(path.dirname(getHistoryPath()), { recursive: true });
1207
+ fs.writeFileSync(
1208
+ getHistoryPath(),
1209
+ JSON.stringify(state$2, null, 2),
1210
+ "utf-8"
1211
+ );
1212
+ } catch (err) {
1213
+ console.error("[Vessel] Failed to save history:", err);
1214
+ }
1215
+ }
1216
+ function emit$1() {
1217
+ if (!state$2) return;
1218
+ const snapshot = { entries: [...state$2.entries] };
1219
+ for (const listener of listeners$1) {
1220
+ listener(snapshot);
1221
+ }
1222
+ }
1223
+ function getState$1() {
1224
+ load$1();
1225
+ return { entries: [...state$2.entries] };
1226
+ }
1227
+ function subscribe$1(listener) {
1228
+ listeners$1.add(listener);
1229
+ return () => {
1230
+ listeners$1.delete(listener);
1231
+ };
1232
+ }
1233
+ function addEntry(url, title) {
1234
+ if (!url || url === "about:blank") return;
1235
+ load$1();
1236
+ const last = state$2.entries[0];
1237
+ if (last && last.url === url) {
1238
+ if (title && title !== last.title) {
1239
+ last.title = title;
1240
+ save$1();
1241
+ emit$1();
1242
+ }
1243
+ return;
1244
+ }
1245
+ const entry = {
1246
+ url,
1247
+ title: title || url,
1248
+ visitedAt: (/* @__PURE__ */ new Date()).toISOString()
1249
+ };
1250
+ state$2.entries.unshift(entry);
1251
+ if (state$2.entries.length > MAX_HISTORY_ENTRIES) {
1252
+ state$2.entries = state$2.entries.slice(0, MAX_HISTORY_ENTRIES);
1253
+ }
1254
+ save$1();
1255
+ emit$1();
1256
+ }
1257
+ function search(query, limit = 50) {
1258
+ load$1();
1259
+ if (!query.trim()) return state$2.entries.slice(0, limit);
1260
+ const normalized = query.toLowerCase();
1261
+ return state$2.entries.filter(
1262
+ (e) => e.url.toLowerCase().includes(normalized) || e.title.toLowerCase().includes(normalized)
1263
+ ).slice(0, limit);
1264
+ }
1265
+ function clearAll$1() {
1266
+ state$2 = { entries: [] };
1267
+ save$1();
1268
+ emit$1();
1269
+ }
846
1270
  const MAX_CONSOLE_ENTRIES = 500;
847
1271
  const MAX_NETWORK_ENTRIES = 200;
848
1272
  const MAX_ERROR_ENTRIES = 200;
@@ -1567,7 +1991,10 @@ class TabManager {
1567
1991
  onOpenUrl: ({ url: requestedUrl, background: background2, adBlockingEnabled }) => {
1568
1992
  this.createTab(requestedUrl, { background: background2, adBlockingEnabled });
1569
1993
  },
1570
- onPageLoad: (pageUrl, wc) => this.reapplyHighlights(pageUrl, wc),
1994
+ onPageLoad: (pageUrl, wc) => {
1995
+ this.reapplyHighlights(pageUrl, wc);
1996
+ addEntry(pageUrl, wc.getTitle());
1997
+ },
1571
1998
  onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
1572
1999
  onHighlightRemove: (url2, text) => this.removeHighlightByText(url2, text),
1573
2000
  onHighlightRecolor: (url2, text, color) => this.recolorHighlightByText(url2, text, color)
@@ -1719,16 +2146,14 @@ class TabManager {
1719
2146
  if (last && last.url === normalized && now - last.at < 500) return;
1720
2147
  this.lastReapply.set(wcId, { url: normalized, at: now });
1721
2148
  const highlights = getHighlightsForUrl(url);
1722
- for (const h of highlights) {
1723
- if (!h.selector && !h.text) continue;
1724
- void highlightOnPage(
1725
- wc,
1726
- h.selector ?? null,
1727
- h.text,
1728
- h.label,
1729
- void 0,
1730
- h.color
1731
- ).catch(() => {
2149
+ const entries = highlights.filter((h) => h.selector || h.text).map((h) => ({
2150
+ selector: h.selector ?? null,
2151
+ text: h.text,
2152
+ label: h.label,
2153
+ color: h.color
2154
+ }));
2155
+ if (entries.length > 0) {
2156
+ void highlightBatchOnPage(wc, entries).catch(() => {
1732
2157
  });
1733
2158
  }
1734
2159
  }
@@ -1736,62 +2161,23 @@ class TabManager {
1736
2161
  this.highlightCaptureCallback = callback;
1737
2162
  }
1738
2163
  captureHighlightFromActiveTab() {
1739
- console.log("[Vessel] captureHighlightFromActiveTab called");
1740
2164
  const activeTab = this.getActiveTab();
1741
2165
  if (!activeTab) {
1742
- console.log("[Vessel] No active tab in captureHighlightFromActiveTab");
1743
2166
  return { success: false, message: "No active tab" };
1744
2167
  }
1745
2168
  const wc = activeTab.view.webContents;
1746
- console.log("[Vessel] Calling captureHighlightFromPage for:", wc.getURL());
1747
2169
  this.captureHighlightFromPage(wc);
1748
2170
  return null;
1749
2171
  }
1750
2172
  captureHighlightFromPage(wc) {
1751
- console.log("[Vessel] captureHighlightFromPage called");
1752
2173
  void (async () => {
1753
2174
  try {
1754
- if (wc.isDestroyed()) {
1755
- console.log("[Vessel] WebContents destroyed");
1756
- return;
1757
- }
1758
- const url = wc.getURL();
1759
- console.log("[Vessel] URL:", url);
1760
- if (!url || url === "about:blank") {
1761
- console.log("[Vessel] No URL or about:blank");
1762
- return;
2175
+ const result = await captureSelectionHighlight(wc);
2176
+ if (result.success && result.text) {
2177
+ await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(() => {
2178
+ });
1763
2179
  }
1764
- const selectedText = await wc.executeJavaScript(`
1765
- (function() {
1766
- var sel = window.getSelection();
1767
- return sel ? sel.toString().trim() : '';
1768
- })()
1769
- `);
1770
- console.log("[Vessel] Selected text:", selectedText?.slice(0, 50));
1771
- if (!selectedText) return;
1772
- const capped = selectedText.length > 5e3 ? selectedText.slice(0, 5e3) : selectedText;
1773
- const highlight = addHighlight(
1774
- url,
1775
- void 0,
1776
- capped,
1777
- void 0,
1778
- "yellow",
1779
- "user"
1780
- );
1781
- await highlightOnPage(
1782
- wc,
1783
- null,
1784
- capped,
1785
- void 0,
1786
- void 0,
1787
- "yellow"
1788
- ).catch(() => {
1789
- });
1790
- this.highlightCaptureCallback?.({
1791
- success: true,
1792
- text: capped,
1793
- id: highlight.id
1794
- });
2180
+ this.highlightCaptureCallback?.(result);
1795
2181
  } catch {
1796
2182
  this.highlightCaptureCallback?.({
1797
2183
  success: false,
@@ -1879,83 +2265,6 @@ class TabManager {
1879
2265
  this.onStateChange(states, this.activeTabId || "");
1880
2266
  }
1881
2267
  }
1882
- const defaults = {
1883
- defaultUrl: "https://start.duckduckgo.com",
1884
- theme: "dark",
1885
- sidebarWidth: 340,
1886
- mcpPort: 3100,
1887
- autoRestoreSession: true,
1888
- clearBookmarksOnLaunch: false,
1889
- obsidianVaultPath: "",
1890
- approvalMode: "confirm-dangerous",
1891
- agentTranscriptMode: "summary",
1892
- chatProvider: null,
1893
- maxToolIterations: 200
1894
- };
1895
- let settings = null;
1896
- let settingsIssues = [];
1897
- function getSettingsPath() {
1898
- return path.join(electron.app.getPath("userData"), "vessel-settings.json");
1899
- }
1900
- function getSettingsLoadIssues() {
1901
- return settingsIssues.map((issue) => ({ ...issue }));
1902
- }
1903
- function sanitizePort(value) {
1904
- const parsed = Number(value);
1905
- if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) {
1906
- return parsed;
1907
- }
1908
- settingsIssues.push({
1909
- code: "settings-invalid-mcp-port",
1910
- severity: "warning",
1911
- title: "Invalid MCP port in settings",
1912
- detail: `Expected an integer between 1 and 65535 but found ${JSON.stringify(value)}.`,
1913
- action: `Using default port ${defaults.mcpPort} instead.`
1914
- });
1915
- return defaults.mcpPort;
1916
- }
1917
- function loadSettings() {
1918
- if (settings) return settings;
1919
- settingsIssues = [];
1920
- try {
1921
- const raw = fs.readFileSync(getSettingsPath(), "utf-8");
1922
- const parsed = JSON.parse(raw);
1923
- delete parsed.apiKey;
1924
- delete parsed.provider;
1925
- settings = {
1926
- ...defaults,
1927
- ...parsed,
1928
- mcpPort: sanitizePort(parsed.mcpPort ?? defaults.mcpPort),
1929
- agentTranscriptMode: parsed.agentTranscriptMode === "off" || parsed.agentTranscriptMode === "summary" || parsed.agentTranscriptMode === "full" ? parsed.agentTranscriptMode : parsed.showAgentTranscript === false ? "off" : defaults.agentTranscriptMode
1930
- };
1931
- } catch (error) {
1932
- if (fs.existsSync(getSettingsPath())) {
1933
- settingsIssues.push({
1934
- code: "settings-read-failed",
1935
- severity: "warning",
1936
- title: "Could not read Vessel settings",
1937
- detail: error instanceof Error ? error.message : "Unknown settings error.",
1938
- action: "Falling back to built-in defaults for this launch."
1939
- });
1940
- }
1941
- settings = { ...defaults };
1942
- }
1943
- return settings;
1944
- }
1945
- function saveSettings() {
1946
- fs.mkdirSync(path.dirname(getSettingsPath()), { recursive: true });
1947
- fs.writeFileSync(getSettingsPath(), JSON.stringify(settings, null, 2));
1948
- }
1949
- function setSetting(key, value) {
1950
- loadSettings();
1951
- if (key === "mcpPort") {
1952
- settings.mcpPort = sanitizePort(value);
1953
- } else {
1954
- settings[key] = value;
1955
- }
1956
- saveSettings();
1957
- return { ...settings };
1958
- }
1959
2268
  const Channels = {
1960
2269
  // Tab management
1961
2270
  TAB_CREATE: "tab:create",
@@ -2020,6 +2329,21 @@ const Channels = {
2020
2329
  // DevTools panel
2021
2330
  DEVTOOLS_PANEL_TOGGLE: "devtools-panel:toggle",
2022
2331
  DEVTOOLS_PANEL_STATE: "devtools-panel:state",
2332
+ DEVTOOLS_PANEL_RESIZE: "devtools-panel:resize",
2333
+ // Find in page
2334
+ FIND_IN_PAGE_START: "find:start",
2335
+ FIND_IN_PAGE_NEXT: "find:next",
2336
+ FIND_IN_PAGE_STOP: "find:stop",
2337
+ FIND_IN_PAGE_RESULT: "find:result",
2338
+ // Browsing history
2339
+ HISTORY_GET: "history:get",
2340
+ HISTORY_SEARCH: "history:search",
2341
+ HISTORY_CLEAR: "history:clear",
2342
+ HISTORY_UPDATE: "history:update",
2343
+ // Downloads
2344
+ DOWNLOAD_STARTED: "download:started",
2345
+ DOWNLOAD_PROGRESS: "download:progress",
2346
+ DOWNLOAD_DONE: "download:done",
2023
2347
  // Window controls
2024
2348
  WINDOW_MINIMIZE: "window:minimize",
2025
2349
  WINDOW_MAXIMIZE: "window:maximize",
@@ -3141,6 +3465,16 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3141
3465
  "dialog, [role='dialog'], [role='alertdialog'], [aria-modal='true']"
3142
3466
  ).forEach(function(el) { candidates.add(el); });
3143
3467
 
3468
+ // Known consent manager containers — these are often missed by generic
3469
+ // heuristics because they use custom stacking or non-standard z-indices
3470
+ document.body.querySelectorAll(
3471
+ '#onetrust-consent-sdk, #CybotCookiebotDialog, [class*="consent-banner"], ' +
3472
+ '[class*="cookie-banner"], [class*="privacy-banner"], [id*="consent-wall"], ' +
3473
+ '.fc-consent-root, #sp_message_container_, [id*="trustarc"], ' +
3474
+ '[class*="cmp-"], [id*="cmp-container"], [class*="gdpr"], ' +
3475
+ '[data-testid*="consent"], [data-testid*="cookie"], [data-testid*="privacy"]'
3476
+ ).forEach(function(el) { candidates.add(el); });
3477
+
3144
3478
  // Fixed/sticky elements are the other overlay category — walk only
3145
3479
  // direct children of body and high-level containers (depth ≤ 3)
3146
3480
  // since real overlays are almost always near the top of the DOM tree.
@@ -3168,13 +3502,23 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3168
3502
  var cartConfirm = !dialogLike && !drawerLike && isPositioned(style) &&
3169
3503
  rect.width >= 160 && rect.height >= 100 &&
3170
3504
  looksLikeCartConfirmation(node);
3505
+ // Body scroll-lock + large fixed element is a strong overlay signal
3506
+ // even without high z-index or exact center coverage
3507
+ var bodyLocked = (function() {
3508
+ var bs = window.getComputedStyle(document.body);
3509
+ var hs = window.getComputedStyle(document.documentElement);
3510
+ return bs.overflow === "hidden" || hs.overflow === "hidden";
3511
+ })();
3171
3512
  var blocksInteraction = dialogLike ||
3172
3513
  drawerLike ||
3173
3514
  cartConfirm ||
3174
3515
  ((style.position === "fixed" || style.position === "sticky") &&
3175
3516
  parseZIndex(style) >= 10 &&
3176
3517
  areaRatio >= 0.3 &&
3177
- coversViewportCenter(rect));
3518
+ coversViewportCenter(rect)) ||
3519
+ (bodyLocked &&
3520
+ (style.position === "fixed" || style.position === "sticky") &&
3521
+ areaRatio >= 0.2);
3178
3522
 
3179
3523
  if (!blocksInteraction && type !== "dialog" && type !== "modal") return;
3180
3524
 
@@ -4001,9 +4345,12 @@ function generateReaderHTML(page) {
4001
4345
  function escapeHtml(str) {
4002
4346
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4003
4347
  }
4004
- let mcpStatusChangeListener = null;
4348
+ const mcpStatusChangeListeners = /* @__PURE__ */ new Set();
4005
4349
  function onMcpStatusChange(listener) {
4006
- mcpStatusChangeListener = listener;
4350
+ mcpStatusChangeListeners.add(listener);
4351
+ return () => {
4352
+ mcpStatusChangeListeners.delete(listener);
4353
+ };
4007
4354
  }
4008
4355
  function getMcpStatus() {
4009
4356
  return state$1.mcp.status;
@@ -4054,7 +4401,9 @@ function setMcpHealth(update) {
4054
4401
  state$1.mcp.status = update.status;
4055
4402
  state$1.mcp.message = update.message;
4056
4403
  if (prevStatus !== state$1.mcp.status) {
4057
- mcpStatusChangeListener?.(state$1.mcp.status);
4404
+ for (const listener of mcpStatusChangeListeners) {
4405
+ listener(state$1.mcp.status);
4406
+ }
4058
4407
  }
4059
4408
  }
4060
4409
  const DEFAULT_MAX_ITERATIONS$1 = 200;
@@ -4571,6 +4920,158 @@ function createProvider(config) {
4571
4920
  }
4572
4921
  return new OpenAICompatProvider(normalized);
4573
4922
  }
4923
+ const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
4924
+ const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
4925
+ function elementLabel(el) {
4926
+ return el.text?.trim() || el.label?.trim() || el.value?.trim() || el.placeholder?.trim() || void 0;
4927
+ }
4928
+ function isOverlayAction(el) {
4929
+ if (el.type === "button" || el.type === "link") return true;
4930
+ if (el.type !== "input") return false;
4931
+ return ["button", "submit", "radio", "checkbox"].includes(
4932
+ (el.inputType || "").toLowerCase()
4933
+ );
4934
+ }
4935
+ function isRadioOption(el) {
4936
+ return el.role === "radio" || el.type === "input" && (el.inputType || "").toLowerCase() === "radio";
4937
+ }
4938
+ function normalizeAction(el) {
4939
+ return {
4940
+ index: el.index,
4941
+ label: elementLabel(el),
4942
+ selector: el.selector,
4943
+ role: el.role,
4944
+ labelSource: el.labelSource,
4945
+ looksCorrect: el.looksCorrect !== void 0 ? el.looksCorrect : looksLikeCorrectOption(elementLabel(el))
4946
+ };
4947
+ }
4948
+ function normalizeStoredAction(action) {
4949
+ return {
4950
+ label: action.label,
4951
+ selector: action.selector,
4952
+ role: action.kind === "radio" ? "radio" : void 0
4953
+ };
4954
+ }
4955
+ function normalizeStoredRadioOption(option) {
4956
+ return {
4957
+ label: option.label,
4958
+ selector: option.selector,
4959
+ role: "radio",
4960
+ labelSource: option.labelSource,
4961
+ looksCorrect: option.looksCorrect !== void 0 ? option.looksCorrect : looksLikeCorrectOption(option.label)
4962
+ };
4963
+ }
4964
+ function dedupeCandidates(actions) {
4965
+ const seen = /* @__PURE__ */ new Set();
4966
+ return actions.filter((action) => {
4967
+ const key = [
4968
+ action.selector || "",
4969
+ action.label || "",
4970
+ action.role || "",
4971
+ action.labelSource || ""
4972
+ ].join("::");
4973
+ if (seen.has(key)) return false;
4974
+ seen.add(key);
4975
+ return true;
4976
+ });
4977
+ }
4978
+ function classifyOverlayKind(overlay, radioOptions) {
4979
+ if (overlay.kind) {
4980
+ return overlay.kind;
4981
+ }
4982
+ const haystack = [overlay.label, overlay.text, overlay.role].filter(Boolean).join(" ").toLowerCase();
4983
+ if (/cookie|consent|privacy|gdpr|ccpa|onetrust|trustarc|cookiebot/.test(
4984
+ haystack
4985
+ )) {
4986
+ return "cookie_consent";
4987
+ }
4988
+ if (radioOptions.length > 0) return "selection_modal";
4989
+ if (overlay.role === "alertdialog" || /\b(alert|warning|error)\b/.test(haystack)) {
4990
+ return "alert";
4991
+ }
4992
+ if (overlay.type === "dialog") return "dialog";
4993
+ if (overlay.type === "modal") return "modal";
4994
+ return "overlay";
4995
+ }
4996
+ function findAction(actions, matcher) {
4997
+ return actions.find(
4998
+ (action) => matcher.test((action.label || "").toLowerCase())
4999
+ );
5000
+ }
5001
+ function looksLikeCorrectOption(label) {
5002
+ const text = label?.trim();
5003
+ if (!text) return void 0;
5004
+ if (CORRECT_HINT_RE.test(text)) return true;
5005
+ if (WRONG_HINT_RE.test(text)) return false;
5006
+ return void 0;
5007
+ }
5008
+ function getBlockingOverlaySignature(overlays) {
5009
+ return overlays.filter((overlay) => overlay.blocksInteraction).map(
5010
+ (overlay) => [
5011
+ overlay.kind,
5012
+ overlay.selector || "",
5013
+ overlay.label || "",
5014
+ overlay.text || "",
5015
+ overlay.actions.map((action) => `${action.selector || ""}:${action.label || ""}`).join("|"),
5016
+ overlay.radioOptions.map((option) => `${option.selector || ""}:${option.label || ""}`).join("|")
5017
+ ].join("::")
5018
+ ).join("||");
5019
+ }
5020
+ function buildOverlayInventory(page) {
5021
+ if (page.overlays.length === 0) return [];
5022
+ return page.overlays.map((overlay) => {
5023
+ const controls = dedupeCandidates([
5024
+ ...page.interactiveElements.filter((el) => {
5025
+ if (overlay.selector && el.parentOverlay === overlay.selector) {
5026
+ return true;
5027
+ }
5028
+ return page.overlays.length === 1 && el.context === "dialog";
5029
+ }).filter(isOverlayAction).map(normalizeAction),
5030
+ ...(overlay.actions || []).map(normalizeStoredAction)
5031
+ ]).filter((action) => action.label || action.selector);
5032
+ const radioOptions = dedupeCandidates([
5033
+ ...page.interactiveElements.filter((el) => {
5034
+ if (!isRadioOption(el)) return false;
5035
+ if (overlay.selector && el.parentOverlay === overlay.selector) {
5036
+ return true;
5037
+ }
5038
+ return page.overlays.length === 1 && el.context === "dialog";
5039
+ }).map(normalizeAction),
5040
+ ...(overlay.radioOptions || []).map(normalizeStoredRadioOption)
5041
+ ]).filter((action) => action.label || action.selector);
5042
+ const kind = classifyOverlayKind(overlay, radioOptions);
5043
+ const dismissAction = findAction(
5044
+ controls,
5045
+ /\b(close|dismiss|skip|cancel|reject|decline|no thanks|not now|maybe later|continue without)\b/
5046
+ );
5047
+ const acceptAction = findAction(
5048
+ controls,
5049
+ /\b(accept|allow|agree|got it|ok|okay|consent)\b/
5050
+ );
5051
+ const submitAction = findAction(
5052
+ controls,
5053
+ /\b(submit|continue|confirm|done|next|save|apply|finish)\b/
5054
+ );
5055
+ const correctOption = radioOptions.find(
5056
+ (option) => option.looksCorrect === true
5057
+ );
5058
+ return {
5059
+ type: overlay.type,
5060
+ kind,
5061
+ role: overlay.role,
5062
+ label: overlay.label,
5063
+ selector: overlay.selector,
5064
+ text: overlay.text,
5065
+ blocksInteraction: overlay.blocksInteraction,
5066
+ actions: controls,
5067
+ radioOptions,
5068
+ dismissAction,
5069
+ acceptAction,
5070
+ submitAction,
5071
+ correctOption
5072
+ };
5073
+ });
5074
+ }
4574
5075
  const MAX_CONTENT_LENGTH = 6e4;
4575
5076
  const MAX_STRUCTURED_ITEMS = 100;
4576
5077
  const LARGE_PAGE_HINT_THRESHOLD = 12e3;
@@ -4638,6 +5139,14 @@ function formatElementMeta(el) {
4638
5139
  if (el.pattern) {
4639
5140
  meta.push(`pattern="${el.pattern}"`);
4640
5141
  }
5142
+ if (el.labelSource) {
5143
+ meta.push(`source=${el.labelSource}`);
5144
+ }
5145
+ if (el.looksCorrect === true) {
5146
+ meta.push("likely-correct");
5147
+ } else if (el.looksCorrect === false) {
5148
+ meta.push("likely-wrong");
5149
+ }
4641
5150
  if (el.description) {
4642
5151
  meta.push(`desc="${el.description.slice(0, 80)}"`);
4643
5152
  }
@@ -4867,7 +5376,7 @@ function formatInteractiveElements(elements) {
4867
5376
  const parts = [prefix];
4868
5377
  if (el.type === "button") {
4869
5378
  parts.push(`[${el.text || "Button"}]`);
4870
- parts.push("button");
5379
+ parts.push(el.role === "radio" ? "radio" : "button");
4871
5380
  } else if (el.type === "link") {
4872
5381
  parts.push(`[${el.text || "Link"}]`);
4873
5382
  parts.push("link");
@@ -4932,7 +5441,7 @@ function formatForms(forms) {
4932
5441
  ];
4933
5442
  if (field.type === "button") {
4934
5443
  fieldParts.push(`[${field.text || "Submit"}]`);
4935
- fieldParts.push("button");
5444
+ fieldParts.push(field.role === "radio" ? "radio" : "button");
4936
5445
  } else if (field.type === "input") {
4937
5446
  fieldParts.push(`[${field.label || field.placeholder || "Input"}]`);
4938
5447
  fieldParts.push(field.inputType || "text");
@@ -4979,18 +5488,51 @@ function formatLandmarks(landmarks) {
4979
5488
  function formatViewport(page) {
4980
5489
  return `${page.viewport.width}x${page.viewport.height} at scroll (${page.viewport.scrollX}, ${page.viewport.scrollY})`;
4981
5490
  }
4982
- function formatOverlays(overlays) {
4983
- if (overlays.length === 0) return "None detected";
4984
- const items = limitItems(overlays, 10);
5491
+ function formatOverlays(page) {
5492
+ if (page.overlays.length === 0) return "None detected";
5493
+ const items = limitItems(buildOverlayInventory(page), 10);
4985
5494
  return items.map((overlay) => {
4986
- const parts = [`- ${overlay.type}`];
4987
- if (overlay.role) parts.push(`role=${overlay.role}`);
4988
- if (overlay.blocksInteraction) parts.push("blocking");
4989
- if (overlay.label) parts.push(`label="${overlay.label.slice(0, 80)}"`);
4990
- if (overlay.text) parts.push(`text="${overlay.text.slice(0, 100)}"`);
4991
- return parts.join(" ");
5495
+ const lines = [
5496
+ [
5497
+ `- ${overlay.kind}`,
5498
+ overlay.role ? `role=${overlay.role}` : "",
5499
+ overlay.blocksInteraction ? "blocking" : "",
5500
+ overlay.label ? `label="${overlay.label.slice(0, 80)}"` : "",
5501
+ overlay.text ? `text="${overlay.text.slice(0, 100)}"` : ""
5502
+ ].filter(Boolean).join(" ")
5503
+ ];
5504
+ if (overlay.radioOptions.length > 0) {
5505
+ const options = overlay.radioOptions.slice(0, 4).map((option) => {
5506
+ const tags = [];
5507
+ if (option.labelSource) tags.push(`source=${option.labelSource}`);
5508
+ if (option.looksCorrect === true) tags.push("likely-correct");
5509
+ if (option.looksCorrect === false) tags.push("likely-wrong");
5510
+ const suffix = tags.length > 0 ? ` (${tags.join(", ")})` : "";
5511
+ return `${option.label || option.selector || "radio"}${suffix}`;
5512
+ }).join(" | ");
5513
+ lines.push(` options: ${options}`);
5514
+ }
5515
+ const actionLabels = [
5516
+ overlay.dismissAction?.label ? `dismiss="${overlay.dismissAction.label}"` : "",
5517
+ overlay.acceptAction?.label ? `accept="${overlay.acceptAction.label}"` : "",
5518
+ overlay.submitAction?.label ? `submit="${overlay.submitAction.label}"` : ""
5519
+ ].filter(Boolean);
5520
+ if (actionLabels.length > 0) {
5521
+ lines.push(` actions: ${actionLabels.join(" ")}`);
5522
+ }
5523
+ return lines.join("\n");
4992
5524
  }).join("\n");
4993
5525
  }
5526
+ function getScrollHints(page) {
5527
+ const candidates = page.interactiveElements.filter(
5528
+ (el) => el.visible !== false && el.inViewport === false && el.context !== "nav" && el.context !== "footer" && el.context !== "sidebar" && el.blockedByOverlay !== true && (el.type === "input" || el.type === "textarea" || el.type === "select" || el.type === "button")
5529
+ );
5530
+ if (candidates.length === 0) return [];
5531
+ const labels = limitItems(candidates, 3).map((el) => el.text || el.label || el.placeholder || el.type).filter(Boolean);
5532
+ return [
5533
+ `Scroll to reveal offscreen controls: ${labels.join(", ")}${candidates.length > labels.length ? ", ..." : ""}`
5534
+ ];
5535
+ }
4994
5536
  function formatDormantOverlays(overlays) {
4995
5537
  if (overlays.length === 0) return "None detected";
4996
5538
  const items = limitItems(overlays, 10);
@@ -5327,6 +5869,10 @@ function buildScopedContext(page, mode) {
5327
5869
  if (page.excerpt) sections.push(`**Summary:** ${page.excerpt}`);
5328
5870
  const largePageHint = formatLargePageHint(page);
5329
5871
  if (largePageHint) sections.push(`**Reading Hint:** ${largePageHint}`);
5872
+ const scrollHints = getScrollHints(page);
5873
+ if (scrollHints.length > 0) {
5874
+ sections.push(`**Scroll Hint:** ${scrollHints[0]}`);
5875
+ }
5330
5876
  sections.push("");
5331
5877
  const summaryIntent = analyzePageIntent(page);
5332
5878
  if (summaryIntent) {
@@ -5395,6 +5941,10 @@ function buildScopedContext(page, mode) {
5395
5941
  sections.push(`**URL:** ${page.url}`);
5396
5942
  sections.push(`**Title:** ${page.title}`);
5397
5943
  sections.push(`**Viewport:** ${formatViewport(page)}`);
5944
+ const interactivesScrollHints = getScrollHints(page);
5945
+ if (interactivesScrollHints.length > 0) {
5946
+ sections.push(`**Scroll Hint:** ${interactivesScrollHints[0]}`);
5947
+ }
5398
5948
  sections.push("");
5399
5949
  const interactivesIntent = analyzePageIntent(page);
5400
5950
  if (interactivesIntent) {
@@ -5419,7 +5969,7 @@ function buildScopedContext(page, mode) {
5419
5969
  }
5420
5970
  if (page.overlays.length > 0) {
5421
5971
  sections.push("### Active Overlays");
5422
- sections.push(formatOverlays(page.overlays));
5972
+ sections.push(formatOverlays(page));
5423
5973
  sections.push("");
5424
5974
  }
5425
5975
  if (dialogFocus) {
@@ -5457,6 +6007,10 @@ function buildScopedContext(page, mode) {
5457
6007
  sections.push(`**URL:** ${page.url}`);
5458
6008
  sections.push(`**Title:** ${page.title}`);
5459
6009
  sections.push(`**Viewport:** ${formatViewport(page)}`);
6010
+ const visibleScrollHints = getScrollHints(page);
6011
+ if (visibleScrollHints.length > 0) {
6012
+ sections.push(`**Scroll Hint:** ${visibleScrollHints[0]}`);
6013
+ }
5460
6014
  sections.push("");
5461
6015
  const formsHighlights = getHighlightsForPage(page.url);
5462
6016
  if (formsHighlights.length > 0) {
@@ -5476,7 +6030,7 @@ function buildScopedContext(page, mode) {
5476
6030
  }
5477
6031
  if (page.overlays.length > 0) {
5478
6032
  sections.push("### Active Overlays");
5479
- sections.push(formatOverlays(page.overlays));
6033
+ sections.push(formatOverlays(page));
5480
6034
  sections.push("");
5481
6035
  }
5482
6036
  if (page.dormantOverlays.length > 0) {
@@ -5559,7 +6113,7 @@ function buildScopedContext(page, mode) {
5559
6113
  }
5560
6114
  if (page.overlays.length > 0) {
5561
6115
  sections.push("### Active Overlays");
5562
- sections.push(formatOverlays(page.overlays));
6116
+ sections.push(formatOverlays(page));
5563
6117
  sections.push("");
5564
6118
  }
5565
6119
  if (dialogFocus) {
@@ -5744,6 +6298,10 @@ function buildStructuredContext(page) {
5744
6298
  sections.push(`**Viewport:** ${formatViewport(page)}`);
5745
6299
  if (page.byline) sections.push(`**Author:** ${page.byline}`);
5746
6300
  if (page.excerpt) sections.push(`**Summary:** ${page.excerpt}`);
6301
+ const structuredScrollHints = getScrollHints(page);
6302
+ if (structuredScrollHints.length > 0) {
6303
+ sections.push(`**Scroll Hint:** ${structuredScrollHints[0]}`);
6304
+ }
5747
6305
  sections.push("");
5748
6306
  const pageIntent = analyzePageIntent(page);
5749
6307
  if (pageIntent) {
@@ -5782,7 +6340,7 @@ function buildStructuredContext(page) {
5782
6340
  sections.push(formatLandmarks(page.landmarks));
5783
6341
  sections.push("");
5784
6342
  sections.push("### Active Overlays / Modals");
5785
- sections.push(formatOverlays(page.overlays));
6343
+ sections.push(formatOverlays(page));
5786
6344
  sections.push("");
5787
6345
  sections.push("### Dormant Consent / Modal UI");
5788
6346
  sections.push(formatDormantOverlays(page.dormantOverlays));
@@ -5861,6 +6419,83 @@ function buildGeneralPrompt(query) {
5861
6419
  user: query
5862
6420
  };
5863
6421
  }
6422
+ const WRAPPING_QUOTES = /* @__PURE__ */ new Set(['"', "'", "`"]);
6423
+ function stripWrappingQuotes(value) {
6424
+ const trimmed = value.trim();
6425
+ if (trimmed.length < 2) return trimmed;
6426
+ const first = trimmed[0];
6427
+ const last = trimmed[trimmed.length - 1];
6428
+ if (first === last && WRAPPING_QUOTES.has(first)) {
6429
+ return trimmed.slice(1, -1).trim();
6430
+ }
6431
+ return trimmed;
6432
+ }
6433
+ function normalizeArrayItem(value) {
6434
+ if (typeof value === "string") {
6435
+ return stripWrappingQuotes(value).trim();
6436
+ }
6437
+ return String(value).trim();
6438
+ }
6439
+ function normalizeLooseString(value) {
6440
+ if (typeof value !== "string") return void 0;
6441
+ const normalized = stripWrappingQuotes(value);
6442
+ return normalized ? normalized : void 0;
6443
+ }
6444
+ function coerceOptionalNumber(value) {
6445
+ if (typeof value === "number" && Number.isFinite(value)) {
6446
+ return value;
6447
+ }
6448
+ const normalized = normalizeLooseString(value);
6449
+ if (!normalized) return void 0;
6450
+ const parsed = Number(normalized);
6451
+ return Number.isFinite(parsed) ? parsed : void 0;
6452
+ }
6453
+ function coerceStringArray(value) {
6454
+ if (Array.isArray(value)) {
6455
+ return value.map(normalizeArrayItem).filter(Boolean);
6456
+ }
6457
+ const normalized = normalizeLooseString(value);
6458
+ if (!normalized) return [];
6459
+ try {
6460
+ const parsed = JSON.parse(normalized);
6461
+ if (Array.isArray(parsed)) {
6462
+ return parsed.map(normalizeArrayItem).filter(Boolean);
6463
+ }
6464
+ } catch {
6465
+ }
6466
+ const lines = normalized.split(/\r?\n/).map((line) => line.replace(/^\s*[-*]\s+/, "").trim()).filter(Boolean);
6467
+ if (lines.length > 1) {
6468
+ return lines;
6469
+ }
6470
+ return [normalized];
6471
+ }
6472
+ function optionalNumberLikeSchema() {
6473
+ return zod.z.preprocess(
6474
+ (value) => {
6475
+ if (value == null) return void 0;
6476
+ return coerceOptionalNumber(value) ?? value;
6477
+ },
6478
+ zod.z.number().finite().optional()
6479
+ );
6480
+ }
6481
+ function normalizedOptionalStringSchema() {
6482
+ return zod.z.preprocess(
6483
+ (value) => {
6484
+ if (value == null) return void 0;
6485
+ return normalizeLooseString(value) ?? value;
6486
+ },
6487
+ zod.z.string().optional()
6488
+ );
6489
+ }
6490
+ function stringArrayLikeSchema() {
6491
+ return zod.z.preprocess(
6492
+ (value) => {
6493
+ if (value == null) return value;
6494
+ return coerceStringArray(value) ?? value;
6495
+ },
6496
+ zod.z.array(zod.z.string().min(1)).min(1)
6497
+ );
6498
+ }
5864
6499
  const TOOL_DEFINITIONS = [
5865
6500
  // --- Tab Management ---
5866
6501
  {
@@ -5989,7 +6624,9 @@ const TOOL_DEFINITIONS = [
5989
6624
  description: "Scroll the page up or down.",
5990
6625
  inputSchema: {
5991
6626
  direction: zod.z.enum(["up", "down"]).describe("Scroll direction"),
5992
- amount: zod.z.number().optional().describe("Pixels to scroll (default 500)")
6627
+ amount: optionalNumberLikeSchema().describe(
6628
+ "Pixels to scroll (default 500)"
6629
+ )
5993
6630
  },
5994
6631
  tier: 0,
5995
6632
  relevance: ["ARTICLE", "SEARCH_RESULTS", "PAGINATED_LIST"]
@@ -6034,6 +6671,17 @@ const TOOL_DEFINITIONS = [
6034
6671
  description: "Dismiss a modal, popup, newsletter gate, cookie banner, or overlay using common close/decline actions.",
6035
6672
  tier: 1
6036
6673
  },
6674
+ {
6675
+ name: "clear_overlays",
6676
+ title: "Clear Overlays",
6677
+ description: "Work through blocking overlays and modals until the page is unblocked, using overlay-specific heuristics for consent banners and radio-selection dialogs.",
6678
+ inputSchema: {
6679
+ strategy: zod.z.enum(["auto", "interactive"]).optional().describe(
6680
+ 'How aggressively to clear overlays. "auto" uses heuristics; "interactive" stops earlier when human judgment may be needed.'
6681
+ )
6682
+ },
6683
+ tier: 1
6684
+ },
6037
6685
  {
6038
6686
  name: "inspect_element",
6039
6687
  title: "Inspect Element",
@@ -6052,6 +6700,7 @@ const TOOL_DEFINITIONS = [
6052
6700
  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.",
6053
6701
  inputSchema: {
6054
6702
  mode: zod.z.enum([
6703
+ "glance",
6055
6704
  "summary",
6056
6705
  "interactives_only",
6057
6706
  "forms_only",
@@ -6061,7 +6710,7 @@ const TOOL_DEFINITIONS = [
6061
6710
  "full",
6062
6711
  "debug"
6063
6712
  ]).optional().describe(
6064
- "Read mode: visible_only/results_only/forms_only/summary/text_only for narrow reads, full/debug for the complete page dump"
6713
+ "Read mode: glance (fastest — viewport snapshot, no JS extraction, ideal for heavy pages), visible_only/results_only/forms_only/summary/text_only for narrow reads, full/debug for the complete page dump"
6065
6714
  )
6066
6715
  },
6067
6716
  tier: 0
@@ -6233,7 +6882,9 @@ const TOOL_DEFINITIONS = [
6233
6882
  inputSchema: {
6234
6883
  index: zod.z.number().optional().describe("Element index from page content to highlight"),
6235
6884
  selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
6236
- text: zod.z.string().optional().describe("Text to find and highlight on the page (all occurrences)"),
6885
+ text: normalizedOptionalStringSchema().describe(
6886
+ "Text to find and highlight on the page (all occurrences)"
6887
+ ),
6237
6888
  label: zod.z.string().optional().describe("Annotation label to display near the highlight"),
6238
6889
  durationMs: zod.z.number().optional().describe(
6239
6890
  "Auto-clear after this many milliseconds (omit for permanent)"
@@ -6258,7 +6909,7 @@ const TOOL_DEFINITIONS = [
6258
6909
  goal: zod.z.string().describe(
6259
6910
  "What this workflow accomplishes (e.g. 'Purchase item from Amazon')"
6260
6911
  ),
6261
- steps: zod.z.array(zod.z.string()).describe(
6912
+ steps: stringArrayLikeSchema().describe(
6262
6913
  "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
6263
6914
  )
6264
6915
  },
@@ -6495,6 +7146,7 @@ const ALWAYS_FAST_TOOL_NAMES = /* @__PURE__ */ new Set([
6495
7146
  "search",
6496
7147
  "scroll",
6497
7148
  "dismiss_popup",
7149
+ "clear_overlays",
6498
7150
  "accept_cookies",
6499
7151
  "wait_for",
6500
7152
  "read_page",
@@ -6518,6 +7170,9 @@ function inferIntent(query) {
6518
7170
  }
6519
7171
  if (/\b(highlight|mark|annotate)\b/.test(lowered)) intents.add("highlight");
6520
7172
  if (/\b(table|csv|rows|columns)\b/.test(lowered)) intents.add("table");
7173
+ if (/\b(overlay|modal|popup|consent|cookie|blocking ui)\b/.test(lowered)) {
7174
+ intents.add("debug");
7175
+ }
6521
7176
  if (/\b(debug|diagnose|what should i do|stuck|inspect)\b/.test(lowered)) {
6522
7177
  intents.add("debug");
6523
7178
  }
@@ -6759,8 +7414,12 @@ function load() {
6759
7414
  return state;
6760
7415
  }
6761
7416
  function save() {
6762
- fs.mkdirSync(path.dirname(getBookmarksPath()), { recursive: true });
6763
- fs.writeFileSync(getBookmarksPath(), JSON.stringify(state, null, 2), "utf-8");
7417
+ try {
7418
+ fs.mkdirSync(path.dirname(getBookmarksPath()), { recursive: true });
7419
+ fs.writeFileSync(getBookmarksPath(), JSON.stringify(state, null, 2), "utf-8");
7420
+ } catch (err) {
7421
+ console.error("[Vessel] Failed to save bookmarks:", err);
7422
+ }
6764
7423
  }
6765
7424
  function emit() {
6766
7425
  if (!state) return;
@@ -7186,6 +7845,22 @@ function formatDeadLinkMessage(label, result) {
7186
7845
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
7187
7846
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
7188
7847
  }
7848
+ const ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
7849
+ function isSafeNavigationURL(url) {
7850
+ try {
7851
+ const parsed = new URL(url);
7852
+ return ALLOWED_SCHEMES.has(parsed.protocol);
7853
+ } catch {
7854
+ return false;
7855
+ }
7856
+ }
7857
+ function assertSafeURL(url) {
7858
+ if (!isSafeNavigationURL(url)) {
7859
+ throw new Error(
7860
+ `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`
7861
+ );
7862
+ }
7863
+ }
7189
7864
  const SESSION_VERSION = 1;
7190
7865
  function getSessionsDir() {
7191
7866
  return path$1.join(electron.app.getPath("userData"), "named-sessions");
@@ -7205,7 +7880,7 @@ function normalizeSessionName(name) {
7205
7880
  function sessionFileName(name) {
7206
7881
  const normalized = normalizeSessionName(name).toLowerCase();
7207
7882
  const slug = normalized.replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "session";
7208
- const hash = node_crypto.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
7883
+ const hash = crypto$1.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
7209
7884
  return `${slug}-${hash}.json`;
7210
7885
  }
7211
7886
  function getSessionPath(name) {
@@ -7470,10 +8145,148 @@ const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
7470
8145
  function pageBusyError(action) {
7471
8146
  return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
7472
8147
  }
8148
+ async function glanceExtract(wc) {
8149
+ const startMs = Date.now();
8150
+ const result = await executePageScript(
8151
+ wc,
8152
+ `(function() {
8153
+ var vw = window.innerWidth || document.documentElement.clientWidth || 0;
8154
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
8155
+ var sy = window.scrollY || window.pageYOffset || 0;
8156
+
8157
+ function inViewport(el) {
8158
+ var r = el.getBoundingClientRect();
8159
+ return r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw && r.width > 0 && r.height > 0;
8160
+ }
8161
+
8162
+ function label(el) {
8163
+ return (el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 120);
8164
+ }
8165
+
8166
+ // Headings visible on screen
8167
+ var headings = [];
8168
+ document.querySelectorAll('h1, h2, h3, h4').forEach(function(h) {
8169
+ if (!inViewport(h)) return;
8170
+ var t = (h.textContent || '').trim();
8171
+ if (t && t.length < 200) headings.push(h.tagName.toLowerCase() + ': ' + t);
8172
+ });
8173
+
8174
+ // Links visible on screen (deduplicated by text)
8175
+ var links = [];
8176
+ var seenLinks = {};
8177
+ var idx = 1;
8178
+ document.querySelectorAll('a[href]').forEach(function(a) {
8179
+ if (!inViewport(a)) return;
8180
+ var t = (a.textContent || '').trim().slice(0, 100);
8181
+ if (!t || t.length < 2 || seenLinks[t]) return;
8182
+ seenLinks[t] = true;
8183
+ links.push({ text: t, href: (a.href || '').slice(0, 200), index: idx++ });
8184
+ });
8185
+
8186
+ // Buttons visible on screen
8187
+ var buttons = [];
8188
+ document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]').forEach(function(b) {
8189
+ if (!inViewport(b)) return;
8190
+ var t = label(b);
8191
+ if (!t || t.length < 1) return;
8192
+ buttons.push({ text: t, index: idx++ });
8193
+ });
8194
+
8195
+ // Input fields visible on screen
8196
+ var inputs = [];
8197
+ document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), select, textarea').forEach(function(inp) {
8198
+ if (!inViewport(inp)) return;
8199
+ var type = (inp.type || inp.tagName.toLowerCase() || '').toLowerCase();
8200
+ var lbl = (inp.getAttribute('aria-label') || inp.getAttribute('placeholder') || inp.name || '').trim();
8201
+ inputs.push({ type: type, label: lbl.slice(0, 80), placeholder: (inp.getAttribute('placeholder') || '').slice(0, 80), index: idx++ });
8202
+ });
8203
+
8204
+ // Content snapshot from main content area using textContent (instant, no reflow)
8205
+ var roots = ['main', 'article', '[role="main"]', '#content', '.content', '.story-body'];
8206
+ var contentRoot = null;
8207
+ for (var i = 0; i < roots.length; i++) {
8208
+ contentRoot = document.querySelector(roots[i]);
8209
+ if (contentRoot && contentRoot.textContent.trim().length > 50) break;
8210
+ contentRoot = null;
8211
+ }
8212
+ var snippet = '';
8213
+ if (contentRoot) {
8214
+ snippet = contentRoot.textContent.replace(/[ \\t]+/g, ' ').replace(/(\\n\\s*){3,}/g, '\\n\\n').trim().slice(0, 8000);
8215
+ } else {
8216
+ // Fallback: grab text from visible elements only
8217
+ var parts = [];
8218
+ document.querySelectorAll('h1, h2, h3, p, li, td, span, div').forEach(function(el) {
8219
+ if (parts.length > 100 || !inViewport(el)) return;
8220
+ var t = (el.textContent || '').trim();
8221
+ if (t.length > 10 && t.length < 500) parts.push(t);
8222
+ });
8223
+ snippet = parts.join('\\n').slice(0, 8000);
8224
+ }
8225
+
8226
+ return {
8227
+ title: document.title || '',
8228
+ url: location.href,
8229
+ headings: headings.slice(0, 20),
8230
+ links: links.slice(0, 40),
8231
+ buttons: buttons.slice(0, 20),
8232
+ inputs: inputs.slice(0, 15),
8233
+ contentSnippet: snippet,
8234
+ viewportHeight: vh,
8235
+ viewportWidth: vw,
8236
+ scrollY: Math.round(sy),
8237
+ };
8238
+ })()`,
8239
+ { timeoutMs: 2500, label: "glance-extract" }
8240
+ );
8241
+ const elapsed = Date.now() - startMs;
8242
+ if (!result || result === PAGE_SCRIPT_TIMEOUT) {
8243
+ return [
8244
+ `# ${wc.getTitle() || "(untitled)"}`,
8245
+ `URL: ${wc.getURL()}`,
8246
+ "",
8247
+ "[read_page mode=glance — page JS thread is completely blocked, no content available]",
8248
+ "[Try: click or type_text to interact directly, or wait a few seconds and retry]"
8249
+ ].join("\n");
8250
+ }
8251
+ const sections = [
8252
+ `# ${result.title}`,
8253
+ `URL: ${result.url}`,
8254
+ `Viewport: ${result.viewportWidth}×${result.viewportHeight} scrollY=${result.scrollY}`,
8255
+ `[read_page mode=glance — ${elapsed}ms, showing what's visible on screen]`
8256
+ ];
8257
+ if (result.headings.length > 0) {
8258
+ sections.push("", "## Headings", ...result.headings);
8259
+ }
8260
+ if (result.inputs.length > 0) {
8261
+ sections.push("", "## Input Fields");
8262
+ for (const inp of result.inputs) {
8263
+ const desc = inp.label || inp.placeholder || inp.type;
8264
+ sections.push(` [#${inp.index}] ${inp.type}: ${desc}`);
8265
+ }
8266
+ }
8267
+ if (result.buttons.length > 0) {
8268
+ sections.push("", "## Buttons");
8269
+ for (const btn of result.buttons) {
8270
+ sections.push(` [#${btn.index}] ${btn.text}`);
8271
+ }
8272
+ }
8273
+ if (result.links.length > 0) {
8274
+ sections.push("", "## Visible Links");
8275
+ for (const link of result.links) {
8276
+ sections.push(` [#${link.index}] ${link.text}`);
8277
+ }
8278
+ }
8279
+ if (result.contentSnippet) {
8280
+ const truncated = result.contentSnippet.length > 6e3 ? result.contentSnippet.slice(0, 6e3) + "\n[truncated]" : result.contentSnippet;
8281
+ sections.push("", "## Page Content (viewport)", "", truncated);
8282
+ }
8283
+ return sections.join("\n");
8284
+ }
7473
8285
  function normalizeReadPageMode(mode, pageContent) {
7474
8286
  if (typeof mode === "string") {
7475
8287
  const normalized = mode.trim().toLowerCase();
7476
8288
  if (normalized === "debug") return "debug";
8289
+ if (normalized === "glance") return "glance";
7477
8290
  if (normalized === "full" || normalized === "summary" || normalized === "interactives_only" || normalized === "forms_only" || normalized === "text_only" || normalized === "visible_only" || normalized === "results_only") {
7478
8291
  return normalized;
7479
8292
  }
@@ -7601,10 +8414,62 @@ function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
7601
8414
  wc.on("page-title-updated", onNativeChange);
7602
8415
  });
7603
8416
  }
7604
- function getPostNavSummary(wc) {
8417
+ async function getPostNavSummary(wc) {
7605
8418
  const title = wc.getTitle();
7606
- return title ? `
8419
+ const titleLine = title ? `
7607
8420
  Page title: ${title}` : "";
8421
+ const overlaySignal = await executePageScript(
8422
+ wc,
8423
+ `(function() {
8424
+ var signals = [];
8425
+ // Body scroll lock is a strong overlay signal
8426
+ var bodyStyle = window.getComputedStyle(document.body);
8427
+ var htmlStyle = window.getComputedStyle(document.documentElement);
8428
+ if (bodyStyle.overflow === 'hidden' || htmlStyle.overflow === 'hidden') {
8429
+ signals.push('body-scroll-locked');
8430
+ }
8431
+ // Check for known consent manager containers
8432
+ var consentSelectors = [
8433
+ '#onetrust-consent-sdk', '#CybotCookiebotDialog', '[class*="consent-banner"]',
8434
+ '[class*="cookie-banner"]', '[class*="privacy-banner"]', '[id*="consent"]',
8435
+ '[class*="gdpr"]', '[data-testid*="consent"]', '[data-testid*="cookie"]',
8436
+ '.fc-consent-root', '#sp_message_container_', '[id*="trustarc"]',
8437
+ '[class*="cmp-"]', '[id*="cmp-"]'
8438
+ ];
8439
+ for (var i = 0; i < consentSelectors.length; i++) {
8440
+ try {
8441
+ var el = document.querySelector(consentSelectors[i]);
8442
+ if (el && el.offsetHeight > 50) {
8443
+ signals.push('consent-banner:' + consentSelectors[i]);
8444
+ break;
8445
+ }
8446
+ } catch(e) {}
8447
+ }
8448
+ // Check for large fixed/sticky elements covering viewport
8449
+ var vw = window.innerWidth || 0;
8450
+ var vh = window.innerHeight || 0;
8451
+ var vpArea = Math.max(1, vw * vh);
8452
+ var els = document.querySelectorAll('dialog[open], [role="dialog"], [aria-modal="true"]');
8453
+ if (els.length > 0) signals.push('dialog-open');
8454
+ if (signals.length === 0) {
8455
+ var fixed = document.querySelectorAll('div[style*="position: fixed"], div[style*="position:fixed"]');
8456
+ for (var j = 0; j < fixed.length && j < 20; j++) {
8457
+ var r = fixed[j].getBoundingClientRect();
8458
+ if ((r.width * r.height) / vpArea > 0.3) {
8459
+ signals.push('large-fixed-overlay');
8460
+ break;
8461
+ }
8462
+ }
8463
+ }
8464
+ return signals.length > 0 ? signals.join(', ') : null;
8465
+ })()`,
8466
+ { timeoutMs: 1500, label: "overlay-probe" }
8467
+ );
8468
+ if (overlaySignal && overlaySignal !== PAGE_SCRIPT_TIMEOUT) {
8469
+ return `${titleLine}
8470
+ WARNING: Blocking overlay detected (${overlaySignal}). Call clear_overlays or accept_cookies before reading the page.`;
8471
+ }
8472
+ return titleLine;
7608
8473
  }
7609
8474
  async function scrollPage$1(wc, deltaY) {
7610
8475
  const getScrollY = async () => {
@@ -8053,6 +8918,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
8053
8918
  }
8054
8919
  if (snapshot.url && snapshot.url !== wc.getURL()) {
8055
8920
  try {
8921
+ assertSafeURL(snapshot.url);
8056
8922
  await wc.loadURL(snapshot.url);
8057
8923
  await waitForLoad$1(wc, 3e3);
8058
8924
  return;
@@ -8683,6 +9549,166 @@ async function dismissPopup$1(wc) {
8683
9549
  }
8684
9550
  return initialBlocking > 0 ? "Could not dismiss the blocking popup automatically" : initialDormant > 0 ? `No active blocking popup detected. Found ${initialDormant} dormant consent/modal surface(s) in the DOM, likely geo-gated or inactive in this session.` : "No blocking popup detected";
8685
9551
  }
9552
+ function describeOverlayState(page) {
9553
+ const inventory = buildOverlayInventory(page);
9554
+ return {
9555
+ inventory,
9556
+ blocking: inventory.filter((overlay) => overlay.blocksInteraction).length,
9557
+ total: inventory.length,
9558
+ signature: getBlockingOverlaySignature(inventory)
9559
+ };
9560
+ }
9561
+ async function clickOverlayCandidate(wc, action) {
9562
+ if (!action?.selector) return null;
9563
+ const result = await clickResolvedSelector$1(wc, action.selector);
9564
+ return `${action.label || action.selector}: ${result}`;
9565
+ }
9566
+ async function tryDismissConsentIframe(wc) {
9567
+ try {
9568
+ const hasSignal = await executePageScript(
9569
+ wc,
9570
+ `(function() {
9571
+ var bs = window.getComputedStyle(document.body);
9572
+ var hs = window.getComputedStyle(document.documentElement);
9573
+ if (bs.overflow === 'hidden' || hs.overflow === 'hidden') return true;
9574
+ var sels = '#onetrust-consent-sdk, [class*="consent"], [class*="cookie-banner"], [id*="consent"], [id*="sp_message"], .fc-consent-root, [class*="cmp-"]';
9575
+ var el = document.querySelector(sels);
9576
+ return !!(el && el.offsetHeight > 20);
9577
+ })()`,
9578
+ { timeoutMs: 1e3, label: "iframe-consent-signal" }
9579
+ );
9580
+ if (!hasSignal || hasSignal === PAGE_SCRIPT_TIMEOUT) return null;
9581
+ const frames = wc.mainFrame.framesInSubtree;
9582
+ for (const frame of frames) {
9583
+ if (frame === wc.mainFrame) continue;
9584
+ try {
9585
+ const result = await frame.executeJavaScript(`
9586
+ (function() {
9587
+ var selectors = [
9588
+ 'button[title*="Accept"], button[title*="Agree"], button[title*="OK"]',
9589
+ '[class*="accept"], [class*="agree"], [class*="consent-accept"]',
9590
+ 'button[aria-label*="accept" i], button[aria-label*="agree" i]',
9591
+ '.sp_choice_type_11', '.message-component.message-button',
9592
+ ];
9593
+ // Try selectors first
9594
+ for (var i = 0; i < selectors.length; i++) {
9595
+ try {
9596
+ var els = document.querySelectorAll(selectors[i]);
9597
+ for (var j = 0; j < els.length; j++) {
9598
+ var el = els[j];
9599
+ if (!(el instanceof HTMLElement)) continue;
9600
+ var text = (el.textContent || '').trim().toLowerCase();
9601
+ if (/accept|agree|consent|got it|ok|continue|i understand/i.test(text) || el.offsetHeight > 0) {
9602
+ el.click();
9603
+ return 'Clicked iframe consent button: ' + text.slice(0, 60);
9604
+ }
9605
+ }
9606
+ } catch(e) {}
9607
+ }
9608
+ // Text-match fallback on all buttons
9609
+ var buttons = document.querySelectorAll('button, [role="button"], a.message-component');
9610
+ for (var k = 0; k < buttons.length; k++) {
9611
+ var btn = buttons[k];
9612
+ var label = (btn.textContent || '').trim().toLowerCase();
9613
+ if (/^(accept|agree|accept all|i agree|i accept|ok|got it|allow|continue|yes)$/i.test(label) ||
9614
+ /accept all|agree and|accept & continue|accept and continue/i.test(label)) {
9615
+ btn.click();
9616
+ return 'Clicked iframe consent button: ' + label.slice(0, 60);
9617
+ }
9618
+ }
9619
+ return null;
9620
+ })()
9621
+ `);
9622
+ if (result) return result;
9623
+ } catch {
9624
+ continue;
9625
+ }
9626
+ }
9627
+ } catch {
9628
+ }
9629
+ return null;
9630
+ }
9631
+ async function clearOverlays(wc, strategy = "auto") {
9632
+ const steps = [];
9633
+ let cleared = 0;
9634
+ const maxIterations = 8;
9635
+ for (let iteration = 0; iteration < maxIterations; iteration += 1) {
9636
+ const before = await extractContent(wc);
9637
+ const beforeState = describeOverlayState(before);
9638
+ const blockingOverlays = beforeState.inventory.filter(
9639
+ (overlay2) => overlay2.blocksInteraction
9640
+ );
9641
+ if (blockingOverlays.length === 0) {
9642
+ if (cleared === 0) {
9643
+ const iframeResult = await tryDismissConsentIframe(wc);
9644
+ if (iframeResult) {
9645
+ steps.push(`Iframe consent: ${iframeResult}`);
9646
+ await sleep$1(500);
9647
+ return steps.join("\n");
9648
+ }
9649
+ return "No blocking overlays detected";
9650
+ }
9651
+ steps.push(`Overlays remaining: ${beforeState.total}`);
9652
+ steps.push("Page still blocked: false");
9653
+ return steps.join("\n");
9654
+ }
9655
+ const overlay = blockingOverlays[0];
9656
+ let actionMessage = null;
9657
+ if (overlay.kind === "cookie_consent") {
9658
+ actionMessage = await clickOverlayCandidate(
9659
+ wc,
9660
+ overlay.acceptAction || overlay.dismissAction || overlay.actions[0]
9661
+ );
9662
+ } else if (overlay.kind === "selection_modal") {
9663
+ if (!overlay.correctOption?.selector) {
9664
+ if (strategy === "interactive") {
9665
+ steps.push(
9666
+ "Stopped: selection modal needs human judgment because no likely-correct option was detected."
9667
+ );
9668
+ steps.push(`Overlays remaining: ${beforeState.total}`);
9669
+ steps.push("Page still blocked: true");
9670
+ return steps.join("\n");
9671
+ }
9672
+ } else {
9673
+ const optionResult = await clickOverlayCandidate(
9674
+ wc,
9675
+ overlay.correctOption
9676
+ );
9677
+ if (optionResult) {
9678
+ actionMessage = `Selected likely-correct option: ${optionResult}`;
9679
+ await sleep$1(120);
9680
+ const submitResult = await clickOverlayCandidate(
9681
+ wc,
9682
+ overlay.submitAction || overlay.acceptAction
9683
+ );
9684
+ if (submitResult) {
9685
+ actionMessage += `
9686
+ Submitted modal: ${submitResult}`;
9687
+ }
9688
+ }
9689
+ }
9690
+ }
9691
+ if (!actionMessage) {
9692
+ actionMessage = `Fallback popup handling: ${await dismissPopup$1(wc)}`;
9693
+ }
9694
+ steps.push(actionMessage);
9695
+ await sleep$1(250);
9696
+ const after = await extractContent(wc);
9697
+ const afterState = describeOverlayState(after);
9698
+ steps.push(`Overlays remaining: ${afterState.total}`);
9699
+ steps.push(`Page still blocked: ${afterState.blocking > 0}`);
9700
+ if (afterState.blocking === 0) {
9701
+ return steps.join("\n");
9702
+ }
9703
+ const progressMade = afterState.blocking < beforeState.blocking || afterState.total !== beforeState.total || afterState.signature !== beforeState.signature;
9704
+ if (progressMade) {
9705
+ cleared += 1;
9706
+ continue;
9707
+ }
9708
+ return steps.join("\n");
9709
+ }
9710
+ return steps.join("\n");
9711
+ }
8686
9712
  async function resolveSelector$1(wc, index, selector) {
8687
9713
  if (selector) return selector;
8688
9714
  if (index == null) return null;
@@ -9473,6 +10499,7 @@ async function submitForm$1(wc, args) {
9473
10499
  if (formInfo.params) {
9474
10500
  url.search = formInfo.params;
9475
10501
  }
10502
+ assertSafeURL(url.toString());
9476
10503
  wc.loadURL(url.toString());
9477
10504
  await waitForPotentialNavigation$1(wc, beforeUrl);
9478
10505
  const afterUrl = wc.getURL();
@@ -9589,7 +10616,8 @@ async function getPostActionState$1(ctx, name) {
9589
10616
  "hover",
9590
10617
  "focus",
9591
10618
  "fill_form",
9592
- "inspect_element"
10619
+ "inspect_element",
10620
+ "clear_overlays"
9593
10621
  ];
9594
10622
  const tabActions = [
9595
10623
  "create_tab",
@@ -9639,6 +10667,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
9639
10667
  "focus",
9640
10668
  "set_ad_blocking",
9641
10669
  "dismiss_popup",
10670
+ "clear_overlays",
9642
10671
  "read_page",
9643
10672
  "wait_for",
9644
10673
  "create_checkpoint",
@@ -9765,7 +10794,7 @@ async function executeAction(name, args, ctx) {
9765
10794
  const created = ctx.tabManager.getActiveTab();
9766
10795
  if (created) {
9767
10796
  await waitForLoad$1(created.view.webContents);
9768
- return `Created tab ${createdId}${getPostNavSummary(created.view.webContents)}`;
10797
+ return `Created tab ${createdId}${await getPostNavSummary(created.view.webContents)}`;
9769
10798
  }
9770
10799
  return `Created tab ${createdId}`;
9771
10800
  }
@@ -9777,7 +10806,7 @@ async function executeAction(name, args, ctx) {
9777
10806
  }
9778
10807
  ctx.tabManager.navigateTab(tabId, args.url);
9779
10808
  await waitForLoad$1(wc);
9780
- return `Navigated to ${wc.getURL()}${getPostNavSummary(wc)}`;
10809
+ return `Navigated to ${wc.getURL()}${await getPostNavSummary(wc)}`;
9781
10810
  }
9782
10811
  case "go_back": {
9783
10812
  if (!tab || !wc || !tabId) return "Error: No active tab";
@@ -9788,7 +10817,7 @@ async function executeAction(name, args, ctx) {
9788
10817
  ctx.tabManager.goBack(tabId);
9789
10818
  await waitForLoad$1(wc);
9790
10819
  const afterUrl = wc.getURL();
9791
- return afterUrl !== beforeUrl ? `Went back to ${afterUrl}${getPostNavSummary(wc)}` : `Back action completed but page stayed on ${afterUrl}`;
10820
+ return afterUrl !== beforeUrl ? `Went back to ${afterUrl}${await getPostNavSummary(wc)}` : `Back action completed but page stayed on ${afterUrl}`;
9792
10821
  }
9793
10822
  case "go_forward": {
9794
10823
  if (!tab || !wc || !tabId) return "Error: No active tab";
@@ -9799,7 +10828,7 @@ async function executeAction(name, args, ctx) {
9799
10828
  ctx.tabManager.goForward(tabId);
9800
10829
  await waitForLoad$1(wc);
9801
10830
  const afterUrl = wc.getURL();
9802
- return afterUrl !== beforeUrl ? `Went forward to ${afterUrl}${getPostNavSummary(wc)}` : `Forward action completed but page stayed on ${afterUrl}`;
10831
+ return afterUrl !== beforeUrl ? `Went forward to ${afterUrl}${await getPostNavSummary(wc)}` : `Forward action completed but page stayed on ${afterUrl}`;
9803
10832
  }
9804
10833
  case "reload": {
9805
10834
  if (!wc || !tabId) return "Error: No active tab";
@@ -9866,7 +10895,7 @@ async function executeAction(name, args, ctx) {
9866
10895
  }
9867
10896
  case "scroll": {
9868
10897
  if (!wc) return "Error: No active tab";
9869
- const pixels = args.amount || 500;
10898
+ const pixels = coerceOptionalNumber(args.amount) ?? 500;
9870
10899
  const dir = args.direction === "up" ? -pixels : pixels;
9871
10900
  const result2 = await scrollPage$1(wc, dir);
9872
10901
  return `Scrolled ${args.direction} by ${pixels}px (moved ${Math.abs(result2.movedY)}px, now at y=${Math.round(result2.afterY)})`;
@@ -9911,8 +10940,17 @@ async function executeAction(name, args, ctx) {
9911
10940
  if (!wc) return "Error: No active tab";
9912
10941
  return dismissPopup$1(wc);
9913
10942
  }
10943
+ case "clear_overlays": {
10944
+ if (!wc) return "Error: No active tab";
10945
+ const strategy = args.strategy === "interactive" ? "interactive" : "auto";
10946
+ return clearOverlays(wc, strategy);
10947
+ }
9914
10948
  case "read_page": {
9915
10949
  if (!wc) return "Error: No active tab";
10950
+ const requestedGlance = typeof args.mode === "string" && args.mode.trim().toLowerCase() === "glance";
10951
+ if (requestedGlance) {
10952
+ return glanceExtract(wc);
10953
+ }
9916
10954
  console.log("[Vessel read_page] starting extraction with 6s timeout");
9917
10955
  let content = null;
9918
10956
  try {
@@ -9931,7 +10969,29 @@ async function executeAction(name, args, ctx) {
9931
10969
  console.log(
9932
10970
  `[Vessel read_page] extraction result: ${content ? `content=${content.content.length}` : "null (timeout)"}`
9933
10971
  );
9934
- if (content) {
10972
+ if (!content || content.content.length === 0) {
10973
+ console.log("[Vessel read_page] content empty/null, trying quick iframe dismiss");
10974
+ try {
10975
+ const iframeResult = await Promise.race([
10976
+ tryDismissConsentIframe(wc),
10977
+ new Promise((resolve) => setTimeout(() => resolve(null), 2e3))
10978
+ ]);
10979
+ if (iframeResult) {
10980
+ console.log(`[Vessel read_page] iframe dismiss: ${iframeResult}`);
10981
+ await sleep$1(500);
10982
+ try {
10983
+ content = await Promise.race([
10984
+ extractContent(wc),
10985
+ new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
10986
+ ]);
10987
+ } catch {
10988
+ content = null;
10989
+ }
10990
+ }
10991
+ } catch {
10992
+ }
10993
+ }
10994
+ if (content && content.content.length > 0) {
9935
10995
  const liveSelectionSection = formatLiveSelectionSection(
9936
10996
  await captureLiveHighlightSnapshot(
9937
10997
  wc,
@@ -9963,16 +11023,8 @@ ${truncated}`;
9963
11023
  `Need more detail? Escalate with read_page(mode="debug") only if the narrow modes are insufficient.`
9964
11024
  ].filter(Boolean).join("\n\n");
9965
11025
  }
9966
- const title = wc.getTitle() || "(untitled)";
9967
- const url = wc.getURL();
9968
- return [
9969
- `# ${title}`,
9970
- `URL: ${url}`,
9971
- "",
9972
- "[Page content extraction timed out — the page JS thread is busy.]",
9973
- "[Use the search tool to search the site, or type_text/click to interact directly.]",
9974
- "[You can retry read_page in a few seconds once the page finishes loading.]"
9975
- ].join("\n");
11026
+ console.log("[Vessel read_page] falling back to glance mode");
11027
+ return glanceExtract(wc);
9976
11028
  }
9977
11029
  case "wait_for": {
9978
11030
  if (!wc) return "Error: No active tab";
@@ -10277,9 +11329,9 @@ ${truncated}`;
10277
11329
  if (!wc) return "Error: No active tab";
10278
11330
  const selector = await resolveSelector$1(wc, args.index, args.selector);
10279
11331
  const highlightColor = args.color || "yellow";
11332
+ const highlightText = normalizeLooseString(args.text);
10280
11333
  const url = wc.getURL();
10281
11334
  if (url && url !== "about:blank") {
10282
- const highlightText = typeof args.text === "string" ? args.text : void 0;
10283
11335
  addHighlight(
10284
11336
  url,
10285
11337
  typeof selector === "string" ? selector : void 0,
@@ -10292,7 +11344,7 @@ ${truncated}`;
10292
11344
  return highlightOnPage(
10293
11345
  wc,
10294
11346
  selector,
10295
- args.text,
11347
+ highlightText,
10296
11348
  args.label,
10297
11349
  args.durationMs,
10298
11350
  highlightColor
@@ -10305,7 +11357,7 @@ ${truncated}`;
10305
11357
  // --- Speedee System ---
10306
11358
  case "flow_start": {
10307
11359
  const goal = typeof args.goal === "string" ? args.goal : "";
10308
- const steps = Array.isArray(args.steps) ? args.steps.map(String) : [];
11360
+ const steps = coerceStringArray(args.steps) ?? [];
10309
11361
  if (!goal || steps.length === 0)
10310
11362
  return "Error: goal and steps are required";
10311
11363
  const flow = ctx.runtime.startFlow(goal, steps, wc?.getURL());
@@ -10365,9 +11417,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
10365
11417
  const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
10366
11418
  if (hasOverlays) {
10367
11419
  suggestions.push("BLOCKING OVERLAY detected — dismiss it first:");
10368
- suggestions.push(
10369
- " → dismiss_popup or click on close/accept button"
10370
- );
11420
+ suggestions.push(" → clear_overlays for stacked modals");
11421
+ suggestions.push(" → or dismiss_popup for a single popup");
10371
11422
  suggestions.push("");
10372
11423
  }
10373
11424
  if (hasPasswordField) {
@@ -10675,6 +11726,7 @@ ${steps.join("\n")}`;
10675
11726
  try {
10676
11727
  const url = new URL(searchInfo.formAction);
10677
11728
  url.searchParams.set(searchInfo.inputName || "q", query);
11729
+ assertSafeURL(url.toString());
10678
11730
  wc.loadURL(url.toString());
10679
11731
  await waitForPotentialNavigation$1(wc, beforeUrl);
10680
11732
  afterUrl = wc.getURL();
@@ -10744,9 +11796,20 @@ ${steps.join("\n")}`;
10744
11796
  '[aria-label="Accept cookies"]',
10745
11797
  '[aria-label="Accept all cookies"]',
10746
11798
  '[data-testid="cookie-accept"]',
11799
+ // CNN / WarnerMedia / common consent SDKs
11800
+ '[data-testid="consent-accept"]',
11801
+ '[data-testid="accept-all"]',
11802
+ 'button[class*="consent"][class*="accept"]',
11803
+ 'button[class*="privacy"][class*="accept"]',
11804
+ '.fc-cta-consent',
11805
+ '#sp_choice_button_accept',
11806
+ '.message-component.message-button.no-children.focusable.sp_choice_type_11',
11807
+ '[class*="truste"] [class*="accept"]',
11808
+ '[id*="consent-accept"]',
11809
+ '[class*="cmp-accept"]',
10747
11810
  ];
10748
11811
  // Also try text-matching on buttons
10749
- var textPatterns = ['accept all', 'accept cookies', 'allow all', 'allow cookies', 'agree', 'got it', 'ok', 'i agree', 'consent'];
11812
+ var textPatterns = ['accept all', 'accept cookies', 'allow all', 'allow cookies', 'agree', 'got it', 'ok', 'i agree', 'i accept', 'consent', 'continue', 'accept and continue', 'accept & continue'];
10750
11813
  for (var i = 0; i < selectors.length; i++) {
10751
11814
  var el = document.querySelector(selectors[i]);
10752
11815
  if (el && el instanceof HTMLElement) { el.click(); return "Dismissed cookie banner via: " + selectors[i]; }
@@ -10772,7 +11835,10 @@ ${steps.join("\n")}`;
10772
11835
  if (dismissed === PAGE_SCRIPT_TIMEOUT) {
10773
11836
  return pageBusyError("accept_cookies");
10774
11837
  }
10775
- return dismissed || "No cookie consent banner detected. Try dismiss_popup for other overlays.";
11838
+ if (dismissed) return dismissed;
11839
+ const iframeResult = await tryDismissConsentIframe(wc);
11840
+ if (iframeResult) return iframeResult;
11841
+ return "No cookie consent banner detected. Try dismiss_popup for other overlays.";
10776
11842
  }
10777
11843
  case "extract_table": {
10778
11844
  if (!wc) return "Error: No active tab";
@@ -10870,10 +11936,10 @@ ${JSON.stringify(tableJson, null, 2)}`;
10870
11936
  const flowCtx = ctx.runtime.getFlowContext();
10871
11937
  return result + await getPostActionState$1(ctx, name) + flowCtx;
10872
11938
  }
10873
- async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime, history) {
11939
+ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history) {
10874
11940
  const lowerQuery = query.toLowerCase().trim();
10875
11941
  const isSummarize = lowerQuery.startsWith("summarize") || lowerQuery.startsWith("tldr") || lowerQuery === "summary";
10876
- if (provider.streamAgentQuery && tabManager && activeWebContents && runtime) {
11942
+ if (provider.streamAgentQuery && tabManager && activeWebContents && runtime2) {
10877
11943
  try {
10878
11944
  const extractStart = Date.now();
10879
11945
  const pageContent = await extractContent(activeWebContents);
@@ -10886,7 +11952,7 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
10886
11952
  pageContent,
10887
11953
  defaultReadMode
10888
11954
  );
10889
- const runtimeState = runtime.getState();
11955
+ const runtimeState = runtime2.getState();
10890
11956
  const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
10891
11957
  const activeTabTitle = pageContent.title || "(untitled)";
10892
11958
  const activeTabUrl = pageContent.url || activeWebContents.getURL();
@@ -10929,8 +11995,12 @@ Instructions:
10929
11995
  - 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.
10930
11996
  - The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
10931
11997
  - When you only need detail on one product/result/card/form section, use inspect_element instead of reading the page.
10932
- - 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.
11998
+ - Escalate page reads progressively: read_page(mode="glance") for a fast viewport snapshot on heavy/slow pages, then read_page(mode="visible_only"), read_page(mode="results_only"), read_page(mode="forms_only"), read_page(mode="summary"), or read_page(mode="text_only") depending on what you need.
11999
+ - Use read_page(mode="glance") when a page is slow to load or extraction times out — it shows what's on screen (headings, links, buttons, inputs) without waiting for heavy JS. It's what a human would see by just looking at the page.
10933
12000
  - Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
12001
+ - If read_page returns empty or times out, do NOT retry with the same mode. Switch to read_page(mode="glance") or interact directly with click/type_text.
12002
+ - VIEWPORT SYNC: Treat scrolling as a real, user-visible browser action. If you say you are going to scroll, call scroll or scroll_to_element so the human sees the page move too.
12003
+ - read_page inspects the page without moving the human-visible viewport. Do not describe read_page as scrolling. If you want more context without changing the user's view, say you're reading the page; if you want the user to follow along lower on the page, actually scroll first.
10934
12004
  - 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").
10935
12005
  - 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.
10936
12006
  - If a page behaves abnormally or key UI fails to load, consider disabling ad blocking for that tab and reloading before retrying.
@@ -10946,7 +12016,7 @@ Instructions:
10946
12016
  - 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.
10947
12017
  - 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.
10948
12018
  - NEVER USE EMOJIS unless the user uses them first.`;
10949
- const actionCtx = { tabManager, runtime };
12019
+ const actionCtx = { tabManager, runtime: runtime2 };
10950
12020
  const contextualTools = pruneToolsForContext(
10951
12021
  AGENT_TOOLS,
10952
12022
  pageType,
@@ -11374,7 +12444,7 @@ function broadcastState(tabManager) {
11374
12444
  const tabId = tabManager.getActiveTabId();
11375
12445
  stateListener(getDevToolsPanelState(tabId));
11376
12446
  }
11377
- async function withDevToolsAction(runtime, tabManager, name, args, executor) {
12447
+ async function withDevToolsAction(runtime2, tabManager, name, args, executor) {
11378
12448
  const activityEntry = {
11379
12449
  id: ++activityCounter,
11380
12450
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11391,7 +12461,7 @@ async function withDevToolsAction(runtime, tabManager, name, args, executor) {
11391
12461
  broadcastState(tabManager);
11392
12462
  const startTime = Date.now();
11393
12463
  try {
11394
- const result = await runtime.runControlledAction({
12464
+ const result = await runtime2.runControlledAction({
11395
12465
  source: "mcp",
11396
12466
  name,
11397
12467
  args,
@@ -11413,7 +12483,7 @@ async function withDevToolsAction(runtime, tabManager, name, args, executor) {
11413
12483
  return asTextResponse$1(`Error: ${message}`);
11414
12484
  }
11415
12485
  }
11416
- function registerDevTools(server, tabManager, runtime) {
12486
+ function registerDevTools(server, tabManager, runtime2) {
11417
12487
  server.registerTool(
11418
12488
  "vessel_devtools_console_logs",
11419
12489
  {
@@ -11425,16 +12495,16 @@ function registerDevTools(server, tabManager, runtime) {
11425
12495
  search: zod.z.string().optional().describe("Filter entries containing this text (case-insensitive)")
11426
12496
  }
11427
12497
  },
11428
- async ({ level, limit, search }) => {
12498
+ async ({ level, limit, search: search2 }) => {
11429
12499
  return withDevToolsAction(
11430
- runtime,
12500
+ runtime2,
11431
12501
  tabManager,
11432
12502
  "devtools_console_logs",
11433
- { level, limit, search },
12503
+ { level, limit, search: search2 },
11434
12504
  async () => {
11435
12505
  const session = getOrCreateSession(tabManager);
11436
12506
  await session.ensureConsoleDomain();
11437
- const entries = session.getConsoleLogs({ level, limit, search });
12507
+ const entries = session.getConsoleLogs({ level, limit, search: search2 });
11438
12508
  if (entries.length === 0) {
11439
12509
  return "No console entries captured yet. Console monitoring is now active — new entries will be captured as they occur.";
11440
12510
  }
@@ -11451,7 +12521,7 @@ function registerDevTools(server, tabManager, runtime) {
11451
12521
  },
11452
12522
  async () => {
11453
12523
  return withDevToolsAction(
11454
- runtime,
12524
+ runtime2,
11455
12525
  tabManager,
11456
12526
  "devtools_console_clear",
11457
12527
  {},
@@ -11478,7 +12548,7 @@ function registerDevTools(server, tabManager, runtime) {
11478
12548
  },
11479
12549
  async ({ url_pattern, method, status_min, status_max, limit }) => {
11480
12550
  return withDevToolsAction(
11481
- runtime,
12551
+ runtime2,
11482
12552
  tabManager,
11483
12553
  "devtools_network_log",
11484
12554
  { url_pattern, method, status_min, status_max, limit },
@@ -11510,7 +12580,7 @@ function registerDevTools(server, tabManager, runtime) {
11510
12580
  },
11511
12581
  async ({ request_id }) => {
11512
12582
  return withDevToolsAction(
11513
- runtime,
12583
+ runtime2,
11514
12584
  tabManager,
11515
12585
  "devtools_network_response_body",
11516
12586
  { request_id },
@@ -11536,7 +12606,7 @@ function registerDevTools(server, tabManager, runtime) {
11536
12606
  },
11537
12607
  async () => {
11538
12608
  return withDevToolsAction(
11539
- runtime,
12609
+ runtime2,
11540
12610
  tabManager,
11541
12611
  "devtools_network_clear",
11542
12612
  {},
@@ -11560,7 +12630,7 @@ function registerDevTools(server, tabManager, runtime) {
11560
12630
  },
11561
12631
  async ({ selector, include_html }) => {
11562
12632
  return withDevToolsAction(
11563
- runtime,
12633
+ runtime2,
11564
12634
  tabManager,
11565
12635
  "devtools_query_dom",
11566
12636
  { selector, include_html },
@@ -11591,7 +12661,7 @@ function registerDevTools(server, tabManager, runtime) {
11591
12661
  },
11592
12662
  async ({ selector, properties }) => {
11593
12663
  return withDevToolsAction(
11594
- runtime,
12664
+ runtime2,
11595
12665
  tabManager,
11596
12666
  "devtools_get_styles",
11597
12667
  { selector, properties },
@@ -11619,7 +12689,7 @@ function registerDevTools(server, tabManager, runtime) {
11619
12689
  },
11620
12690
  async ({ selector, attribute, value }) => {
11621
12691
  return withDevToolsAction(
11622
- runtime,
12692
+ runtime2,
11623
12693
  tabManager,
11624
12694
  "devtools_modify_dom",
11625
12695
  { selector, attribute, value },
@@ -11641,7 +12711,7 @@ function registerDevTools(server, tabManager, runtime) {
11641
12711
  },
11642
12712
  async ({ expression }) => {
11643
12713
  return withDevToolsAction(
11644
- runtime,
12714
+ runtime2,
11645
12715
  tabManager,
11646
12716
  "devtools_execute_js",
11647
12717
  { expression: expression.slice(0, 200) },
@@ -11669,7 +12739,7 @@ Exception: ${result.exceptionDetails}`);
11669
12739
  },
11670
12740
  async ({ type }) => {
11671
12741
  return withDevToolsAction(
11672
- runtime,
12742
+ runtime2,
11673
12743
  tabManager,
11674
12744
  "devtools_get_storage",
11675
12745
  { type },
@@ -11698,7 +12768,7 @@ Exception: ${result.exceptionDetails}`);
11698
12768
  },
11699
12769
  async ({ type, key, value }) => {
11700
12770
  return withDevToolsAction(
11701
- runtime,
12771
+ runtime2,
11702
12772
  tabManager,
11703
12773
  "devtools_set_storage",
11704
12774
  { type, key, value: value ? value.slice(0, 100) : null },
@@ -11717,7 +12787,7 @@ Exception: ${result.exceptionDetails}`);
11717
12787
  },
11718
12788
  async () => {
11719
12789
  return withDevToolsAction(
11720
- runtime,
12790
+ runtime2,
11721
12791
  tabManager,
11722
12792
  "devtools_performance",
11723
12793
  {},
@@ -11741,7 +12811,7 @@ Exception: ${result.exceptionDetails}`);
11741
12811
  },
11742
12812
  async ({ type, limit }) => {
11743
12813
  return withDevToolsAction(
11744
- runtime,
12814
+ runtime2,
11745
12815
  tabManager,
11746
12816
  "devtools_get_errors",
11747
12817
  { type, limit },
@@ -11765,7 +12835,7 @@ Exception: ${result.exceptionDetails}`);
11765
12835
  },
11766
12836
  async () => {
11767
12837
  return withDevToolsAction(
11768
- runtime,
12838
+ runtime2,
11769
12839
  tabManager,
11770
12840
  "devtools_clear_errors",
11771
12841
  {},
@@ -11779,6 +12849,7 @@ Exception: ${result.exceptionDetails}`);
11779
12849
  );
11780
12850
  }
11781
12851
  let httpServer = null;
12852
+ let mcpAuthToken = null;
11782
12853
  function asTextResponse(text) {
11783
12854
  return { content: [{ type: "text", text }] };
11784
12855
  }
@@ -12716,9 +13787,9 @@ async function getPostActionState(tabManager, name) {
12716
13787
  }
12717
13788
  return "";
12718
13789
  }
12719
- async function withAction(runtime, tabManager, name, args, executor) {
13790
+ async function withAction(runtime2, tabManager, name, args, executor) {
12720
13791
  try {
12721
- const result = await runtime.runControlledAction({
13792
+ const result = await runtime2.runControlledAction({
12722
13793
  source: "mcp",
12723
13794
  name,
12724
13795
  args,
@@ -12727,7 +13798,7 @@ async function withAction(runtime, tabManager, name, args, executor) {
12727
13798
  executor
12728
13799
  });
12729
13800
  const stateInfo = await getPostActionState(tabManager, name);
12730
- const flowCtx = runtime.getFlowContext();
13801
+ const flowCtx = runtime2.getFlowContext();
12731
13802
  return asTextResponse(result + stateInfo + flowCtx);
12732
13803
  } catch (error) {
12733
13804
  return asTextResponse(
@@ -13021,6 +14092,7 @@ async function submitForm(wc, index, selector) {
13021
14092
  if (formInfo.params) {
13022
14093
  url.search = formInfo.params;
13023
14094
  }
14095
+ assertSafeURL(url.toString());
13024
14096
  wc.loadURL(url.toString());
13025
14097
  await waitForPotentialNavigation(wc, beforeUrl);
13026
14098
  const afterUrl = wc.getURL();
@@ -13162,7 +14234,7 @@ async function captureScreenshotPayload(wc) {
13162
14234
  }
13163
14235
  return { ok: false, error: "page image was empty after 3 attempts" };
13164
14236
  }
13165
- function registerTools(server, tabManager, runtime) {
14237
+ function registerTools(server, tabManager, runtime2) {
13166
14238
  server.registerPrompt(
13167
14239
  "vessel-supervisor-brief",
13168
14240
  {
@@ -13170,7 +14242,7 @@ function registerTools(server, tabManager, runtime) {
13170
14242
  description: "A reusable prompt for reviewing the current Vessel runtime state."
13171
14243
  },
13172
14244
  async () => {
13173
- const state2 = runtime.getState();
14245
+ const state2 = runtime2.getState();
13174
14246
  const activeTab = getActiveTabSummary(tabManager);
13175
14247
  return asPromptResponse(
13176
14248
  [
@@ -13197,7 +14269,7 @@ function registerTools(server, tabManager, runtime) {
13197
14269
  contents: [
13198
14270
  {
13199
14271
  uri: "vessel://runtime/state",
13200
- text: JSON.stringify(runtime.getState(), null, 2)
14272
+ text: JSON.stringify(runtime2.getState(), null, 2)
13201
14273
  }
13202
14274
  ]
13203
14275
  })
@@ -13313,7 +14385,7 @@ function registerTools(server, tabManager, runtime) {
13313
14385
  }
13314
14386
  },
13315
14387
  async ({ text, stream_id, mode, kind, title }) => {
13316
- const entry = runtime.publishTranscript({
14388
+ const entry = runtime2.publishTranscript({
13317
14389
  source: "mcp",
13318
14390
  text,
13319
14391
  streamId: stream_id,
@@ -13343,7 +14415,7 @@ function registerTools(server, tabManager, runtime) {
13343
14415
  description: "Clear the in-browser transcript monitor state."
13344
14416
  },
13345
14417
  async () => {
13346
- runtime.clearTranscript();
14418
+ runtime2.clearTranscript();
13347
14419
  return asTextResponse("Cleared browser transcript monitor.");
13348
14420
  }
13349
14421
  );
@@ -13483,7 +14555,7 @@ ${buildScopedContext(pageContent, mode)}`;
13483
14555
  `Navigation blocked: ${url} returned ${preCheck.detail || "dead link"}. Try a different URL or go back and choose another link.`
13484
14556
  );
13485
14557
  }
13486
- return withAction(runtime, tabManager, "navigate", { url }, async () => {
14558
+ return withAction(runtime2, tabManager, "navigate", { url }, async () => {
13487
14559
  const id = tabManager.getActiveTabId();
13488
14560
  tabManager.navigateTab(id, url);
13489
14561
  const { httpStatus } = await waitForLoadWithStatus(
@@ -13513,7 +14585,7 @@ ${buildScopedContext(pageContent, mode)}`;
13513
14585
  return asTextResponse("Error: No active tab");
13514
14586
  }
13515
14587
  return withAction(
13516
- runtime,
14588
+ runtime2,
13517
14589
  tabManager,
13518
14590
  "set_ad_blocking",
13519
14591
  { enabled, tabId, match, reload },
@@ -13602,7 +14674,7 @@ ${buildScopedContext(pageContent, mode)}`;
13602
14674
  async () => {
13603
14675
  const tab = tabManager.getActiveTab();
13604
14676
  if (!tab) return asTextResponse("Error: No active tab");
13605
- return withAction(runtime, tabManager, "go_back", {}, async () => {
14677
+ return withAction(runtime2, tabManager, "go_back", {}, async () => {
13606
14678
  if (!tab.canGoBack()) {
13607
14679
  return "No previous page in history";
13608
14680
  }
@@ -13623,7 +14695,7 @@ ${buildScopedContext(pageContent, mode)}`;
13623
14695
  async () => {
13624
14696
  const tab = tabManager.getActiveTab();
13625
14697
  if (!tab) return asTextResponse("Error: No active tab");
13626
- return withAction(runtime, tabManager, "go_forward", {}, async () => {
14698
+ return withAction(runtime2, tabManager, "go_forward", {}, async () => {
13627
14699
  if (!tab.canGoForward()) {
13628
14700
  return "No forward page in history";
13629
14701
  }
@@ -13644,7 +14716,7 @@ ${buildScopedContext(pageContent, mode)}`;
13644
14716
  async () => {
13645
14717
  const tab = tabManager.getActiveTab();
13646
14718
  if (!tab) return asTextResponse("Error: No active tab");
13647
- return withAction(runtime, tabManager, "reload", {}, async () => {
14719
+ return withAction(runtime2, tabManager, "reload", {}, async () => {
13648
14720
  tabManager.reloadTab(tabManager.getActiveTabId());
13649
14721
  await waitForLoad(tab.view.webContents);
13650
14722
  return `Reloaded ${tab.view.webContents.getURL()}`;
@@ -13665,7 +14737,7 @@ ${buildScopedContext(pageContent, mode)}`;
13665
14737
  const tab = tabManager.getActiveTab();
13666
14738
  if (!tab) return asTextResponse("Error: No active tab");
13667
14739
  return withAction(
13668
- runtime,
14740
+ runtime2,
13669
14741
  tabManager,
13670
14742
  "click",
13671
14743
  { index, selector },
@@ -13694,7 +14766,7 @@ ${buildScopedContext(pageContent, mode)}`;
13694
14766
  const tab = tabManager.getActiveTab();
13695
14767
  if (!tab) return asTextResponse("Error: No active tab");
13696
14768
  return withAction(
13697
- runtime,
14769
+ runtime2,
13698
14770
  tabManager,
13699
14771
  "hover",
13700
14772
  { index, selector },
@@ -13723,7 +14795,7 @@ ${buildScopedContext(pageContent, mode)}`;
13723
14795
  const tab = tabManager.getActiveTab();
13724
14796
  if (!tab) return asTextResponse("Error: No active tab");
13725
14797
  return withAction(
13726
- runtime,
14798
+ runtime2,
13727
14799
  tabManager,
13728
14800
  "focus",
13729
14801
  { index, selector },
@@ -13837,7 +14909,7 @@ ${buildScopedContext(pageContent, mode)}`;
13837
14909
  const tab = tabManager.getActiveTab();
13838
14910
  if (!tab) return asTextResponse("Error: No active tab");
13839
14911
  return withAction(
13840
- runtime,
14912
+ runtime2,
13841
14913
  tabManager,
13842
14914
  "type",
13843
14915
  { index, selector, text, mode },
@@ -13876,7 +14948,7 @@ ${buildScopedContext(pageContent, mode)}`;
13876
14948
  const tab = tabManager.getActiveTab();
13877
14949
  if (!tab) return asTextResponse("Error: No active tab");
13878
14950
  return withAction(
13879
- runtime,
14951
+ runtime2,
13880
14952
  tabManager,
13881
14953
  "type_text",
13882
14954
  { index, selector, text, mode },
@@ -13913,7 +14985,7 @@ ${buildScopedContext(pageContent, mode)}`;
13913
14985
  const tab = tabManager.getActiveTab();
13914
14986
  if (!tab) return asTextResponse("Error: No active tab");
13915
14987
  return withAction(
13916
- runtime,
14988
+ runtime2,
13917
14989
  tabManager,
13918
14990
  "select_option",
13919
14991
  { index, selector, label, value },
@@ -13935,7 +15007,7 @@ ${buildScopedContext(pageContent, mode)}`;
13935
15007
  const tab = tabManager.getActiveTab();
13936
15008
  if (!tab) return asTextResponse("Error: No active tab");
13937
15009
  return withAction(
13938
- runtime,
15010
+ runtime2,
13939
15011
  tabManager,
13940
15012
  "submit_form",
13941
15013
  { index, selector },
@@ -13968,7 +15040,7 @@ ${buildScopedContext(pageContent, mode)}`;
13968
15040
  const tab = tabManager.getActiveTab();
13969
15041
  if (!tab) return asTextResponse("Error: No active tab");
13970
15042
  return withAction(
13971
- runtime,
15043
+ runtime2,
13972
15044
  tabManager,
13973
15045
  "press_key",
13974
15046
  { key, index, selector },
@@ -13995,19 +15067,21 @@ ${buildScopedContext(pageContent, mode)}`;
13995
15067
  description: "Scroll the page up or down.",
13996
15068
  inputSchema: {
13997
15069
  direction: zod.z.enum(["up", "down"]).describe("Scroll direction"),
13998
- amount: zod.z.number().optional().describe("Pixels to scroll (default 500)")
15070
+ amount: optionalNumberLikeSchema().describe(
15071
+ "Pixels to scroll (default 500)"
15072
+ )
13999
15073
  }
14000
15074
  },
14001
15075
  async ({ direction, amount }) => {
14002
15076
  const tab = tabManager.getActiveTab();
14003
15077
  if (!tab) return asTextResponse("Error: No active tab");
14004
15078
  return withAction(
14005
- runtime,
15079
+ runtime2,
14006
15080
  tabManager,
14007
15081
  "scroll",
14008
15082
  { direction, amount },
14009
15083
  async () => {
14010
- const pixels = amount || 500;
15084
+ const pixels = coerceOptionalNumber(amount) ?? 500;
14011
15085
  const dir = direction === "up" ? -pixels : pixels;
14012
15086
  const result = await scrollPage(tab.view.webContents, dir);
14013
15087
  return `Scrolled ${direction} by ${pixels}px (moved ${Math.abs(result.movedY)}px, now at y=${Math.round(result.afterY)})`;
@@ -14025,7 +15099,7 @@ ${buildScopedContext(pageContent, mode)}`;
14025
15099
  const tab = tabManager.getActiveTab();
14026
15100
  if (!tab) return asTextResponse("Error: No active tab");
14027
15101
  return withAction(
14028
- runtime,
15102
+ runtime2,
14029
15103
  tabManager,
14030
15104
  "dismiss_popup",
14031
15105
  {},
@@ -14033,6 +15107,32 @@ ${buildScopedContext(pageContent, mode)}`;
14033
15107
  );
14034
15108
  }
14035
15109
  );
15110
+ server.registerTool(
15111
+ "vessel_clear_overlays",
15112
+ {
15113
+ title: "Clear Overlays",
15114
+ description: "Work through blocking overlays and modals until the page is unblocked, using overlay-specific heuristics for consent banners and radio-selection dialogs.",
15115
+ inputSchema: {
15116
+ strategy: zod.z.enum(["auto", "interactive"]).optional().describe(
15117
+ 'How aggressively to clear overlays. "auto" uses heuristics; "interactive" stops earlier when human judgment may be needed.'
15118
+ )
15119
+ }
15120
+ },
15121
+ async ({ strategy }) => {
15122
+ const tab = tabManager.getActiveTab();
15123
+ if (!tab) return asTextResponse("Error: No active tab");
15124
+ return withAction(
15125
+ runtime2,
15126
+ tabManager,
15127
+ "clear_overlays",
15128
+ { strategy: strategy || "auto" },
15129
+ async () => clearOverlays(
15130
+ tab.view.webContents,
15131
+ strategy === "interactive" ? "interactive" : "auto"
15132
+ )
15133
+ );
15134
+ }
15135
+ );
14036
15136
  server.registerTool(
14037
15137
  "vessel_wait_for",
14038
15138
  {
@@ -14048,7 +15148,7 @@ ${buildScopedContext(pageContent, mode)}`;
14048
15148
  const tab = tabManager.getActiveTab();
14049
15149
  if (!tab) return asTextResponse("Error: No active tab");
14050
15150
  return withAction(
14051
- runtime,
15151
+ runtime2,
14052
15152
  tabManager,
14053
15153
  "wait_for",
14054
15154
  { text, selector, timeoutMs },
@@ -14065,7 +15165,7 @@ ${buildScopedContext(pageContent, mode)}`;
14065
15165
  url: zod.z.string().optional().describe("URL to open (defaults to about:blank)")
14066
15166
  }
14067
15167
  },
14068
- async ({ url }) => withAction(runtime, tabManager, "create_tab", { url }, async () => {
15168
+ async ({ url }) => withAction(runtime2, tabManager, "create_tab", { url }, async () => {
14069
15169
  const id = tabManager.createTab(url || "about:blank");
14070
15170
  const tab = tabManager.getActiveTab();
14071
15171
  if (tab) {
@@ -14085,7 +15185,7 @@ ${buildScopedContext(pageContent, mode)}`;
14085
15185
  }
14086
15186
  },
14087
15187
  async ({ tabId, match }) => withAction(
14088
- runtime,
15188
+ runtime2,
14089
15189
  tabManager,
14090
15190
  "switch_tab",
14091
15191
  { tabId, match },
@@ -14108,7 +15208,7 @@ ${buildScopedContext(pageContent, mode)}`;
14108
15208
  tabId: zod.z.string().describe("The tab ID to close")
14109
15209
  }
14110
15210
  },
14111
- async ({ tabId }) => withAction(runtime, tabManager, "close_tab", { tabId }, async () => {
15211
+ async ({ tabId }) => withAction(runtime2, tabManager, "close_tab", { tabId }, async () => {
14112
15212
  tabManager.closeTab(tabId);
14113
15213
  return `Closed tab ${tabId}`;
14114
15214
  })
@@ -14124,12 +15224,12 @@ ${buildScopedContext(pageContent, mode)}`;
14124
15224
  }
14125
15225
  },
14126
15226
  async ({ name, note }) => withAction(
14127
- runtime,
15227
+ runtime2,
14128
15228
  tabManager,
14129
15229
  "create_checkpoint",
14130
15230
  { name, note },
14131
15231
  async () => {
14132
- const checkpoint = runtime.createCheckpoint(name, note);
15232
+ const checkpoint = runtime2.createCheckpoint(name, note);
14133
15233
  return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
14134
15234
  }
14135
15235
  )
@@ -14145,12 +15245,12 @@ ${buildScopedContext(pageContent, mode)}`;
14145
15245
  }
14146
15246
  },
14147
15247
  async ({ name, note }) => withAction(
14148
- runtime,
15248
+ runtime2,
14149
15249
  tabManager,
14150
15250
  "create_checkpoint",
14151
15251
  { name, note },
14152
15252
  async () => {
14153
- const checkpoint = runtime.createCheckpoint(name, note);
15253
+ const checkpoint = runtime2.createCheckpoint(name, note);
14154
15254
  return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
14155
15255
  }
14156
15256
  )
@@ -14166,17 +15266,17 @@ ${buildScopedContext(pageContent, mode)}`;
14166
15266
  }
14167
15267
  },
14168
15268
  async ({ checkpointId, name }) => withAction(
14169
- runtime,
15269
+ runtime2,
14170
15270
  tabManager,
14171
15271
  "restore_checkpoint",
14172
15272
  { checkpointId, name },
14173
15273
  async () => {
14174
- const state2 = runtime.getState();
15274
+ const state2 = runtime2.getState();
14175
15275
  const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
14176
15276
  if (!checkpoint) {
14177
15277
  return "Error: No matching checkpoint found";
14178
15278
  }
14179
- runtime.restoreCheckpoint(checkpoint.id);
15279
+ runtime2.restoreCheckpoint(checkpoint.id);
14180
15280
  return `Restored checkpoint ${checkpoint.name}`;
14181
15281
  }
14182
15282
  )
@@ -14192,17 +15292,17 @@ ${buildScopedContext(pageContent, mode)}`;
14192
15292
  }
14193
15293
  },
14194
15294
  async ({ checkpointId, name }) => withAction(
14195
- runtime,
15295
+ runtime2,
14196
15296
  tabManager,
14197
15297
  "restore_checkpoint",
14198
15298
  { checkpointId, name },
14199
15299
  async () => {
14200
- const state2 = runtime.getState();
15300
+ const state2 = runtime2.getState();
14201
15301
  const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
14202
15302
  if (!checkpoint) {
14203
15303
  return "Error: No matching checkpoint found";
14204
15304
  }
14205
- runtime.restoreCheckpoint(checkpoint.id);
15305
+ runtime2.restoreCheckpoint(checkpoint.id);
14206
15306
  return `Restored checkpoint ${checkpoint.name}`;
14207
15307
  }
14208
15308
  )
@@ -14216,7 +15316,7 @@ ${buildScopedContext(pageContent, mode)}`;
14216
15316
  name: zod.z.string().describe("Session name such as github-logged-in")
14217
15317
  }
14218
15318
  },
14219
- async ({ name }) => withAction(runtime, tabManager, "save_session", { name }, async () => {
15319
+ async ({ name }) => withAction(runtime2, tabManager, "save_session", { name }, async () => {
14220
15320
  const saved = await saveNamedSession(
14221
15321
  tabManager,
14222
15322
  name
@@ -14233,7 +15333,7 @@ ${buildScopedContext(pageContent, mode)}`;
14233
15333
  name: zod.z.string().describe("Previously saved session name")
14234
15334
  }
14235
15335
  },
14236
- async ({ name }) => withAction(runtime, tabManager, "load_session", { name }, async () => {
15336
+ async ({ name }) => withAction(runtime2, tabManager, "load_session", { name }, async () => {
14237
15337
  const loaded = await loadNamedSession(
14238
15338
  tabManager,
14239
15339
  name
@@ -14247,7 +15347,7 @@ ${buildScopedContext(pageContent, mode)}`;
14247
15347
  title: "List Sessions",
14248
15348
  description: "List previously saved named browser sessions with cookie and storage counts."
14249
15349
  },
14250
- async () => withAction(runtime, tabManager, "list_sessions", {}, async () => {
15350
+ async () => withAction(runtime2, tabManager, "list_sessions", {}, async () => {
14251
15351
  const sessions2 = listNamedSessions();
14252
15352
  if (sessions2.length === 0) return "No saved sessions";
14253
15353
  return sessions2.map(
@@ -14265,7 +15365,7 @@ ${buildScopedContext(pageContent, mode)}`;
14265
15365
  }
14266
15366
  },
14267
15367
  async ({ name }) => withAction(
14268
- runtime,
15368
+ runtime2,
14269
15369
  tabManager,
14270
15370
  "delete_session",
14271
15371
  { name },
@@ -14332,7 +15432,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14332
15432
  inputSchema: {
14333
15433
  index: zod.z.number().optional().describe("Element index from extracted content to highlight"),
14334
15434
  selector: zod.z.string().optional().describe("CSS selector of element to highlight"),
14335
- text: zod.z.string().optional().describe(
15435
+ text: normalizedOptionalStringSchema().describe(
14336
15436
  "Text to find and highlight on the page (highlights all occurrences)"
14337
15437
  ),
14338
15438
  label: zod.z.string().optional().describe("Optional annotation label to display near the highlight"),
@@ -14350,18 +15450,27 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14350
15450
  async ({ index, selector, text, label, durationMs, persist, color }) => {
14351
15451
  const tab = tabManager.getActiveTab();
14352
15452
  if (!tab) return asTextResponse("Error: No active tab");
15453
+ const normalizedText = normalizeLooseString(text);
14353
15454
  return withAction(
14354
- runtime,
15455
+ runtime2,
14355
15456
  tabManager,
14356
15457
  "highlight",
14357
- { index, selector, text, label, durationMs, persist, color },
15458
+ {
15459
+ index,
15460
+ selector,
15461
+ text: normalizedText,
15462
+ label,
15463
+ durationMs,
15464
+ persist,
15465
+ color
15466
+ },
14358
15467
  async () => {
14359
15468
  const wc = tab.view.webContents;
14360
15469
  const resolvedSelector = await resolveSelector(wc, index, selector);
14361
15470
  const result = await highlightOnPage(
14362
15471
  wc,
14363
15472
  resolvedSelector,
14364
- text,
15473
+ normalizedText,
14365
15474
  label,
14366
15475
  durationMs,
14367
15476
  color
@@ -14371,7 +15480,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14371
15480
  addHighlight(
14372
15481
  url,
14373
15482
  resolvedSelector ?? void 0,
14374
- text,
15483
+ normalizedText,
14375
15484
  label,
14376
15485
  color,
14377
15486
  "agent"
@@ -14392,7 +15501,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14392
15501
  const tab = tabManager.getActiveTab();
14393
15502
  if (!tab) return asTextResponse("Error: No active tab");
14394
15503
  return withAction(
14395
- runtime,
15504
+ runtime2,
14396
15505
  tabManager,
14397
15506
  "clear_highlights",
14398
15507
  {},
@@ -14417,7 +15526,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14417
15526
  }
14418
15527
  },
14419
15528
  async ({ url }) => {
14420
- const state2 = getState$1();
15529
+ const state2 = getState$2();
14421
15530
  const activeTab = tabManager.getActiveTab();
14422
15531
  const activeUrl = activeTab ? normalizeUrl(activeTab.view.webContents.getURL()) : null;
14423
15532
  const activeSavedHighlights = activeUrl ? state2.highlights.filter((highlight) => highlight.url === activeUrl) : [];
@@ -14548,7 +15657,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14548
15657
  },
14549
15658
  async ({ name, summary }) => {
14550
15659
  return withAction(
14551
- runtime,
15660
+ runtime2,
14552
15661
  tabManager,
14553
15662
  "create_bookmark_folder",
14554
15663
  { name, summary },
@@ -14610,7 +15719,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14610
15719
  on_duplicate
14611
15720
  }) => {
14612
15721
  return withAction(
14613
- runtime,
15722
+ runtime2,
14614
15723
  tabManager,
14615
15724
  "save_bookmark",
14616
15725
  {
@@ -14689,7 +15798,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14689
15798
  },
14690
15799
  async ({ folder_id, folder_name }) => {
14691
15800
  return withAction(
14692
- runtime,
15801
+ runtime2,
14693
15802
  tabManager,
14694
15803
  "list_bookmarks",
14695
15804
  { folder_id, folder_name },
@@ -14754,7 +15863,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14754
15863
  },
14755
15864
  async (args) => {
14756
15865
  return withAction(
14757
- runtime,
15866
+ runtime2,
14758
15867
  tabManager,
14759
15868
  "organize_bookmark",
14760
15869
  args,
@@ -14821,7 +15930,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14821
15930
  },
14822
15931
  async ({ query }) => {
14823
15932
  return withAction(
14824
- runtime,
15933
+ runtime2,
14825
15934
  tabManager,
14826
15935
  "search_bookmarks",
14827
15936
  { query },
@@ -14852,7 +15961,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14852
15961
  },
14853
15962
  async ({ bookmark_id }) => {
14854
15963
  return withAction(
14855
- runtime,
15964
+ runtime2,
14856
15965
  tabManager,
14857
15966
  "remove_bookmark",
14858
15967
  { bookmark_id },
@@ -14885,7 +15994,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14885
15994
  },
14886
15995
  async ({ bookmark_id, url, title, index, selector, note }) => {
14887
15996
  return withAction(
14888
- runtime,
15997
+ runtime2,
14889
15998
  tabManager,
14890
15999
  "archive_bookmark",
14891
16000
  { bookmark_id, url, title, index, selector, note },
@@ -14955,7 +16064,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14955
16064
  },
14956
16065
  async ({ bookmark_id, new_tab }) => {
14957
16066
  return withAction(
14958
- runtime,
16067
+ runtime2,
14959
16068
  tabManager,
14960
16069
  "open_bookmark",
14961
16070
  { bookmark_id, new_tab },
@@ -14998,7 +16107,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14998
16107
  },
14999
16108
  async ({ folder_id }) => {
15000
16109
  return withAction(
15001
- runtime,
16110
+ runtime2,
15002
16111
  tabManager,
15003
16112
  "remove_bookmark_folder",
15004
16113
  { folder_id },
@@ -15024,7 +16133,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15024
16133
  },
15025
16134
  async ({ folder_id, new_name, summary }) => {
15026
16135
  return withAction(
15027
- runtime,
16136
+ runtime2,
15028
16137
  tabManager,
15029
16138
  "rename_bookmark_folder",
15030
16139
  { folder_id, new_name, summary },
@@ -15061,7 +16170,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15061
16170
  },
15062
16171
  async ({ title, body, folder, tags }) => {
15063
16172
  return withAction(
15064
- runtime,
16173
+ runtime2,
15065
16174
  tabManager,
15066
16175
  "memory_note_create",
15067
16176
  { title, folder, tags },
@@ -15085,7 +16194,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15085
16194
  },
15086
16195
  async ({ note_path, content, heading }) => {
15087
16196
  return withAction(
15088
- runtime,
16197
+ runtime2,
15089
16198
  tabManager,
15090
16199
  "memory_note_append",
15091
16200
  { note_path, heading },
@@ -15112,7 +16221,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15112
16221
  },
15113
16222
  async ({ folder, limit }) => {
15114
16223
  return withAction(
15115
- runtime,
16224
+ runtime2,
15116
16225
  tabManager,
15117
16226
  "memory_note_list",
15118
16227
  { folder, limit },
@@ -15142,7 +16251,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15142
16251
  },
15143
16252
  async ({ query, folder, tags, limit }) => {
15144
16253
  return withAction(
15145
- runtime,
16254
+ runtime2,
15146
16255
  tabManager,
15147
16256
  "memory_note_search",
15148
16257
  { query, folder, tags, limit },
@@ -15175,7 +16284,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15175
16284
  const tab = tabManager.getActiveTab();
15176
16285
  if (!tab) return asTextResponse("Error: No active tab");
15177
16286
  return withAction(
15178
- runtime,
16287
+ runtime2,
15179
16288
  tabManager,
15180
16289
  "memory_page_capture",
15181
16290
  { title, folder, tags },
@@ -15212,7 +16321,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15212
16321
  },
15213
16322
  async ({ bookmark_id, note_path, title, folder, note, tags }) => {
15214
16323
  return withAction(
15215
- runtime,
16324
+ runtime2,
15216
16325
  tabManager,
15217
16326
  "memory_link_bookmark",
15218
16327
  { bookmark_id, note_path, title, folder, tags },
@@ -15243,16 +16352,17 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15243
16352
  goal: zod.z.string().describe(
15244
16353
  "What this workflow accomplishes (e.g. 'Purchase item from Amazon')"
15245
16354
  ),
15246
- steps: zod.z.array(zod.z.string()).describe(
16355
+ steps: stringArrayLikeSchema().describe(
15247
16356
  "Ordered list of step labels (e.g. ['Log in', 'Search', 'Select item', 'Checkout'])"
15248
16357
  )
15249
16358
  }
15250
16359
  },
15251
16360
  async ({ goal, steps }) => {
16361
+ const normalizedSteps = coerceStringArray(steps) ?? [];
15252
16362
  const tab = tabManager.getActiveTab();
15253
- const flow = runtime.startFlow(
16363
+ const flow = runtime2.startFlow(
15254
16364
  goal,
15255
- steps,
16365
+ normalizedSteps,
15256
16366
  tab?.view.webContents.getURL()
15257
16367
  );
15258
16368
  return asTextResponse(
@@ -15271,9 +16381,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15271
16381
  }
15272
16382
  },
15273
16383
  async ({ detail }) => {
15274
- const flow = runtime.advanceFlow(detail);
16384
+ const flow = runtime2.advanceFlow(detail);
15275
16385
  if (!flow) return asTextResponse("No active flow to advance");
15276
- const ctx = runtime.getFlowContext();
16386
+ const ctx = runtime2.getFlowContext();
15277
16387
  return asTextResponse(`Step completed.${ctx}`);
15278
16388
  }
15279
16389
  );
@@ -15284,9 +16394,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15284
16394
  description: "Check the current workflow progress."
15285
16395
  },
15286
16396
  async () => {
15287
- const flow = runtime.getFlowState();
16397
+ const flow = runtime2.getFlowState();
15288
16398
  if (!flow) return asTextResponse("No active workflow.");
15289
- return asTextResponse(runtime.getFlowContext());
16399
+ return asTextResponse(runtime2.getFlowContext());
15290
16400
  }
15291
16401
  );
15292
16402
  server.registerTool(
@@ -15296,7 +16406,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15296
16406
  description: "Clear the active workflow tracker."
15297
16407
  },
15298
16408
  async () => {
15299
- runtime.clearFlow();
16409
+ runtime2.clearFlow();
15300
16410
  return asTextResponse("Workflow ended.");
15301
16411
  }
15302
16412
  );
@@ -15325,7 +16435,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15325
16435
  suggestions.push(`Page: ${page.title || "(untitled)"}`);
15326
16436
  suggestions.push(`URL: ${page.url}`);
15327
16437
  suggestions.push("");
15328
- const flowCtx = runtime.getFlowContext();
16438
+ const flowCtx = runtime2.getFlowContext();
15329
16439
  if (flowCtx) {
15330
16440
  suggestions.push(flowCtx);
15331
16441
  suggestions.push("");
@@ -15348,9 +16458,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15348
16458
  const hasOverlays = page.overlays.some((o) => o.blocksInteraction);
15349
16459
  if (hasOverlays) {
15350
16460
  suggestions.push("⚠ BLOCKING OVERLAY detected — dismiss it first:");
15351
- suggestions.push(
15352
- " → vessel_dismiss_popup or vessel_click on close/accept button"
15353
- );
16461
+ suggestions.push(" → vessel_clear_overlays for stacked modals");
16462
+ suggestions.push(" → or vessel_dismiss_popup for a single popup");
15354
16463
  suggestions.push("");
15355
16464
  }
15356
16465
  if (hasPasswordField) {
@@ -15426,7 +16535,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15426
16535
  const tab = tabManager.getActiveTab();
15427
16536
  if (!tab) return asTextResponse("Error: No active tab");
15428
16537
  return withAction(
15429
- runtime,
16538
+ runtime2,
15430
16539
  tabManager,
15431
16540
  "fill_form",
15432
16541
  { fieldCount: fields.length, submit },
@@ -15483,7 +16592,7 @@ ${results.join("\n")}`;
15483
16592
  const tab = tabManager.getActiveTab();
15484
16593
  if (!tab) return asTextResponse("Error: No active tab");
15485
16594
  return withAction(
15486
- runtime,
16595
+ runtime2,
15487
16596
  tabManager,
15488
16597
  "login",
15489
16598
  { url, username: username.slice(0, 3) + "***" },
@@ -15588,7 +16697,7 @@ ${steps.join("\n")}`;
15588
16697
  `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`
15589
16698
  );
15590
16699
  }
15591
- return withAction(runtime, tabManager, "search", { query }, async () => {
16700
+ return withAction(runtime2, tabManager, "search", { query }, async () => {
15592
16701
  const wc = tab.view.webContents;
15593
16702
  const searchSel = selector || await wc.executeJavaScript(`
15594
16703
  (function() {
@@ -15642,7 +16751,7 @@ ${steps.join("\n")}`;
15642
16751
  const tab = tabManager.getActiveTab();
15643
16752
  if (!tab) return asTextResponse("Error: No active tab");
15644
16753
  return withAction(
15645
- runtime,
16754
+ runtime2,
15646
16755
  tabManager,
15647
16756
  "paginate",
15648
16757
  { direction },
@@ -15693,7 +16802,7 @@ ${steps.join("\n")}`;
15693
16802
  const tab = tabManager.getActiveTab();
15694
16803
  if (!tab) return asTextResponse("Error: No active tab");
15695
16804
  return withAction(
15696
- runtime,
16805
+ runtime2,
15697
16806
  tabManager,
15698
16807
  "vessel_accept_cookies",
15699
16808
  {},
@@ -15751,7 +16860,7 @@ ${steps.join("\n")}`;
15751
16860
  const tab = tabManager.getActiveTab();
15752
16861
  if (!tab) return asTextResponse("Error: No active tab");
15753
16862
  return withAction(
15754
- runtime,
16863
+ runtime2,
15755
16864
  tabManager,
15756
16865
  "vessel_extract_table",
15757
16866
  { index, selector: rawSelector },
@@ -15806,7 +16915,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
15806
16915
  const tab = tabManager.getActiveTab();
15807
16916
  if (!tab) return asTextResponse("Error: No active tab");
15808
16917
  return withAction(
15809
- runtime,
16918
+ runtime2,
15810
16919
  tabManager,
15811
16920
  "vessel_scroll_to_element",
15812
16921
  { index, selector: rawSelector, position },
@@ -15864,7 +16973,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
15864
16973
  const tab = tabManager.getActiveTab();
15865
16974
  if (!tab) return asTextResponse("Error: No active tab");
15866
16975
  return withAction(
15867
- runtime,
16976
+ runtime2,
15868
16977
  tabManager,
15869
16978
  "vessel_wait_for_navigation",
15870
16979
  { timeoutMs },
@@ -15915,7 +17024,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
15915
17024
  inputSchema: zod.z.object({})
15916
17025
  },
15917
17026
  async () => {
15918
- const m = runtime.getMetrics();
17027
+ const m = runtime2.getMetrics();
15919
17028
  const lines = [
15920
17029
  `Session Metrics:`,
15921
17030
  ` Total actions: ${m.totalActions}`,
@@ -16075,16 +17184,16 @@ async function resolveSelector(wc, index, selector) {
16075
17184
  `
16076
17185
  );
16077
17186
  }
16078
- function createMcpServer(tabManager, runtime) {
17187
+ function createMcpServer(tabManager, runtime2) {
16079
17188
  const server = new mcp_js.McpServer({
16080
17189
  name: "vessel-browser",
16081
17190
  version: "0.1.0"
16082
17191
  });
16083
- registerTools(server, tabManager, runtime);
16084
- registerDevTools(server, tabManager, runtime);
17192
+ registerTools(server, tabManager, runtime2);
17193
+ registerDevTools(server, tabManager, runtime2);
16085
17194
  return server;
16086
17195
  }
16087
- function startMcpServer(tabManager, runtime, port) {
17196
+ function startMcpServer(tabManager, runtime2, port) {
16088
17197
  setMcpHealth({
16089
17198
  configuredPort: port,
16090
17199
  activePort: null,
@@ -16092,6 +17201,7 @@ function startMcpServer(tabManager, runtime, port) {
16092
17201
  status: "starting",
16093
17202
  message: `Starting MCP server on port ${port}.`
16094
17203
  });
17204
+ mcpAuthToken = crypto$1.randomBytes(32).toString("hex");
16095
17205
  return new Promise((resolve) => {
16096
17206
  const server = http.createServer(async (req, res) => {
16097
17207
  const url = new URL(req.url || "/", `http://localhost:${port}`);
@@ -16100,22 +17210,28 @@ function startMcpServer(tabManager, runtime, port) {
16100
17210
  res.end("Not found");
16101
17211
  return;
16102
17212
  }
16103
- res.setHeader("Access-Control-Allow-Origin", "*");
17213
+ res.setHeader("Access-Control-Allow-Origin", "null");
16104
17214
  res.setHeader(
16105
17215
  "Access-Control-Allow-Methods",
16106
17216
  "POST, GET, DELETE, OPTIONS"
16107
17217
  );
16108
17218
  res.setHeader(
16109
17219
  "Access-Control-Allow-Headers",
16110
- "Content-Type, mcp-session-id"
17220
+ "Content-Type, mcp-session-id, Authorization"
16111
17221
  );
16112
17222
  if (req.method === "OPTIONS") {
16113
17223
  res.writeHead(204);
16114
17224
  res.end();
16115
17225
  return;
16116
17226
  }
17227
+ const authHeader = req.headers.authorization;
17228
+ if (!authHeader || authHeader !== `Bearer ${mcpAuthToken}`) {
17229
+ res.writeHead(401, { "Content-Type": "application/json" });
17230
+ res.end(JSON.stringify({ error: "Unauthorized — missing or invalid bearer token" }));
17231
+ return;
17232
+ }
16117
17233
  try {
16118
- const mcpServer = createMcpServer(tabManager, runtime);
17234
+ const mcpServer = createMcpServer(tabManager, runtime2);
16119
17235
  const transport = new streamableHttp_js.StreamableHTTPServerTransport({
16120
17236
  sessionIdGenerator: void 0
16121
17237
  });
@@ -16157,6 +17273,7 @@ function startMcpServer(tabManager, runtime, port) {
16157
17273
  configuredPort: port,
16158
17274
  activePort: null,
16159
17275
  endpoint: null,
17276
+ authToken: null,
16160
17277
  error: message
16161
17278
  });
16162
17279
  });
@@ -16173,11 +17290,13 @@ function startMcpServer(tabManager, runtime, port) {
16173
17290
  message: `MCP server listening on ${endpoint}.`
16174
17291
  });
16175
17292
  console.log(`[Vessel MCP] Server listening on ${endpoint}`);
17293
+ console.log(`[Vessel MCP] Auth token: ${mcpAuthToken}`);
16176
17294
  finish({
16177
17295
  ok: true,
16178
17296
  configuredPort: port,
16179
17297
  activePort: actualPort,
16180
- endpoint
17298
+ endpoint,
17299
+ authToken: mcpAuthToken
16181
17300
  });
16182
17301
  });
16183
17302
  });
@@ -16196,6 +17315,7 @@ function stopMcpServer() {
16196
17315
  }
16197
17316
  const server = httpServer;
16198
17317
  httpServer = null;
17318
+ mcpAuthToken = null;
16199
17319
  server.close(() => {
16200
17320
  setMcpHealth({
16201
17321
  activePort: null,
@@ -16209,14 +17329,14 @@ function stopMcpServer() {
16209
17329
  });
16210
17330
  }
16211
17331
  let activeChatProvider = null;
16212
- function registerIpcHandlers(windowState, runtime) {
17332
+ function registerIpcHandlers(windowState, runtime2) {
16213
17333
  const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
16214
17334
  const sendToRendererViews = (channel, ...args) => {
16215
17335
  chromeView.webContents.send(channel, ...args);
16216
17336
  sidebarView.webContents.send(channel, ...args);
16217
17337
  devtoolsPanelView.webContents.send(channel, ...args);
16218
17338
  };
16219
- runtime.setUpdateListener((state2) => {
17339
+ runtime2.setUpdateListener((state2) => {
16220
17340
  sendToRendererViews(Channels.AGENT_RUNTIME_UPDATE, state2);
16221
17341
  });
16222
17342
  electron.ipcMain.handle(Channels.TAB_CREATE, (_, url) => {
@@ -16266,7 +17386,7 @@ function registerIpcHandlers(windowState, runtime) {
16266
17386
  (chunk) => sendToRendererViews(Channels.AI_STREAM_CHUNK, chunk),
16267
17387
  () => sendToRendererViews(Channels.AI_STREAM_END),
16268
17388
  tabManager,
16269
- runtime,
17389
+ runtime2,
16270
17390
  history
16271
17391
  );
16272
17392
  } catch (err) {
@@ -16349,46 +17469,50 @@ function registerIpcHandlers(windowState, runtime) {
16349
17469
  });
16350
17470
  electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
16351
17471
  electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key, value) => {
16352
- const updatedSettings = setSetting(key, value);
17472
+ if (!SETTABLE_KEYS.has(key)) {
17473
+ throw new Error(`Unknown setting key: ${key}`);
17474
+ }
17475
+ const settingsKey = key;
17476
+ const updatedSettings = setSetting(settingsKey, value);
16353
17477
  if (key === "approvalMode") {
16354
- runtime.setApprovalMode(value);
17478
+ runtime2.setApprovalMode(value);
16355
17479
  }
16356
17480
  if (key === "mcpPort") {
16357
17481
  await stopMcpServer();
16358
- await startMcpServer(tabManager, runtime, updatedSettings.mcpPort);
17482
+ await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
16359
17483
  }
16360
17484
  sendToRendererViews(Channels.SETTINGS_UPDATE, updatedSettings);
16361
17485
  return updatedSettings;
16362
17486
  });
16363
- electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime.getState());
16364
- electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime.pause());
16365
- electron.ipcMain.handle(Channels.AGENT_RESUME, () => runtime.resume());
17487
+ electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime2.getState());
17488
+ electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime2.pause());
17489
+ electron.ipcMain.handle(Channels.AGENT_RESUME, () => runtime2.resume());
16366
17490
  electron.ipcMain.handle(
16367
17491
  Channels.AGENT_SET_APPROVAL_MODE,
16368
17492
  (_, mode) => {
16369
17493
  setSetting("approvalMode", mode);
16370
- return runtime.setApprovalMode(mode);
17494
+ return runtime2.setApprovalMode(mode);
16371
17495
  }
16372
17496
  );
16373
17497
  electron.ipcMain.handle(
16374
17498
  Channels.AGENT_APPROVAL_RESOLVE,
16375
- (_, approvalId, approved) => runtime.resolveApproval(approvalId, approved)
17499
+ (_, approvalId, approved) => runtime2.resolveApproval(approvalId, approved)
16376
17500
  );
16377
17501
  electron.ipcMain.handle(
16378
17502
  Channels.AGENT_CHECKPOINT_CREATE,
16379
- (_, name, note) => runtime.createCheckpoint(name, note)
17503
+ (_, name, note) => runtime2.createCheckpoint(name, note)
16380
17504
  );
16381
17505
  electron.ipcMain.handle(
16382
17506
  Channels.AGENT_CHECKPOINT_RESTORE,
16383
- (_, checkpointId) => runtime.restoreCheckpoint(checkpointId)
17507
+ (_, checkpointId) => runtime2.restoreCheckpoint(checkpointId)
16384
17508
  );
16385
17509
  electron.ipcMain.handle(
16386
17510
  Channels.AGENT_SESSION_CAPTURE,
16387
- (_, note) => runtime.captureSession(note)
17511
+ (_, note) => runtime2.captureSession(note)
16388
17512
  );
16389
17513
  electron.ipcMain.handle(
16390
17514
  Channels.AGENT_SESSION_RESTORE,
16391
- (_, snapshot) => runtime.restoreSession(snapshot)
17515
+ (_, snapshot) => runtime2.restoreSession(snapshot)
16392
17516
  );
16393
17517
  electron.ipcMain.handle(Channels.BOOKMARKS_GET, () => {
16394
17518
  return getState();
@@ -16422,36 +17546,12 @@ function registerIpcHandlers(windowState, runtime) {
16422
17546
  return { success: false, message: "No active tab" };
16423
17547
  }
16424
17548
  const wc = activeTab.view.webContents;
16425
- if (wc.isDestroyed()) {
16426
- return { success: false, message: "Tab is not available" };
16427
- }
16428
- const url = wc.getURL();
16429
- if (!url || url === "about:blank") {
16430
- return { success: false, message: "No page loaded" };
16431
- }
16432
- const selectedText = await wc.executeJavaScript(`
16433
- (function() {
16434
- var sel = window.getSelection();
16435
- return sel ? sel.toString().trim() : '';
16436
- })()
16437
- `);
16438
- if (!selectedText) {
16439
- return { success: false, message: "No text selected" };
17549
+ const result = await captureSelectionHighlight(wc);
17550
+ if (result.success && result.text) {
17551
+ await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(() => {
17552
+ });
16440
17553
  }
16441
- const capped = selectedText.length > 5e3 ? selectedText.slice(0, 5e3) : selectedText;
16442
- const highlight = addHighlight(
16443
- url,
16444
- void 0,
16445
- capped,
16446
- void 0,
16447
- "yellow",
16448
- "user"
16449
- );
16450
- await highlightOnPage(wc, null, capped, void 0, void 0, "yellow").catch(
16451
- () => {
16452
- }
16453
- );
16454
- return { success: true, text: capped, id: highlight.id };
17554
+ return result;
16455
17555
  } catch {
16456
17556
  return { success: false, message: "Could not capture selection" };
16457
17557
  }
@@ -16467,24 +17567,11 @@ function registerIpcHandlers(windowState, runtime) {
16467
17567
  if (wc.isDestroyed()) return;
16468
17568
  const tab = tabManager.findTabByWebContentsId(wc.id);
16469
17569
  if (!tab || !tab.highlightModeActive) return;
16470
- const url = wc.getURL();
16471
- if (!url || url === "about:blank") return;
16472
- const capped = text.length > 5e3 ? text.slice(0, 5e3) : text;
16473
- const highlight = addHighlight(
16474
- url,
16475
- void 0,
16476
- capped,
16477
- void 0,
16478
- "yellow",
16479
- "user"
16480
- );
16481
- if (!chromeView.webContents.isDestroyed()) {
16482
- chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, {
16483
- success: true,
16484
- text: capped,
16485
- id: highlight.id
16486
- });
16487
- }
17570
+ void persistAndMarkHighlight(wc, text).then((result) => {
17571
+ if (result.success && !chromeView.webContents.isDestroyed()) {
17572
+ chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, result);
17573
+ }
17574
+ });
16488
17575
  } catch {
16489
17576
  }
16490
17577
  });
@@ -16494,9 +17581,7 @@ function registerIpcHandlers(windowState, runtime) {
16494
17581
  const wc = tab.view.webContents;
16495
17582
  if (wc.isDestroyed()) return 0;
16496
17583
  try {
16497
- return wc.executeJavaScript(
16498
- `document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text').length`
16499
- );
17584
+ return getHighlightCount(wc);
16500
17585
  } catch {
16501
17586
  return 0;
16502
17587
  }
@@ -16507,20 +17592,7 @@ function registerIpcHandlers(windowState, runtime) {
16507
17592
  const wc = tab.view.webContents;
16508
17593
  if (wc.isDestroyed()) return false;
16509
17594
  try {
16510
- return wc.executeJavaScript(`
16511
- (function() {
16512
- var highlights = document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text');
16513
- if (${index} < 0 || ${index} >= highlights.length) return false;
16514
- // Remove focus ring from all highlights
16515
- highlights.forEach(function(h) { h.style.removeProperty('outline'); h.style.removeProperty('outline-offset'); });
16516
- var target = highlights[${index}];
16517
- target.scrollIntoView({ behavior: 'smooth', block: 'center' });
16518
- // Add focus ring to current highlight
16519
- target.style.setProperty('outline', '2px solid rgba(255, 255, 255, 0.9)', 'important');
16520
- target.style.setProperty('outline-offset', '2px', 'important');
16521
- return true;
16522
- })()
16523
- `);
17595
+ return scrollToHighlight(wc, index);
16524
17596
  } catch {
16525
17597
  return false;
16526
17598
  }
@@ -16531,32 +17603,7 @@ function registerIpcHandlers(windowState, runtime) {
16531
17603
  const wc = tab.view.webContents;
16532
17604
  if (wc.isDestroyed()) return false;
16533
17605
  try {
16534
- return wc.executeJavaScript(`
16535
- (function() {
16536
- var highlights = document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text');
16537
- if (${index} < 0 || ${index} >= highlights.length) return false;
16538
- var el = highlights[${index}];
16539
- // Remove associated label if any
16540
- document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) {
16541
- if (b.__vesselAnchor === el) b.remove();
16542
- });
16543
- // Unwrap text highlights, remove class from element highlights
16544
- if (el.tagName === 'MARK' && el.classList.contains('__vessel-highlight-text')) {
16545
- var parent = el.parentNode;
16546
- while (el.firstChild) parent.insertBefore(el.firstChild, el);
16547
- parent.removeChild(el);
16548
- parent.normalize();
16549
- } else {
16550
- el.classList.remove('__vessel-highlight');
16551
- el.style.removeProperty('background');
16552
- el.style.removeProperty('outline-color');
16553
- el.style.removeProperty('box-shadow');
16554
- el.style.removeProperty('outline');
16555
- el.style.removeProperty('outline-offset');
16556
- }
16557
- return true;
16558
- })()
16559
- `);
17606
+ return removeHighlightAtIndex(wc, index);
16560
17607
  } catch {
16561
17608
  return false;
16562
17609
  }
@@ -16567,39 +17614,65 @@ function registerIpcHandlers(windowState, runtime) {
16567
17614
  const wc = tab.view.webContents;
16568
17615
  if (wc.isDestroyed()) return false;
16569
17616
  try {
16570
- return wc.executeJavaScript(`
16571
- (function() {
16572
- // Remove all labels
16573
- document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) { b.remove(); });
16574
- // Unwrap text highlights
16575
- document.querySelectorAll('.__vessel-highlight-text').forEach(function(mark) {
16576
- var parent = mark.parentNode;
16577
- while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
16578
- parent.removeChild(mark);
16579
- parent.normalize();
16580
- });
16581
- // Remove element highlights
16582
- document.querySelectorAll('.__vessel-highlight').forEach(function(el) {
16583
- el.classList.remove('__vessel-highlight');
16584
- el.style.removeProperty('background');
16585
- el.style.removeProperty('outline-color');
16586
- el.style.removeProperty('box-shadow');
16587
- el.style.removeProperty('outline');
16588
- el.style.removeProperty('outline-offset');
16589
- });
16590
- return true;
16591
- })()
16592
- `);
17617
+ return clearAllHighlightElements(wc);
16593
17618
  } catch {
16594
17619
  return false;
16595
17620
  }
16596
17621
  });
17622
+ let findWiredWcId = null;
17623
+ function wireFindEvents(wc) {
17624
+ if (findWiredWcId === wc.id) return;
17625
+ if (findWiredWcId !== null) {
17626
+ const prev = tabManager.findTabByWebContentsId(findWiredWcId);
17627
+ if (prev) prev.view.webContents.removeAllListeners("found-in-page");
17628
+ }
17629
+ findWiredWcId = wc.id;
17630
+ wc.on("found-in-page", (_event, result) => {
17631
+ if (!chromeView.webContents.isDestroyed()) {
17632
+ chromeView.webContents.send(Channels.FIND_IN_PAGE_RESULT, result);
17633
+ }
17634
+ });
17635
+ }
17636
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_START, (_, text, options) => {
17637
+ const tab = tabManager.getActiveTab();
17638
+ if (!tab) return null;
17639
+ const wc = tab.view.webContents;
17640
+ if (wc.isDestroyed()) return null;
17641
+ wireFindEvents(wc);
17642
+ return wc.findInPage(text, {
17643
+ forward: options?.forward ?? true,
17644
+ findNext: options?.findNext ?? false
17645
+ });
17646
+ });
17647
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_NEXT, (_, forward) => {
17648
+ const tab = tabManager.getActiveTab();
17649
+ if (!tab) return null;
17650
+ const wc = tab.view.webContents;
17651
+ if (wc.isDestroyed()) return null;
17652
+ return wc.findInPage("", { forward: forward ?? true, findNext: true });
17653
+ });
17654
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_STOP, (_, action) => {
17655
+ const tab = tabManager.getActiveTab();
17656
+ if (!tab) return;
17657
+ const wc = tab.view.webContents;
17658
+ if (wc.isDestroyed()) return;
17659
+ wc.stopFindInPage(action ?? "clearSelection");
17660
+ });
17661
+ electron.ipcMain.handle(Channels.HISTORY_GET, () => {
17662
+ return getState$1();
17663
+ });
17664
+ electron.ipcMain.handle(Channels.HISTORY_SEARCH, (_, query) => {
17665
+ return search(query);
17666
+ });
17667
+ electron.ipcMain.handle(Channels.HISTORY_CLEAR, () => {
17668
+ clearAll$1();
17669
+ });
16597
17670
  electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => {
16598
17671
  windowState.uiState.devtoolsPanelOpen = !windowState.uiState.devtoolsPanelOpen;
16599
17672
  layoutViews(windowState);
16600
17673
  return { open: windowState.uiState.devtoolsPanelOpen };
16601
17674
  });
16602
- electron.ipcMain.handle("devtools-panel:resize", (_, height) => {
17675
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE, (_, height) => {
16603
17676
  const clamped = Math.max(MIN_DEVTOOLS_PANEL, Math.min(MAX_DEVTOOLS_PANEL, Math.round(height)));
16604
17677
  windowState.uiState.devtoolsPanelHeight = clamped;
16605
17678
  layoutViews(windowState);
@@ -16620,6 +17693,7 @@ function registerIpcHandlers(windowState, runtime) {
16620
17693
  });
16621
17694
  }
16622
17695
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
17696
+ const PERSIST_DEBOUNCE_MS = 500;
16623
17697
  function clone(value) {
16624
17698
  return JSON.parse(JSON.stringify(value));
16625
17699
  }
@@ -16697,7 +17771,7 @@ class AgentRuntime {
16697
17771
  createCheckpoint(name, note) {
16698
17772
  const snapshot = this.captureSession(note);
16699
17773
  const checkpoint = {
16700
- id: node_crypto.randomUUID(),
17774
+ id: crypto$1.randomUUID(),
16701
17775
  name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
16702
17776
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
16703
17777
  note: note?.trim() || void 0,
@@ -16751,7 +17825,7 @@ class AgentRuntime {
16751
17825
  }
16752
17826
  }
16753
17827
  const entry = {
16754
- id: node_crypto.randomUUID(),
17828
+ id: crypto$1.randomUUID(),
16755
17829
  source: input.source,
16756
17830
  kind,
16757
17831
  title: input.title?.trim() || void 0,
@@ -16775,7 +17849,7 @@ class AgentRuntime {
16775
17849
  // --- Speedee Flow State ---
16776
17850
  startFlow(goal, steps, startUrl) {
16777
17851
  const flow = {
16778
- id: node_crypto.randomUUID(),
17852
+ id: crypto$1.randomUUID(),
16779
17853
  goal,
16780
17854
  steps: steps.map((label) => ({ label, status: "pending" })),
16781
17855
  currentStepIndex: 0,
@@ -16961,7 +18035,14 @@ ${progress}
16961
18035
  return sanitizePersistence(null);
16962
18036
  }
16963
18037
  }
16964
- persist() {
18038
+ persistTimer = null;
18039
+ persistDirty = false;
18040
+ persistNow() {
18041
+ this.persistDirty = false;
18042
+ if (this.persistTimer) {
18043
+ clearTimeout(this.persistTimer);
18044
+ this.persistTimer = null;
18045
+ }
16965
18046
  const persisted = {
16966
18047
  session: this.state.session,
16967
18048
  supervisor: {
@@ -16972,20 +18053,36 @@ ${progress}
16972
18053
  actions: this.state.actions.slice(-120),
16973
18054
  checkpoints: this.state.checkpoints.slice(-20)
16974
18055
  };
16975
- fs$1.mkdirSync(path$1.dirname(getRuntimeStatePath()), { recursive: true });
16976
- fs$1.writeFileSync(
16977
- getRuntimeStatePath(),
16978
- JSON.stringify(persisted, null, 2),
16979
- "utf-8"
16980
- );
18056
+ try {
18057
+ fs$1.mkdirSync(path$1.dirname(getRuntimeStatePath()), { recursive: true });
18058
+ fs$1.writeFileSync(
18059
+ getRuntimeStatePath(),
18060
+ JSON.stringify(persisted, null, 2),
18061
+ "utf-8"
18062
+ );
18063
+ } catch (err) {
18064
+ console.error("[Vessel] Failed to persist runtime state:", err);
18065
+ }
18066
+ }
18067
+ schedulePersist() {
18068
+ this.persistDirty = true;
18069
+ if (this.persistTimer) return;
18070
+ this.persistTimer = setTimeout(() => {
18071
+ this.persistTimer = null;
18072
+ if (this.persistDirty) this.persistNow();
18073
+ }, PERSIST_DEBOUNCE_MS);
18074
+ }
18075
+ /** Flush any pending debounced persist to disk immediately. Call on shutdown. */
18076
+ flushPersist() {
18077
+ if (this.persistDirty) this.persistNow();
16981
18078
  }
16982
18079
  emit() {
16983
- this.persist();
18080
+ this.schedulePersist();
16984
18081
  this.updateListener?.(this.getState());
16985
18082
  }
16986
18083
  startAction(input) {
16987
18084
  const action = {
16988
- id: node_crypto.randomUUID(),
18085
+ id: crypto$1.randomUUID(),
16989
18086
  source: input.source,
16990
18087
  name: input.name,
16991
18088
  args: clone(input.args),
@@ -17019,7 +18116,7 @@ ${progress}
17019
18116
  /** Aggregate metrics for all completed actions in this session. */
17020
18117
  getMetrics() {
17021
18118
  const completed = this.state.actions.filter((a) => a.status === "completed");
17022
- const failed = this.state.actions.filter((a) => a.status === "error");
18119
+ const failed = this.state.actions.filter((a) => a.status === "failed");
17023
18120
  const durations = completed.filter((a) => a.durationMs != null).map((a) => a.durationMs);
17024
18121
  const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
17025
18122
  const toolBreakdown = {};
@@ -17057,7 +18154,7 @@ ${progress}
17057
18154
  }
17058
18155
  awaitApproval(action, reason) {
17059
18156
  const approval = {
17060
- id: node_crypto.randomUUID(),
18157
+ id: crypto$1.randomUUID(),
17061
18158
  actionId: action.id,
17062
18159
  source: action.source,
17063
18160
  name: action.name,
@@ -17183,6 +18280,41 @@ function installAdBlocking(tabManager) {
17183
18280
  callback({ cancel: shouldBlockRequest(details) });
17184
18281
  });
17185
18282
  }
18283
+ function installDownloadHandler(chromeView) {
18284
+ electron.session.defaultSession.on("will-download", (_event, item) => {
18285
+ const settings2 = loadSettings();
18286
+ const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
18287
+ const filename = item.getFilename();
18288
+ const savePath = path.join(downloadDir, filename);
18289
+ item.setSavePath(savePath);
18290
+ const info = {
18291
+ filename,
18292
+ savePath,
18293
+ totalBytes: item.getTotalBytes(),
18294
+ receivedBytes: 0,
18295
+ state: "progressing"
18296
+ };
18297
+ if (!chromeView.webContents.isDestroyed()) {
18298
+ chromeView.webContents.send(Channels.DOWNLOAD_STARTED, info);
18299
+ }
18300
+ item.on("updated", (_event2, state2) => {
18301
+ info.receivedBytes = item.getReceivedBytes();
18302
+ info.totalBytes = item.getTotalBytes();
18303
+ info.state = state2 === "progressing" ? "progressing" : "interrupted";
18304
+ if (!chromeView.webContents.isDestroyed()) {
18305
+ chromeView.webContents.send(Channels.DOWNLOAD_PROGRESS, info);
18306
+ }
18307
+ });
18308
+ item.once("done", (_event2, state2) => {
18309
+ info.receivedBytes = item.getReceivedBytes();
18310
+ info.state = state2 === "completed" ? "completed" : "cancelled";
18311
+ if (!chromeView.webContents.isDestroyed()) {
18312
+ chromeView.webContents.send(Channels.DOWNLOAD_DONE, info);
18313
+ }
18314
+ });
18315
+ });
18316
+ }
18317
+ let runtime = null;
17186
18318
  function rendererUrlFor(view) {
17187
18319
  if (!process.env.ELECTRON_RENDERER_URL) return null;
17188
18320
  const url = new URL(process.env.ELECTRON_RENDERER_URL);
@@ -17263,7 +18395,6 @@ async function bootstrap() {
17263
18395
  if (settings2.clearBookmarksOnLaunch) {
17264
18396
  clearAll();
17265
18397
  }
17266
- let runtime = null;
17267
18398
  const windowState = createMainWindow((tabs, activeId) => {
17268
18399
  windowState.chromeView.webContents.send(
17269
18400
  Channels.TAB_STATE_UPDATE,
@@ -17285,15 +18416,10 @@ async function bootstrap() {
17285
18416
  const registerHighlightShortcut = () => {
17286
18417
  electron.globalShortcut.unregister("CommandOrControl+H");
17287
18418
  const success = electron.globalShortcut.register("CommandOrControl+H", () => {
17288
- console.log("[Vessel] Ctrl+H shortcut triggered");
17289
18419
  const activeTab = tabManager.getActiveTab();
17290
- if (!activeTab) {
17291
- console.log("[Vessel] No active tab");
17292
- return;
17293
- }
18420
+ if (!activeTab) return;
17294
18421
  tabManager.captureHighlightFromActiveTab();
17295
18422
  });
17296
- console.log("[Vessel] Ctrl+H shortcut registered:", success);
17297
18423
  if (!success) {
17298
18424
  console.warn("[Vessel] Failed to register Ctrl+H shortcut");
17299
18425
  }
@@ -17319,6 +18445,11 @@ async function bootstrap() {
17319
18445
  chromeView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
17320
18446
  sidebarView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
17321
18447
  });
18448
+ subscribe$1((state2) => {
18449
+ chromeView.webContents.send(Channels.HISTORY_UPDATE, state2);
18450
+ sidebarView.webContents.send(Channels.HISTORY_UPDATE, state2);
18451
+ });
18452
+ installDownloadHandler(chromeView);
17322
18453
  const chromeUrl = rendererUrlFor("chrome");
17323
18454
  const sidebarUrl = rendererUrlFor("sidebar");
17324
18455
  const devtoolsUrl = rendererUrlFor("devtools");
@@ -17358,6 +18489,7 @@ electron.app.whenReady().then(bootstrap).catch((error) => {
17358
18489
  });
17359
18490
  electron.app.on("window-all-closed", () => {
17360
18491
  electron.globalShortcut.unregisterAll();
18492
+ runtime?.flushPersist();
17361
18493
  void stopMcpServer().finally(() => {
17362
18494
  electron.app.quit();
17363
18495
  });