@quanta-intellect/vessel-browser 0.1.14 → 0.1.16

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
  }
@@ -824,6 +949,172 @@ async function highlightOnPage(wc, resolvedSelector, text, label, durationMs, co
824
949
  }
825
950
  return "Error: No element or text to highlight";
826
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
+ }
827
1118
  async function clearHighlights(wc) {
828
1119
  return wc.executeJavaScript(`
829
1120
  (function() {
@@ -848,6 +1139,134 @@ async function clearHighlights(wc) {
848
1139
  })()
849
1140
  `);
850
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
+ }
851
1270
  const MAX_CONSOLE_ENTRIES = 500;
852
1271
  const MAX_NETWORK_ENTRIES = 200;
853
1272
  const MAX_ERROR_ENTRIES = 200;
@@ -1572,7 +1991,10 @@ class TabManager {
1572
1991
  onOpenUrl: ({ url: requestedUrl, background: background2, adBlockingEnabled }) => {
1573
1992
  this.createTab(requestedUrl, { background: background2, adBlockingEnabled });
1574
1993
  },
1575
- onPageLoad: (pageUrl, wc) => this.reapplyHighlights(pageUrl, wc),
1994
+ onPageLoad: (pageUrl, wc) => {
1995
+ this.reapplyHighlights(pageUrl, wc);
1996
+ addEntry(pageUrl, wc.getTitle());
1997
+ },
1576
1998
  onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
1577
1999
  onHighlightRemove: (url2, text) => this.removeHighlightByText(url2, text),
1578
2000
  onHighlightRecolor: (url2, text, color) => this.recolorHighlightByText(url2, text, color)
@@ -1724,16 +2146,14 @@ class TabManager {
1724
2146
  if (last && last.url === normalized && now - last.at < 500) return;
1725
2147
  this.lastReapply.set(wcId, { url: normalized, at: now });
1726
2148
  const highlights = getHighlightsForUrl(url);
1727
- for (const h of highlights) {
1728
- if (!h.selector && !h.text) continue;
1729
- void highlightOnPage(
1730
- wc,
1731
- h.selector ?? null,
1732
- h.text,
1733
- h.label,
1734
- void 0,
1735
- h.color
1736
- ).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(() => {
1737
2157
  });
1738
2158
  }
1739
2159
  }
@@ -1741,62 +2161,23 @@ class TabManager {
1741
2161
  this.highlightCaptureCallback = callback;
1742
2162
  }
1743
2163
  captureHighlightFromActiveTab() {
1744
- console.log("[Vessel] captureHighlightFromActiveTab called");
1745
2164
  const activeTab = this.getActiveTab();
1746
2165
  if (!activeTab) {
1747
- console.log("[Vessel] No active tab in captureHighlightFromActiveTab");
1748
2166
  return { success: false, message: "No active tab" };
1749
2167
  }
1750
2168
  const wc = activeTab.view.webContents;
1751
- console.log("[Vessel] Calling captureHighlightFromPage for:", wc.getURL());
1752
2169
  this.captureHighlightFromPage(wc);
1753
2170
  return null;
1754
2171
  }
1755
2172
  captureHighlightFromPage(wc) {
1756
- console.log("[Vessel] captureHighlightFromPage called");
1757
2173
  void (async () => {
1758
2174
  try {
1759
- if (wc.isDestroyed()) {
1760
- console.log("[Vessel] WebContents destroyed");
1761
- return;
1762
- }
1763
- const url = wc.getURL();
1764
- console.log("[Vessel] URL:", url);
1765
- if (!url || url === "about:blank") {
1766
- console.log("[Vessel] No URL or about:blank");
1767
- 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
+ });
1768
2179
  }
1769
- const selectedText = await wc.executeJavaScript(`
1770
- (function() {
1771
- var sel = window.getSelection();
1772
- return sel ? sel.toString().trim() : '';
1773
- })()
1774
- `);
1775
- console.log("[Vessel] Selected text:", selectedText?.slice(0, 50));
1776
- if (!selectedText) return;
1777
- const capped = selectedText.length > 5e3 ? selectedText.slice(0, 5e3) : selectedText;
1778
- const highlight = addHighlight(
1779
- url,
1780
- void 0,
1781
- capped,
1782
- void 0,
1783
- "yellow",
1784
- "user"
1785
- );
1786
- await highlightOnPage(
1787
- wc,
1788
- null,
1789
- capped,
1790
- void 0,
1791
- void 0,
1792
- "yellow"
1793
- ).catch(() => {
1794
- });
1795
- this.highlightCaptureCallback?.({
1796
- success: true,
1797
- text: capped,
1798
- id: highlight.id
1799
- });
2180
+ this.highlightCaptureCallback?.(result);
1800
2181
  } catch {
1801
2182
  this.highlightCaptureCallback?.({
1802
2183
  success: false,
@@ -1882,84 +2263,7 @@ class TabManager {
1882
2263
  broadcastState() {
1883
2264
  const states = this.getAllStates();
1884
2265
  this.onStateChange(states, this.activeTabId || "");
1885
- }
1886
- }
1887
- const defaults = {
1888
- defaultUrl: "https://start.duckduckgo.com",
1889
- theme: "dark",
1890
- sidebarWidth: 340,
1891
- mcpPort: 3100,
1892
- autoRestoreSession: true,
1893
- clearBookmarksOnLaunch: false,
1894
- obsidianVaultPath: "",
1895
- approvalMode: "confirm-dangerous",
1896
- agentTranscriptMode: "summary",
1897
- chatProvider: null,
1898
- maxToolIterations: 200
1899
- };
1900
- let settings = null;
1901
- let settingsIssues = [];
1902
- function getSettingsPath() {
1903
- return path.join(electron.app.getPath("userData"), "vessel-settings.json");
1904
- }
1905
- function getSettingsLoadIssues() {
1906
- return settingsIssues.map((issue) => ({ ...issue }));
1907
- }
1908
- function sanitizePort(value) {
1909
- const parsed = Number(value);
1910
- if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) {
1911
- return parsed;
1912
- }
1913
- settingsIssues.push({
1914
- code: "settings-invalid-mcp-port",
1915
- severity: "warning",
1916
- title: "Invalid MCP port in settings",
1917
- detail: `Expected an integer between 1 and 65535 but found ${JSON.stringify(value)}.`,
1918
- action: `Using default port ${defaults.mcpPort} instead.`
1919
- });
1920
- return defaults.mcpPort;
1921
- }
1922
- function loadSettings() {
1923
- if (settings) return settings;
1924
- settingsIssues = [];
1925
- try {
1926
- const raw = fs.readFileSync(getSettingsPath(), "utf-8");
1927
- const parsed = JSON.parse(raw);
1928
- delete parsed.apiKey;
1929
- delete parsed.provider;
1930
- settings = {
1931
- ...defaults,
1932
- ...parsed,
1933
- mcpPort: sanitizePort(parsed.mcpPort ?? defaults.mcpPort),
1934
- agentTranscriptMode: parsed.agentTranscriptMode === "off" || parsed.agentTranscriptMode === "summary" || parsed.agentTranscriptMode === "full" ? parsed.agentTranscriptMode : parsed.showAgentTranscript === false ? "off" : defaults.agentTranscriptMode
1935
- };
1936
- } catch (error) {
1937
- if (fs.existsSync(getSettingsPath())) {
1938
- settingsIssues.push({
1939
- code: "settings-read-failed",
1940
- severity: "warning",
1941
- title: "Could not read Vessel settings",
1942
- detail: error instanceof Error ? error.message : "Unknown settings error.",
1943
- action: "Falling back to built-in defaults for this launch."
1944
- });
1945
- }
1946
- settings = { ...defaults };
1947
- }
1948
- return settings;
1949
- }
1950
- function saveSettings() {
1951
- fs.mkdirSync(path.dirname(getSettingsPath()), { recursive: true });
1952
- fs.writeFileSync(getSettingsPath(), JSON.stringify(settings, null, 2));
1953
- }
1954
- function setSetting(key, value) {
1955
- loadSettings();
1956
- if (key === "mcpPort") {
1957
- settings.mcpPort = sanitizePort(value);
1958
- } else {
1959
- settings[key] = value;
1960
- }
1961
- saveSettings();
1962
- return { ...settings };
2266
+ }
1963
2267
  }
1964
2268
  const Channels = {
1965
2269
  // Tab management
@@ -2025,6 +2329,21 @@ const Channels = {
2025
2329
  // DevTools panel
2026
2330
  DEVTOOLS_PANEL_TOGGLE: "devtools-panel:toggle",
2027
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",
2028
2347
  // Window controls
2029
2348
  WINDOW_MINIMIZE: "window:minimize",
2030
2349
  WINDOW_MAXIMIZE: "window:maximize",
@@ -3146,6 +3465,16 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3146
3465
  "dialog, [role='dialog'], [role='alertdialog'], [aria-modal='true']"
3147
3466
  ).forEach(function(el) { candidates.add(el); });
3148
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
+
3149
3478
  // Fixed/sticky elements are the other overlay category — walk only
3150
3479
  // direct children of body and high-level containers (depth ≤ 3)
3151
3480
  // since real overlays are almost always near the top of the DOM tree.
@@ -3173,13 +3502,23 @@ const DIRECT_EXTRACTION_SCRIPT = String.raw`
3173
3502
  var cartConfirm = !dialogLike && !drawerLike && isPositioned(style) &&
3174
3503
  rect.width >= 160 && rect.height >= 100 &&
3175
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
+ })();
3176
3512
  var blocksInteraction = dialogLike ||
3177
3513
  drawerLike ||
3178
3514
  cartConfirm ||
3179
3515
  ((style.position === "fixed" || style.position === "sticky") &&
3180
3516
  parseZIndex(style) >= 10 &&
3181
3517
  areaRatio >= 0.3 &&
3182
- coversViewportCenter(rect));
3518
+ coversViewportCenter(rect)) ||
3519
+ (bodyLocked &&
3520
+ (style.position === "fixed" || style.position === "sticky") &&
3521
+ areaRatio >= 0.2);
3183
3522
 
3184
3523
  if (!blocksInteraction && type !== "dialog" && type !== "modal") return;
3185
3524
 
@@ -4006,9 +4345,12 @@ function generateReaderHTML(page) {
4006
4345
  function escapeHtml(str) {
4007
4346
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4008
4347
  }
4009
- let mcpStatusChangeListener = null;
4348
+ const mcpStatusChangeListeners = /* @__PURE__ */ new Set();
4010
4349
  function onMcpStatusChange(listener) {
4011
- mcpStatusChangeListener = listener;
4350
+ mcpStatusChangeListeners.add(listener);
4351
+ return () => {
4352
+ mcpStatusChangeListeners.delete(listener);
4353
+ };
4012
4354
  }
4013
4355
  function getMcpStatus() {
4014
4356
  return state$1.mcp.status;
@@ -4059,9 +4401,24 @@ function setMcpHealth(update) {
4059
4401
  state$1.mcp.status = update.status;
4060
4402
  state$1.mcp.message = update.message;
4061
4403
  if (prevStatus !== state$1.mcp.status) {
4062
- mcpStatusChangeListener?.(state$1.mcp.status);
4404
+ for (const listener of mcpStatusChangeListeners) {
4405
+ listener(state$1.mcp.status);
4406
+ }
4063
4407
  }
4064
4408
  }
4409
+ function isRichToolResult(value) {
4410
+ return typeof value === "object" && value !== null && value.__richResult === true;
4411
+ }
4412
+ function makeImageResult(base64, description, mediaType = "image/png") {
4413
+ const result = {
4414
+ __richResult: true,
4415
+ content: [
4416
+ { type: "text", text: description },
4417
+ { type: "image", mediaType, base64 }
4418
+ ]
4419
+ };
4420
+ return JSON.stringify(result);
4421
+ }
4065
4422
  const DEFAULT_MAX_ITERATIONS$1 = 200;
4066
4423
  class AnthropicProvider {
4067
4424
  client;
@@ -4093,7 +4450,7 @@ class AnthropicProvider {
4093
4450
  }
4094
4451
  }
4095
4452
  } catch (err) {
4096
- if (err.name !== "AbortError") {
4453
+ if (err instanceof Error && err.name !== "AbortError") {
4097
4454
  onChunk(`
4098
4455
 
4099
4456
  [Error: ${err.message}]`);
@@ -4116,7 +4473,6 @@ class AnthropicProvider {
4116
4473
  iterationsUsed = i + 1;
4117
4474
  const msgTokenEstimate = JSON.stringify(messages).length;
4118
4475
  const sysTokenEstimate = systemPrompt.length;
4119
- console.log(`[Vessel Agent] iteration=${i} messages=${messages.length} msgChars=${msgTokenEstimate} sysChars=${sysTokenEstimate} tools=${tools.length}`);
4120
4476
  const streamStartTime = Date.now();
4121
4477
  const stream = this.client.messages.stream(
4122
4478
  {
@@ -4178,9 +4534,7 @@ class AnthropicProvider {
4178
4534
  } finally {
4179
4535
  if (idleTimer) clearTimeout(idleTimer);
4180
4536
  }
4181
- console.log(`[Vessel Agent] stream complete in ${Date.now() - streamStartTime}ms, toolCalls=${toolUseBlocks.length} textLen=${textContent.length}`);
4182
4537
  const finalMessage = await stream.finalMessage();
4183
- console.log(`[Vessel Agent] finalMessage received, stop_reason=${finalMessage.stop_reason}`);
4184
4538
  const assistantContent = [];
4185
4539
  if (textContent) {
4186
4540
  assistantContent.push({ type: "text", text: textContent });
@@ -4204,19 +4558,43 @@ class AnthropicProvider {
4204
4558
  <<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
4205
4559
  `);
4206
4560
  let result;
4207
- const toolStartTime = Date.now();
4208
- console.log(`[Vessel Agent] executing tool: ${tb.name}`);
4209
4561
  try {
4210
4562
  result = await onToolCall(tb.name, tb.input);
4211
4563
  } catch (toolErr) {
4212
- result = `Error: Tool execution failed — ${toolErr.message || toolErr}. Try a different approach or call read_page to refresh context.`;
4564
+ const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
4565
+ result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
4566
+ }
4567
+ let parsedRich = null;
4568
+ try {
4569
+ const parsed = JSON.parse(result);
4570
+ if (isRichToolResult(parsed)) parsedRich = parsed;
4571
+ } catch {
4572
+ }
4573
+ if (parsedRich) {
4574
+ toolResults.push({
4575
+ type: "tool_result",
4576
+ tool_use_id: tb.id,
4577
+ content: parsedRich.content.map((block) => {
4578
+ if (block.type === "image") {
4579
+ return {
4580
+ type: "image",
4581
+ source: {
4582
+ type: "base64",
4583
+ media_type: block.mediaType,
4584
+ data: block.base64
4585
+ }
4586
+ };
4587
+ }
4588
+ return { type: "text", text: block.text };
4589
+ })
4590
+ });
4591
+ } else {
4592
+ toolResults.push({
4593
+ type: "tool_result",
4594
+ tool_use_id: tb.id,
4595
+ content: result
4596
+ });
4213
4597
  }
4214
- console.log(`[Vessel Agent] tool ${tb.name} completed in ${Date.now() - toolStartTime}ms, resultLen=${result.length}`);
4215
- toolResults.push({
4216
- type: "tool_result",
4217
- tool_use_id: tb.id,
4218
- content: result
4219
- });
4220
4598
  }
4221
4599
  messages.push({ role: "user", content: toolResults });
4222
4600
  }
@@ -4226,7 +4604,7 @@ class AnthropicProvider {
4226
4604
  [Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
4227
4605
  }
4228
4606
  } catch (err) {
4229
- if (err.name !== "AbortError") {
4607
+ if (err instanceof Error && err.name !== "AbortError") {
4230
4608
  onChunk(`
4231
4609
 
4232
4610
  [Error: ${err.message}]`);
@@ -4377,7 +4755,7 @@ class OpenAICompatProvider {
4377
4755
  }
4378
4756
  }
4379
4757
  } catch (err) {
4380
- if (err.name !== "AbortError") {
4758
+ if (err instanceof Error && err.name !== "AbortError") {
4381
4759
  onChunk(`
4382
4760
 
4383
4761
  [Error: ${err.message}]`);
@@ -4401,7 +4779,6 @@ class OpenAICompatProvider {
4401
4779
  for (let i = 0; i < maxIterations; i++) {
4402
4780
  iterationsUsed = i + 1;
4403
4781
  const msgTokenEstimate = JSON.stringify(messages).length;
4404
- console.log(`[Vessel Agent OpenAI] iteration=${i} messages=${messages.length} msgChars=${msgTokenEstimate} tools=${openAITools.length}`);
4405
4782
  const streamStartTime = Date.now();
4406
4783
  let textAccum = "";
4407
4784
  const toolCallAccums = {};
@@ -4437,7 +4814,6 @@ class OpenAICompatProvider {
4437
4814
  }
4438
4815
  }
4439
4816
  }
4440
- console.log(`[Vessel Agent OpenAI] stream complete in ${Date.now() - streamStartTime}ms, toolCalls=${Object.keys(toolCallAccums).length} textLen=${textAccum.length} finishReason=${finishReason}`);
4441
4817
  const toolCalls = Object.values(toolCallAccums);
4442
4818
  for (const tc of Object.values(toolCallAccums)) {
4443
4819
  if (!tc.id) tc.id = `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
@@ -4482,18 +4858,24 @@ class OpenAICompatProvider {
4482
4858
  <<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
4483
4859
  `);
4484
4860
  let result;
4485
- const toolStartTime = Date.now();
4486
- console.log(`[Vessel Agent OpenAI] executing tool: ${tc.name}`);
4487
4861
  try {
4488
4862
  result = await onToolCall(tc.name, args);
4489
4863
  } catch (toolErr) {
4490
- result = `Error: Tool execution failed — ${toolErr.message || toolErr}. Try a different approach or call read_page to refresh context.`;
4864
+ const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
4865
+ result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
4866
+ }
4867
+ let toolContent = result;
4868
+ try {
4869
+ const parsed = JSON.parse(result);
4870
+ if (isRichToolResult(parsed)) {
4871
+ toolContent = parsed.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
4872
+ }
4873
+ } catch {
4491
4874
  }
4492
- console.log(`[Vessel Agent OpenAI] tool ${tc.name} completed in ${Date.now() - toolStartTime}ms, resultLen=${result.length}`);
4493
4875
  messages.push({
4494
4876
  role: "tool",
4495
4877
  tool_call_id: tc.id,
4496
- content: result
4878
+ content: toolContent
4497
4879
  });
4498
4880
  }
4499
4881
  }
@@ -4503,7 +4885,7 @@ class OpenAICompatProvider {
4503
4885
  [Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
4504
4886
  }
4505
4887
  } catch (err) {
4506
- if (err.name !== "AbortError") {
4888
+ if (err instanceof Error && err.name !== "AbortError") {
4507
4889
  onChunk(`
4508
4890
 
4509
4891
  [Error: ${err.message}]`);
@@ -6356,6 +6738,7 @@ const TOOL_DEFINITIONS = [
6356
6738
  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.",
6357
6739
  inputSchema: {
6358
6740
  mode: zod.z.enum([
6741
+ "glance",
6359
6742
  "summary",
6360
6743
  "interactives_only",
6361
6744
  "forms_only",
@@ -6365,11 +6748,18 @@ const TOOL_DEFINITIONS = [
6365
6748
  "full",
6366
6749
  "debug"
6367
6750
  ]).optional().describe(
6368
- "Read mode: visible_only/results_only/forms_only/summary/text_only for narrow reads, full/debug for the complete page dump"
6751
+ "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"
6369
6752
  )
6370
6753
  },
6371
6754
  tier: 0
6372
6755
  },
6756
+ {
6757
+ name: "screenshot",
6758
+ title: "Screenshot",
6759
+ description: "Take a screenshot of the current page — see exactly what the user sees. Returns the image for visual analysis. Use when you need to verify visual layout, check what's actually rendered on screen, or when text extraction fails on heavy pages.",
6760
+ inputSchema: {},
6761
+ tier: 1
6762
+ },
6373
6763
  {
6374
6764
  name: "wait_for",
6375
6765
  title: "Wait For",
@@ -6805,6 +7195,7 @@ const ALWAYS_FAST_TOOL_NAMES = /* @__PURE__ */ new Set([
6805
7195
  "accept_cookies",
6806
7196
  "wait_for",
6807
7197
  "read_page",
7198
+ "screenshot",
6808
7199
  "inspect_element"
6809
7200
  ]);
6810
7201
  function inferIntent(query) {
@@ -7069,8 +7460,12 @@ function load() {
7069
7460
  return state;
7070
7461
  }
7071
7462
  function save() {
7072
- fs.mkdirSync(path.dirname(getBookmarksPath()), { recursive: true });
7073
- fs.writeFileSync(getBookmarksPath(), JSON.stringify(state, null, 2), "utf-8");
7463
+ try {
7464
+ fs.mkdirSync(path.dirname(getBookmarksPath()), { recursive: true });
7465
+ fs.writeFileSync(getBookmarksPath(), JSON.stringify(state, null, 2), "utf-8");
7466
+ } catch (err) {
7467
+ console.error("[Vessel] Failed to save bookmarks:", err);
7468
+ }
7074
7469
  }
7075
7470
  function emit() {
7076
7471
  if (!state) return;
@@ -7496,6 +7891,39 @@ function formatDeadLinkMessage(label, result) {
7496
7891
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
7497
7892
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
7498
7893
  }
7894
+ const ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
7895
+ function isSafeNavigationURL(url) {
7896
+ try {
7897
+ const parsed = new URL(url);
7898
+ return ALLOWED_SCHEMES.has(parsed.protocol);
7899
+ } catch {
7900
+ return false;
7901
+ }
7902
+ }
7903
+ function assertSafeURL(url) {
7904
+ if (!isSafeNavigationURL(url)) {
7905
+ throw new Error(
7906
+ `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`
7907
+ );
7908
+ }
7909
+ }
7910
+ async function captureScreenshot(wc) {
7911
+ for (let attempt = 0; attempt < 3; attempt += 1) {
7912
+ await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
7913
+ try {
7914
+ const image = await wc.capturePage();
7915
+ if (!image.isEmpty()) {
7916
+ const size = image.getSize();
7917
+ const base64 = image.toPNG().toString("base64");
7918
+ if (base64) {
7919
+ return { ok: true, base64, width: size.width, height: size.height };
7920
+ }
7921
+ }
7922
+ } catch {
7923
+ }
7924
+ }
7925
+ return { ok: false, error: "Page image was empty after 3 attempts" };
7926
+ }
7499
7927
  const SESSION_VERSION = 1;
7500
7928
  function getSessionsDir() {
7501
7929
  return path$1.join(electron.app.getPath("userData"), "named-sessions");
@@ -7515,7 +7943,7 @@ function normalizeSessionName(name) {
7515
7943
  function sessionFileName(name) {
7516
7944
  const normalized = normalizeSessionName(name).toLowerCase();
7517
7945
  const slug = normalized.replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "session";
7518
- const hash = node_crypto.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
7946
+ const hash = crypto$1.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
7519
7947
  return `${slug}-${hash}.json`;
7520
7948
  }
7521
7949
  function getSessionPath(name) {
@@ -7780,10 +8208,148 @@ const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
7780
8208
  function pageBusyError(action) {
7781
8209
  return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
7782
8210
  }
8211
+ async function glanceExtract(wc) {
8212
+ const startMs = Date.now();
8213
+ const result = await executePageScript(
8214
+ wc,
8215
+ `(function() {
8216
+ var vw = window.innerWidth || document.documentElement.clientWidth || 0;
8217
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
8218
+ var sy = window.scrollY || window.pageYOffset || 0;
8219
+
8220
+ function inViewport(el) {
8221
+ var r = el.getBoundingClientRect();
8222
+ return r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw && r.width > 0 && r.height > 0;
8223
+ }
8224
+
8225
+ function label(el) {
8226
+ return (el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 120);
8227
+ }
8228
+
8229
+ // Headings visible on screen
8230
+ var headings = [];
8231
+ document.querySelectorAll('h1, h2, h3, h4').forEach(function(h) {
8232
+ if (!inViewport(h)) return;
8233
+ var t = (h.textContent || '').trim();
8234
+ if (t && t.length < 200) headings.push(h.tagName.toLowerCase() + ': ' + t);
8235
+ });
8236
+
8237
+ // Links visible on screen (deduplicated by text)
8238
+ var links = [];
8239
+ var seenLinks = {};
8240
+ var idx = 1;
8241
+ document.querySelectorAll('a[href]').forEach(function(a) {
8242
+ if (!inViewport(a)) return;
8243
+ var t = (a.textContent || '').trim().slice(0, 100);
8244
+ if (!t || t.length < 2 || seenLinks[t]) return;
8245
+ seenLinks[t] = true;
8246
+ links.push({ text: t, href: (a.href || '').slice(0, 200), index: idx++ });
8247
+ });
8248
+
8249
+ // Buttons visible on screen
8250
+ var buttons = [];
8251
+ document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]').forEach(function(b) {
8252
+ if (!inViewport(b)) return;
8253
+ var t = label(b);
8254
+ if (!t || t.length < 1) return;
8255
+ buttons.push({ text: t, index: idx++ });
8256
+ });
8257
+
8258
+ // Input fields visible on screen
8259
+ var inputs = [];
8260
+ document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), select, textarea').forEach(function(inp) {
8261
+ if (!inViewport(inp)) return;
8262
+ var type = (inp.type || inp.tagName.toLowerCase() || '').toLowerCase();
8263
+ var lbl = (inp.getAttribute('aria-label') || inp.getAttribute('placeholder') || inp.name || '').trim();
8264
+ inputs.push({ type: type, label: lbl.slice(0, 80), placeholder: (inp.getAttribute('placeholder') || '').slice(0, 80), index: idx++ });
8265
+ });
8266
+
8267
+ // Content snapshot from main content area using textContent (instant, no reflow)
8268
+ var roots = ['main', 'article', '[role="main"]', '#content', '.content', '.story-body'];
8269
+ var contentRoot = null;
8270
+ for (var i = 0; i < roots.length; i++) {
8271
+ contentRoot = document.querySelector(roots[i]);
8272
+ if (contentRoot && contentRoot.textContent.trim().length > 50) break;
8273
+ contentRoot = null;
8274
+ }
8275
+ var snippet = '';
8276
+ if (contentRoot) {
8277
+ snippet = contentRoot.textContent.replace(/[ \\t]+/g, ' ').replace(/(\\n\\s*){3,}/g, '\\n\\n').trim().slice(0, 8000);
8278
+ } else {
8279
+ // Fallback: grab text from visible elements only
8280
+ var parts = [];
8281
+ document.querySelectorAll('h1, h2, h3, p, li, td, span, div').forEach(function(el) {
8282
+ if (parts.length > 100 || !inViewport(el)) return;
8283
+ var t = (el.textContent || '').trim();
8284
+ if (t.length > 10 && t.length < 500) parts.push(t);
8285
+ });
8286
+ snippet = parts.join('\\n').slice(0, 8000);
8287
+ }
8288
+
8289
+ return {
8290
+ title: document.title || '',
8291
+ url: location.href,
8292
+ headings: headings.slice(0, 20),
8293
+ links: links.slice(0, 40),
8294
+ buttons: buttons.slice(0, 20),
8295
+ inputs: inputs.slice(0, 15),
8296
+ contentSnippet: snippet,
8297
+ viewportHeight: vh,
8298
+ viewportWidth: vw,
8299
+ scrollY: Math.round(sy),
8300
+ };
8301
+ })()`,
8302
+ { timeoutMs: 2500, label: "glance-extract" }
8303
+ );
8304
+ const elapsed = Date.now() - startMs;
8305
+ if (!result || result === PAGE_SCRIPT_TIMEOUT) {
8306
+ return [
8307
+ `# ${wc.getTitle() || "(untitled)"}`,
8308
+ `URL: ${wc.getURL()}`,
8309
+ "",
8310
+ "[read_page mode=glance — page JS thread is completely blocked, no content available]",
8311
+ "[Try: click or type_text to interact directly, or wait a few seconds and retry]"
8312
+ ].join("\n");
8313
+ }
8314
+ const sections = [
8315
+ `# ${result.title}`,
8316
+ `URL: ${result.url}`,
8317
+ `Viewport: ${result.viewportWidth}×${result.viewportHeight} scrollY=${result.scrollY}`,
8318
+ `[read_page mode=glance — ${elapsed}ms, showing what's visible on screen]`
8319
+ ];
8320
+ if (result.headings.length > 0) {
8321
+ sections.push("", "## Headings", ...result.headings);
8322
+ }
8323
+ if (result.inputs.length > 0) {
8324
+ sections.push("", "## Input Fields");
8325
+ for (const inp of result.inputs) {
8326
+ const desc = inp.label || inp.placeholder || inp.type;
8327
+ sections.push(` [#${inp.index}] ${inp.type}: ${desc}`);
8328
+ }
8329
+ }
8330
+ if (result.buttons.length > 0) {
8331
+ sections.push("", "## Buttons");
8332
+ for (const btn of result.buttons) {
8333
+ sections.push(` [#${btn.index}] ${btn.text}`);
8334
+ }
8335
+ }
8336
+ if (result.links.length > 0) {
8337
+ sections.push("", "## Visible Links");
8338
+ for (const link of result.links) {
8339
+ sections.push(` [#${link.index}] ${link.text}`);
8340
+ }
8341
+ }
8342
+ if (result.contentSnippet) {
8343
+ const truncated = result.contentSnippet.length > 6e3 ? result.contentSnippet.slice(0, 6e3) + "\n[truncated]" : result.contentSnippet;
8344
+ sections.push("", "## Page Content (viewport)", "", truncated);
8345
+ }
8346
+ return sections.join("\n");
8347
+ }
7783
8348
  function normalizeReadPageMode(mode, pageContent) {
7784
8349
  if (typeof mode === "string") {
7785
8350
  const normalized = mode.trim().toLowerCase();
7786
8351
  if (normalized === "debug") return "debug";
8352
+ if (normalized === "glance") return "glance";
7787
8353
  if (normalized === "full" || normalized === "summary" || normalized === "interactives_only" || normalized === "forms_only" || normalized === "text_only" || normalized === "visible_only" || normalized === "results_only") {
7788
8354
  return normalized;
7789
8355
  }
@@ -7805,9 +8371,6 @@ async function executePageScript(wc, script, options) {
7805
8371
  })
7806
8372
  ]);
7807
8373
  if (result === PAGE_SCRIPT_TIMEOUT) {
7808
- console.log(
7809
- `[Vessel pageScript] timed out after ${timeoutMs}ms (${options?.label || "page-script"})`
7810
- );
7811
8374
  return PAGE_SCRIPT_TIMEOUT;
7812
8375
  }
7813
8376
  return result;
@@ -7822,9 +8385,6 @@ async function executePageScript(wc, script, options) {
7822
8385
  function waitForLoad$1(wc, timeout = 5e3) {
7823
8386
  return new Promise((resolve) => {
7824
8387
  let finished = false;
7825
- console.log(
7826
- `[Vessel waitForLoad] started, isLoading=${wc.isLoading()}, timeout=${timeout}`
7827
- );
7828
8388
  const cleanup = () => {
7829
8389
  wc.removeListener("did-finish-load", onLoadEvent);
7830
8390
  wc.removeListener("did-stop-loading", onLoadEvent);
@@ -7833,23 +8393,19 @@ function waitForLoad$1(wc, timeout = 5e3) {
7833
8393
  const finish = (reason) => {
7834
8394
  if (finished) return;
7835
8395
  finished = true;
7836
- console.log(`[Vessel waitForLoad] finished: ${reason}`);
7837
8396
  clearTimeout(timer);
7838
8397
  cleanup();
7839
8398
  resolve();
7840
8399
  };
7841
8400
  const onLoadEvent = () => {
7842
8401
  const loading = wc.isLoading();
7843
- console.log(
7844
- `[Vessel waitForLoad] load event fired, isLoading=${loading}`
7845
- );
7846
8402
  if (!loading) {
7847
- finish("load event");
8403
+ finish();
7848
8404
  }
7849
8405
  };
7850
- const timer = setTimeout(() => finish("timeout"), timeout);
8406
+ const timer = setTimeout(() => finish(), timeout);
7851
8407
  if (!wc.isLoading()) {
7852
- finish("already loaded");
8408
+ finish();
7853
8409
  return;
7854
8410
  }
7855
8411
  wc.on("did-finish-load", onLoadEvent);
@@ -7911,10 +8467,62 @@ function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
7911
8467
  wc.on("page-title-updated", onNativeChange);
7912
8468
  });
7913
8469
  }
7914
- function getPostNavSummary(wc) {
8470
+ async function getPostNavSummary(wc) {
7915
8471
  const title = wc.getTitle();
7916
- return title ? `
8472
+ const titleLine = title ? `
7917
8473
  Page title: ${title}` : "";
8474
+ const overlaySignal = await executePageScript(
8475
+ wc,
8476
+ `(function() {
8477
+ var signals = [];
8478
+ // Body scroll lock is a strong overlay signal
8479
+ var bodyStyle = window.getComputedStyle(document.body);
8480
+ var htmlStyle = window.getComputedStyle(document.documentElement);
8481
+ if (bodyStyle.overflow === 'hidden' || htmlStyle.overflow === 'hidden') {
8482
+ signals.push('body-scroll-locked');
8483
+ }
8484
+ // Check for known consent manager containers
8485
+ var consentSelectors = [
8486
+ '#onetrust-consent-sdk', '#CybotCookiebotDialog', '[class*="consent-banner"]',
8487
+ '[class*="cookie-banner"]', '[class*="privacy-banner"]', '[id*="consent"]',
8488
+ '[class*="gdpr"]', '[data-testid*="consent"]', '[data-testid*="cookie"]',
8489
+ '.fc-consent-root', '#sp_message_container_', '[id*="trustarc"]',
8490
+ '[class*="cmp-"]', '[id*="cmp-"]'
8491
+ ];
8492
+ for (var i = 0; i < consentSelectors.length; i++) {
8493
+ try {
8494
+ var el = document.querySelector(consentSelectors[i]);
8495
+ if (el && el.offsetHeight > 50) {
8496
+ signals.push('consent-banner:' + consentSelectors[i]);
8497
+ break;
8498
+ }
8499
+ } catch(e) {}
8500
+ }
8501
+ // Check for large fixed/sticky elements covering viewport
8502
+ var vw = window.innerWidth || 0;
8503
+ var vh = window.innerHeight || 0;
8504
+ var vpArea = Math.max(1, vw * vh);
8505
+ var els = document.querySelectorAll('dialog[open], [role="dialog"], [aria-modal="true"]');
8506
+ if (els.length > 0) signals.push('dialog-open');
8507
+ if (signals.length === 0) {
8508
+ var fixed = document.querySelectorAll('div[style*="position: fixed"], div[style*="position:fixed"]');
8509
+ for (var j = 0; j < fixed.length && j < 20; j++) {
8510
+ var r = fixed[j].getBoundingClientRect();
8511
+ if ((r.width * r.height) / vpArea > 0.3) {
8512
+ signals.push('large-fixed-overlay');
8513
+ break;
8514
+ }
8515
+ }
8516
+ }
8517
+ return signals.length > 0 ? signals.join(', ') : null;
8518
+ })()`,
8519
+ { timeoutMs: 1500, label: "overlay-probe" }
8520
+ );
8521
+ if (overlaySignal && overlaySignal !== PAGE_SCRIPT_TIMEOUT) {
8522
+ return `${titleLine}
8523
+ WARNING: Blocking overlay detected (${overlaySignal}). Call clear_overlays or accept_cookies before reading the page.`;
8524
+ }
8525
+ return titleLine;
7918
8526
  }
7919
8527
  async function scrollPage$1(wc, deltaY) {
7920
8528
  const getScrollY = async () => {
@@ -8363,6 +8971,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
8363
8971
  }
8364
8972
  if (snapshot.url && snapshot.url !== wc.getURL()) {
8365
8973
  try {
8974
+ assertSafeURL(snapshot.url);
8366
8975
  await wc.loadURL(snapshot.url);
8367
8976
  await waitForLoad$1(wc, 3e3);
8368
8977
  return;
@@ -8485,19 +9094,12 @@ ${shadowOverlay}` : result;
8485
9094
  const elInfo = await describeElementForClick$1(wc, selector);
8486
9095
  if ("error" in elInfo) return `Error: ${elInfo.error}`;
8487
9096
  const cartMatch = isAddToCartText(elInfo.text);
8488
- console.log(
8489
- `[Vessel cart-guard] text="${elInfo.text}" cartMatch=${cartMatch} url=${beforeUrl} hasPrior=${recentCartClicks.has(beforeUrl)}`
8490
- );
8491
9097
  if (cartMatch && isDuplicateCartClick(beforeUrl, elInfo.text)) {
8492
- console.log(`[Vessel cart-guard] BLOCKED duplicate add-to-cart click`);
8493
9098
  return `Blocked: "${elInfo.text}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
8494
9099
  }
8495
9100
  if (!cartMatch && recentCartClicks.has(beforeUrl)) {
8496
9101
  const dialogActions = await getCartDialogActions$1(wc);
8497
9102
  if (dialogActions) {
8498
- console.log(
8499
- `[Vessel cart-guard] BLOCKED background click while cart dialog is open`
8500
- );
8501
9103
  return `Blocked: a cart confirmation dialog is open. Do not click background elements.
8502
9104
  ${dialogActions}
8503
9105
  Click one of these dialog actions instead.`;
@@ -8510,7 +9112,6 @@ Click one of these dialog actions instead.`;
8510
9112
  }
8511
9113
  }
8512
9114
  if (cartMatch) {
8513
- console.log(`[Vessel cart-guard] RECORDED cart click for url=${beforeUrl}`);
8514
9115
  recordCartClick(beforeUrl, elInfo.text);
8515
9116
  }
8516
9117
  const clickText = `Clicked: ${elInfo.text}`;
@@ -8750,9 +9351,6 @@ async function dismissPopup$1(wc) {
8750
9351
  { timeoutMs: 1500, label: "cart dialog continue shopping" }
8751
9352
  );
8752
9353
  if (continueResult && continueResult !== PAGE_SCRIPT_TIMEOUT && typeof continueResult === "string" && !continueResult.startsWith("Error")) {
8753
- console.log(
8754
- `[Vessel cart-guard] dismiss_popup auto-clicked dialog action: ${continueResult}`
8755
- );
8756
9354
  return `Cart confirmation handled: ${continueResult}. Item was already added to your cart.`;
8757
9355
  }
8758
9356
  const dialogActions = await getCartDialogActions$1(wc);
@@ -9007,6 +9605,71 @@ async function clickOverlayCandidate(wc, action) {
9007
9605
  const result = await clickResolvedSelector$1(wc, action.selector);
9008
9606
  return `${action.label || action.selector}: ${result}`;
9009
9607
  }
9608
+ async function tryDismissConsentIframe(wc) {
9609
+ try {
9610
+ const hasSignal = await executePageScript(
9611
+ wc,
9612
+ `(function() {
9613
+ var bs = window.getComputedStyle(document.body);
9614
+ var hs = window.getComputedStyle(document.documentElement);
9615
+ if (bs.overflow === 'hidden' || hs.overflow === 'hidden') return true;
9616
+ var sels = '#onetrust-consent-sdk, [class*="consent"], [class*="cookie-banner"], [id*="consent"], [id*="sp_message"], .fc-consent-root, [class*="cmp-"]';
9617
+ var el = document.querySelector(sels);
9618
+ return !!(el && el.offsetHeight > 20);
9619
+ })()`,
9620
+ { timeoutMs: 1e3, label: "iframe-consent-signal" }
9621
+ );
9622
+ if (!hasSignal || hasSignal === PAGE_SCRIPT_TIMEOUT) return null;
9623
+ const frames = wc.mainFrame.framesInSubtree;
9624
+ for (const frame of frames) {
9625
+ if (frame === wc.mainFrame) continue;
9626
+ try {
9627
+ const result = await frame.executeJavaScript(`
9628
+ (function() {
9629
+ var selectors = [
9630
+ 'button[title*="Accept"], button[title*="Agree"], button[title*="OK"]',
9631
+ '[class*="accept"], [class*="agree"], [class*="consent-accept"]',
9632
+ 'button[aria-label*="accept" i], button[aria-label*="agree" i]',
9633
+ '.sp_choice_type_11', '.message-component.message-button',
9634
+ ];
9635
+ // Try selectors first
9636
+ for (var i = 0; i < selectors.length; i++) {
9637
+ try {
9638
+ var els = document.querySelectorAll(selectors[i]);
9639
+ for (var j = 0; j < els.length; j++) {
9640
+ var el = els[j];
9641
+ if (!(el instanceof HTMLElement)) continue;
9642
+ var text = (el.textContent || '').trim().toLowerCase();
9643
+ if (/accept|agree|consent|got it|ok|continue|i understand/i.test(text) || el.offsetHeight > 0) {
9644
+ el.click();
9645
+ return 'Clicked iframe consent button: ' + text.slice(0, 60);
9646
+ }
9647
+ }
9648
+ } catch(e) {}
9649
+ }
9650
+ // Text-match fallback on all buttons
9651
+ var buttons = document.querySelectorAll('button, [role="button"], a.message-component');
9652
+ for (var k = 0; k < buttons.length; k++) {
9653
+ var btn = buttons[k];
9654
+ var label = (btn.textContent || '').trim().toLowerCase();
9655
+ if (/^(accept|agree|accept all|i agree|i accept|ok|got it|allow|continue|yes)$/i.test(label) ||
9656
+ /accept all|agree and|accept & continue|accept and continue/i.test(label)) {
9657
+ btn.click();
9658
+ return 'Clicked iframe consent button: ' + label.slice(0, 60);
9659
+ }
9660
+ }
9661
+ return null;
9662
+ })()
9663
+ `);
9664
+ if (result) return result;
9665
+ } catch {
9666
+ continue;
9667
+ }
9668
+ }
9669
+ } catch {
9670
+ }
9671
+ return null;
9672
+ }
9010
9673
  async function clearOverlays(wc, strategy = "auto") {
9011
9674
  const steps = [];
9012
9675
  let cleared = 0;
@@ -9018,7 +9681,15 @@ async function clearOverlays(wc, strategy = "auto") {
9018
9681
  (overlay2) => overlay2.blocksInteraction
9019
9682
  );
9020
9683
  if (blockingOverlays.length === 0) {
9021
- if (cleared === 0) return "No blocking overlays detected";
9684
+ if (cleared === 0) {
9685
+ const iframeResult = await tryDismissConsentIframe(wc);
9686
+ if (iframeResult) {
9687
+ steps.push(`Iframe consent: ${iframeResult}`);
9688
+ await sleep$1(500);
9689
+ return steps.join("\n");
9690
+ }
9691
+ return "No blocking overlays detected";
9692
+ }
9022
9693
  steps.push(`Overlays remaining: ${beforeState.total}`);
9023
9694
  steps.push("Page still blocked: false");
9024
9695
  return steps.join("\n");
@@ -9870,6 +10541,7 @@ async function submitForm$1(wc, args) {
9870
10541
  if (formInfo.params) {
9871
10542
  url.search = formInfo.params;
9872
10543
  }
10544
+ assertSafeURL(url.toString());
9873
10545
  wc.loadURL(url.toString());
9874
10546
  await waitForPotentialNavigation$1(wc, beforeUrl);
9875
10547
  const afterUrl = wc.getURL();
@@ -10039,6 +10711,7 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
10039
10711
  "dismiss_popup",
10040
10712
  "clear_overlays",
10041
10713
  "read_page",
10714
+ "screenshot",
10042
10715
  "wait_for",
10043
10716
  "create_checkpoint",
10044
10717
  "restore_checkpoint",
@@ -10118,6 +10791,19 @@ async function executeAction(name, args, ctx) {
10118
10791
  dangerous: isDangerousAction$1(name),
10119
10792
  executor: async () => {
10120
10793
  switch (name) {
10794
+ case "screenshot": {
10795
+ if (!wc) return "Error: No active tab";
10796
+ const screenshotStart = Date.now();
10797
+ const shot = await captureScreenshot(wc);
10798
+ if (!shot.ok) return `Error: ${shot.error}`;
10799
+ const screenshotMs = Date.now() - screenshotStart;
10800
+ const title = wc.getTitle() || "(untitled)";
10801
+ const url = wc.getURL();
10802
+ return makeImageResult(
10803
+ shot.base64,
10804
+ `Screenshot of "${title}" (${url}) — ${shot.width}x${shot.height}, captured in ${screenshotMs}ms. Analyze the image to understand the current visual state of the page.`
10805
+ );
10806
+ }
10121
10807
  case "current_tab": {
10122
10808
  const active = ctx.tabManager.getActiveTab();
10123
10809
  const activeId = ctx.tabManager.getActiveTabId();
@@ -10164,7 +10850,7 @@ async function executeAction(name, args, ctx) {
10164
10850
  const created = ctx.tabManager.getActiveTab();
10165
10851
  if (created) {
10166
10852
  await waitForLoad$1(created.view.webContents);
10167
- return `Created tab ${createdId}${getPostNavSummary(created.view.webContents)}`;
10853
+ return `Created tab ${createdId}${await getPostNavSummary(created.view.webContents)}`;
10168
10854
  }
10169
10855
  return `Created tab ${createdId}`;
10170
10856
  }
@@ -10176,7 +10862,7 @@ async function executeAction(name, args, ctx) {
10176
10862
  }
10177
10863
  ctx.tabManager.navigateTab(tabId, args.url);
10178
10864
  await waitForLoad$1(wc);
10179
- return `Navigated to ${wc.getURL()}${getPostNavSummary(wc)}`;
10865
+ return `Navigated to ${wc.getURL()}${await getPostNavSummary(wc)}`;
10180
10866
  }
10181
10867
  case "go_back": {
10182
10868
  if (!tab || !wc || !tabId) return "Error: No active tab";
@@ -10187,7 +10873,7 @@ async function executeAction(name, args, ctx) {
10187
10873
  ctx.tabManager.goBack(tabId);
10188
10874
  await waitForLoad$1(wc);
10189
10875
  const afterUrl = wc.getURL();
10190
- return afterUrl !== beforeUrl ? `Went back to ${afterUrl}${getPostNavSummary(wc)}` : `Back action completed but page stayed on ${afterUrl}`;
10876
+ return afterUrl !== beforeUrl ? `Went back to ${afterUrl}${await getPostNavSummary(wc)}` : `Back action completed but page stayed on ${afterUrl}`;
10191
10877
  }
10192
10878
  case "go_forward": {
10193
10879
  if (!tab || !wc || !tabId) return "Error: No active tab";
@@ -10198,7 +10884,7 @@ async function executeAction(name, args, ctx) {
10198
10884
  ctx.tabManager.goForward(tabId);
10199
10885
  await waitForLoad$1(wc);
10200
10886
  const afterUrl = wc.getURL();
10201
- return afterUrl !== beforeUrl ? `Went forward to ${afterUrl}${getPostNavSummary(wc)}` : `Forward action completed but page stayed on ${afterUrl}`;
10887
+ return afterUrl !== beforeUrl ? `Went forward to ${afterUrl}${await getPostNavSummary(wc)}` : `Forward action completed but page stayed on ${afterUrl}`;
10202
10888
  }
10203
10889
  case "reload": {
10204
10890
  if (!wc || !tabId) return "Error: No active tab";
@@ -10317,14 +11003,16 @@ async function executeAction(name, args, ctx) {
10317
11003
  }
10318
11004
  case "read_page": {
10319
11005
  if (!wc) return "Error: No active tab";
10320
- console.log("[Vessel read_page] starting extraction with 6s timeout");
11006
+ const requestedGlance = typeof args.mode === "string" && args.mode.trim().toLowerCase() === "glance";
11007
+ if (requestedGlance) {
11008
+ return glanceExtract(wc);
11009
+ }
10321
11010
  let content = null;
10322
11011
  try {
10323
11012
  content = await Promise.race([
10324
11013
  extractContent(wc),
10325
11014
  new Promise(
10326
11015
  (resolve) => setTimeout(() => {
10327
- console.log("[Vessel read_page] timeout fired, falling back");
10328
11016
  resolve(null);
10329
11017
  }, 6e3)
10330
11018
  )
@@ -10332,10 +11020,27 @@ async function executeAction(name, args, ctx) {
10332
11020
  } catch {
10333
11021
  content = null;
10334
11022
  }
10335
- console.log(
10336
- `[Vessel read_page] extraction result: ${content ? `content=${content.content.length}` : "null (timeout)"}`
10337
- );
10338
- if (content) {
11023
+ if (!content || content.content.length === 0) {
11024
+ try {
11025
+ const iframeResult = await Promise.race([
11026
+ tryDismissConsentIframe(wc),
11027
+ new Promise((resolve) => setTimeout(() => resolve(null), 2e3))
11028
+ ]);
11029
+ if (iframeResult) {
11030
+ await sleep$1(500);
11031
+ try {
11032
+ content = await Promise.race([
11033
+ extractContent(wc),
11034
+ new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
11035
+ ]);
11036
+ } catch {
11037
+ content = null;
11038
+ }
11039
+ }
11040
+ } catch {
11041
+ }
11042
+ }
11043
+ if (content && content.content.length > 0) {
10339
11044
  const liveSelectionSection = formatLiveSelectionSection(
10340
11045
  await captureLiveHighlightSnapshot(
10341
11046
  wc,
@@ -10367,16 +11072,7 @@ ${truncated}`;
10367
11072
  `Need more detail? Escalate with read_page(mode="debug") only if the narrow modes are insufficient.`
10368
11073
  ].filter(Boolean).join("\n\n");
10369
11074
  }
10370
- const title = wc.getTitle() || "(untitled)";
10371
- const url = wc.getURL();
10372
- return [
10373
- `# ${title}`,
10374
- `URL: ${url}`,
10375
- "",
10376
- "[Page content extraction timed out — the page JS thread is busy.]",
10377
- "[Use the search tool to search the site, or type_text/click to interact directly.]",
10378
- "[You can retry read_page in a few seconds once the page finishes loading.]"
10379
- ].join("\n");
11075
+ return glanceExtract(wc);
10380
11076
  }
10381
11077
  case "wait_for": {
10382
11078
  if (!wc) return "Error: No active tab";
@@ -11078,6 +11774,7 @@ ${steps.join("\n")}`;
11078
11774
  try {
11079
11775
  const url = new URL(searchInfo.formAction);
11080
11776
  url.searchParams.set(searchInfo.inputName || "q", query);
11777
+ assertSafeURL(url.toString());
11081
11778
  wc.loadURL(url.toString());
11082
11779
  await waitForPotentialNavigation$1(wc, beforeUrl);
11083
11780
  afterUrl = wc.getURL();
@@ -11147,9 +11844,20 @@ ${steps.join("\n")}`;
11147
11844
  '[aria-label="Accept cookies"]',
11148
11845
  '[aria-label="Accept all cookies"]',
11149
11846
  '[data-testid="cookie-accept"]',
11847
+ // CNN / WarnerMedia / common consent SDKs
11848
+ '[data-testid="consent-accept"]',
11849
+ '[data-testid="accept-all"]',
11850
+ 'button[class*="consent"][class*="accept"]',
11851
+ 'button[class*="privacy"][class*="accept"]',
11852
+ '.fc-cta-consent',
11853
+ '#sp_choice_button_accept',
11854
+ '.message-component.message-button.no-children.focusable.sp_choice_type_11',
11855
+ '[class*="truste"] [class*="accept"]',
11856
+ '[id*="consent-accept"]',
11857
+ '[class*="cmp-accept"]',
11150
11858
  ];
11151
11859
  // Also try text-matching on buttons
11152
- var textPatterns = ['accept all', 'accept cookies', 'allow all', 'allow cookies', 'agree', 'got it', 'ok', 'i agree', 'consent'];
11860
+ 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'];
11153
11861
  for (var i = 0; i < selectors.length; i++) {
11154
11862
  var el = document.querySelector(selectors[i]);
11155
11863
  if (el && el instanceof HTMLElement) { el.click(); return "Dismissed cookie banner via: " + selectors[i]; }
@@ -11175,7 +11883,10 @@ ${steps.join("\n")}`;
11175
11883
  if (dismissed === PAGE_SCRIPT_TIMEOUT) {
11176
11884
  return pageBusyError("accept_cookies");
11177
11885
  }
11178
- return dismissed || "No cookie consent banner detected. Try dismiss_popup for other overlays.";
11886
+ if (dismissed) return dismissed;
11887
+ const iframeResult = await tryDismissConsentIframe(wc);
11888
+ if (iframeResult) return iframeResult;
11889
+ return "No cookie consent banner detected. Try dismiss_popup for other overlays.";
11179
11890
  }
11180
11891
  case "extract_table": {
11181
11892
  if (!wc) return "Error: No active tab";
@@ -11273,23 +11984,19 @@ ${JSON.stringify(tableJson, null, 2)}`;
11273
11984
  const flowCtx = ctx.runtime.getFlowContext();
11274
11985
  return result + await getPostActionState$1(ctx, name) + flowCtx;
11275
11986
  }
11276
- async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime, history) {
11987
+ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history) {
11277
11988
  const lowerQuery = query.toLowerCase().trim();
11278
11989
  const isSummarize = lowerQuery.startsWith("summarize") || lowerQuery.startsWith("tldr") || lowerQuery === "summary";
11279
- if (provider.streamAgentQuery && tabManager && activeWebContents && runtime) {
11990
+ if (provider.streamAgentQuery && tabManager && activeWebContents && runtime2) {
11280
11991
  try {
11281
- const extractStart = Date.now();
11282
11992
  const pageContent = await extractContent(activeWebContents);
11283
- console.log(
11284
- `[Vessel Agent] initial extractContent completed in ${Date.now() - extractStart}ms, contentLen=${pageContent.content.length}`
11285
- );
11286
11993
  const pageType = detectPageType(pageContent);
11287
11994
  const defaultReadMode = chooseAgentReadMode(pageContent);
11288
11995
  const structuredContext = buildScopedContext(
11289
11996
  pageContent,
11290
11997
  defaultReadMode
11291
11998
  );
11292
- const runtimeState = runtime.getState();
11999
+ const runtimeState = runtime2.getState();
11293
12000
  const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
11294
12001
  const activeTabTitle = pageContent.title || "(untitled)";
11295
12002
  const activeTabUrl = pageContent.url || activeWebContents.getURL();
@@ -11332,8 +12039,11 @@ Instructions:
11332
12039
  - 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.
11333
12040
  - The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
11334
12041
  - When you only need detail on one product/result/card/form section, use inspect_element instead of reading the page.
11335
- - 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.
12042
+ - 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.
12043
+ - 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.
11336
12044
  - Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
12045
+ - If read_page returns empty or times out, do NOT retry with the same mode. Switch to read_page(mode="glance") or use screenshot to see the page visually.
12046
+ - Use screenshot when you need to see exactly what the user sees — visual layout, rendered content, images, or when text extraction is failing. The screenshot returns the actual rendered page image for visual analysis. It works even when the JS thread is completely blocked.
11337
12047
  - 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.
11338
12048
  - 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.
11339
12049
  - 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").
@@ -11351,7 +12061,7 @@ Instructions:
11351
12061
  - 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.
11352
12062
  - 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.
11353
12063
  - NEVER USE EMOJIS unless the user uses them first.`;
11354
- const actionCtx = { tabManager, runtime };
12064
+ const actionCtx = { tabManager, runtime: runtime2 };
11355
12065
  const contextualTools = pruneToolsForContext(
11356
12066
  AGENT_TOOLS,
11357
12067
  pageType,
@@ -11779,7 +12489,7 @@ function broadcastState(tabManager) {
11779
12489
  const tabId = tabManager.getActiveTabId();
11780
12490
  stateListener(getDevToolsPanelState(tabId));
11781
12491
  }
11782
- async function withDevToolsAction(runtime, tabManager, name, args, executor) {
12492
+ async function withDevToolsAction(runtime2, tabManager, name, args, executor) {
11783
12493
  const activityEntry = {
11784
12494
  id: ++activityCounter,
11785
12495
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11796,7 +12506,7 @@ async function withDevToolsAction(runtime, tabManager, name, args, executor) {
11796
12506
  broadcastState(tabManager);
11797
12507
  const startTime = Date.now();
11798
12508
  try {
11799
- const result = await runtime.runControlledAction({
12509
+ const result = await runtime2.runControlledAction({
11800
12510
  source: "mcp",
11801
12511
  name,
11802
12512
  args,
@@ -11818,7 +12528,7 @@ async function withDevToolsAction(runtime, tabManager, name, args, executor) {
11818
12528
  return asTextResponse$1(`Error: ${message}`);
11819
12529
  }
11820
12530
  }
11821
- function registerDevTools(server, tabManager, runtime) {
12531
+ function registerDevTools(server, tabManager, runtime2) {
11822
12532
  server.registerTool(
11823
12533
  "vessel_devtools_console_logs",
11824
12534
  {
@@ -11830,16 +12540,16 @@ function registerDevTools(server, tabManager, runtime) {
11830
12540
  search: zod.z.string().optional().describe("Filter entries containing this text (case-insensitive)")
11831
12541
  }
11832
12542
  },
11833
- async ({ level, limit, search }) => {
12543
+ async ({ level, limit, search: search2 }) => {
11834
12544
  return withDevToolsAction(
11835
- runtime,
12545
+ runtime2,
11836
12546
  tabManager,
11837
12547
  "devtools_console_logs",
11838
- { level, limit, search },
12548
+ { level, limit, search: search2 },
11839
12549
  async () => {
11840
12550
  const session = getOrCreateSession(tabManager);
11841
12551
  await session.ensureConsoleDomain();
11842
- const entries = session.getConsoleLogs({ level, limit, search });
12552
+ const entries = session.getConsoleLogs({ level, limit, search: search2 });
11843
12553
  if (entries.length === 0) {
11844
12554
  return "No console entries captured yet. Console monitoring is now active — new entries will be captured as they occur.";
11845
12555
  }
@@ -11856,7 +12566,7 @@ function registerDevTools(server, tabManager, runtime) {
11856
12566
  },
11857
12567
  async () => {
11858
12568
  return withDevToolsAction(
11859
- runtime,
12569
+ runtime2,
11860
12570
  tabManager,
11861
12571
  "devtools_console_clear",
11862
12572
  {},
@@ -11883,7 +12593,7 @@ function registerDevTools(server, tabManager, runtime) {
11883
12593
  },
11884
12594
  async ({ url_pattern, method, status_min, status_max, limit }) => {
11885
12595
  return withDevToolsAction(
11886
- runtime,
12596
+ runtime2,
11887
12597
  tabManager,
11888
12598
  "devtools_network_log",
11889
12599
  { url_pattern, method, status_min, status_max, limit },
@@ -11915,7 +12625,7 @@ function registerDevTools(server, tabManager, runtime) {
11915
12625
  },
11916
12626
  async ({ request_id }) => {
11917
12627
  return withDevToolsAction(
11918
- runtime,
12628
+ runtime2,
11919
12629
  tabManager,
11920
12630
  "devtools_network_response_body",
11921
12631
  { request_id },
@@ -11941,7 +12651,7 @@ function registerDevTools(server, tabManager, runtime) {
11941
12651
  },
11942
12652
  async () => {
11943
12653
  return withDevToolsAction(
11944
- runtime,
12654
+ runtime2,
11945
12655
  tabManager,
11946
12656
  "devtools_network_clear",
11947
12657
  {},
@@ -11965,7 +12675,7 @@ function registerDevTools(server, tabManager, runtime) {
11965
12675
  },
11966
12676
  async ({ selector, include_html }) => {
11967
12677
  return withDevToolsAction(
11968
- runtime,
12678
+ runtime2,
11969
12679
  tabManager,
11970
12680
  "devtools_query_dom",
11971
12681
  { selector, include_html },
@@ -11996,7 +12706,7 @@ function registerDevTools(server, tabManager, runtime) {
11996
12706
  },
11997
12707
  async ({ selector, properties }) => {
11998
12708
  return withDevToolsAction(
11999
- runtime,
12709
+ runtime2,
12000
12710
  tabManager,
12001
12711
  "devtools_get_styles",
12002
12712
  { selector, properties },
@@ -12024,7 +12734,7 @@ function registerDevTools(server, tabManager, runtime) {
12024
12734
  },
12025
12735
  async ({ selector, attribute, value }) => {
12026
12736
  return withDevToolsAction(
12027
- runtime,
12737
+ runtime2,
12028
12738
  tabManager,
12029
12739
  "devtools_modify_dom",
12030
12740
  { selector, attribute, value },
@@ -12046,7 +12756,7 @@ function registerDevTools(server, tabManager, runtime) {
12046
12756
  },
12047
12757
  async ({ expression }) => {
12048
12758
  return withDevToolsAction(
12049
- runtime,
12759
+ runtime2,
12050
12760
  tabManager,
12051
12761
  "devtools_execute_js",
12052
12762
  { expression: expression.slice(0, 200) },
@@ -12074,7 +12784,7 @@ Exception: ${result.exceptionDetails}`);
12074
12784
  },
12075
12785
  async ({ type }) => {
12076
12786
  return withDevToolsAction(
12077
- runtime,
12787
+ runtime2,
12078
12788
  tabManager,
12079
12789
  "devtools_get_storage",
12080
12790
  { type },
@@ -12103,7 +12813,7 @@ Exception: ${result.exceptionDetails}`);
12103
12813
  },
12104
12814
  async ({ type, key, value }) => {
12105
12815
  return withDevToolsAction(
12106
- runtime,
12816
+ runtime2,
12107
12817
  tabManager,
12108
12818
  "devtools_set_storage",
12109
12819
  { type, key, value: value ? value.slice(0, 100) : null },
@@ -12122,7 +12832,7 @@ Exception: ${result.exceptionDetails}`);
12122
12832
  },
12123
12833
  async () => {
12124
12834
  return withDevToolsAction(
12125
- runtime,
12835
+ runtime2,
12126
12836
  tabManager,
12127
12837
  "devtools_performance",
12128
12838
  {},
@@ -12146,7 +12856,7 @@ Exception: ${result.exceptionDetails}`);
12146
12856
  },
12147
12857
  async ({ type, limit }) => {
12148
12858
  return withDevToolsAction(
12149
- runtime,
12859
+ runtime2,
12150
12860
  tabManager,
12151
12861
  "devtools_get_errors",
12152
12862
  { type, limit },
@@ -12170,7 +12880,7 @@ Exception: ${result.exceptionDetails}`);
12170
12880
  },
12171
12881
  async () => {
12172
12882
  return withDevToolsAction(
12173
- runtime,
12883
+ runtime2,
12174
12884
  tabManager,
12175
12885
  "devtools_clear_errors",
12176
12886
  {},
@@ -12184,6 +12894,7 @@ Exception: ${result.exceptionDetails}`);
12184
12894
  );
12185
12895
  }
12186
12896
  let httpServer = null;
12897
+ let mcpAuthToken = null;
12187
12898
  function asTextResponse(text) {
12188
12899
  return { content: [{ type: "text", text }] };
12189
12900
  }
@@ -13121,9 +13832,9 @@ async function getPostActionState(tabManager, name) {
13121
13832
  }
13122
13833
  return "";
13123
13834
  }
13124
- async function withAction(runtime, tabManager, name, args, executor) {
13835
+ async function withAction(runtime2, tabManager, name, args, executor) {
13125
13836
  try {
13126
- const result = await runtime.runControlledAction({
13837
+ const result = await runtime2.runControlledAction({
13127
13838
  source: "mcp",
13128
13839
  name,
13129
13840
  args,
@@ -13132,7 +13843,7 @@ async function withAction(runtime, tabManager, name, args, executor) {
13132
13843
  executor
13133
13844
  });
13134
13845
  const stateInfo = await getPostActionState(tabManager, name);
13135
- const flowCtx = runtime.getFlowContext();
13846
+ const flowCtx = runtime2.getFlowContext();
13136
13847
  return asTextResponse(result + stateInfo + flowCtx);
13137
13848
  } catch (error) {
13138
13849
  return asTextResponse(
@@ -13426,6 +14137,7 @@ async function submitForm(wc, index, selector) {
13426
14137
  if (formInfo.params) {
13427
14138
  url.search = formInfo.params;
13428
14139
  }
14140
+ assertSafeURL(url.toString());
13429
14141
  wc.loadURL(url.toString());
13430
14142
  await waitForPotentialNavigation(wc, beforeUrl);
13431
14143
  const afterUrl = wc.getURL();
@@ -13548,26 +14260,7 @@ async function waitForCondition(wc, text, selector, timeoutMs) {
13548
14260
  ...diagnostic ? { diagnostic } : {}
13549
14261
  });
13550
14262
  }
13551
- async function captureScreenshotPayload(wc) {
13552
- for (let attempt = 0; attempt < 3; attempt += 1) {
13553
- await new Promise((resolve) => setTimeout(resolve, 120 * (attempt + 1)));
13554
- const image = await wc.capturePage();
13555
- if (!image.isEmpty()) {
13556
- const size = image.getSize();
13557
- const base64 = image.toPNG().toString("base64");
13558
- if (base64) {
13559
- return {
13560
- ok: true,
13561
- base64,
13562
- width: size.width,
13563
- height: size.height
13564
- };
13565
- }
13566
- }
13567
- }
13568
- return { ok: false, error: "page image was empty after 3 attempts" };
13569
- }
13570
- function registerTools(server, tabManager, runtime) {
14263
+ function registerTools(server, tabManager, runtime2) {
13571
14264
  server.registerPrompt(
13572
14265
  "vessel-supervisor-brief",
13573
14266
  {
@@ -13575,7 +14268,7 @@ function registerTools(server, tabManager, runtime) {
13575
14268
  description: "A reusable prompt for reviewing the current Vessel runtime state."
13576
14269
  },
13577
14270
  async () => {
13578
- const state2 = runtime.getState();
14271
+ const state2 = runtime2.getState();
13579
14272
  const activeTab = getActiveTabSummary(tabManager);
13580
14273
  return asPromptResponse(
13581
14274
  [
@@ -13602,7 +14295,7 @@ function registerTools(server, tabManager, runtime) {
13602
14295
  contents: [
13603
14296
  {
13604
14297
  uri: "vessel://runtime/state",
13605
- text: JSON.stringify(runtime.getState(), null, 2)
14298
+ text: JSON.stringify(runtime2.getState(), null, 2)
13606
14299
  }
13607
14300
  ]
13608
14301
  })
@@ -13718,7 +14411,7 @@ function registerTools(server, tabManager, runtime) {
13718
14411
  }
13719
14412
  },
13720
14413
  async ({ text, stream_id, mode, kind, title }) => {
13721
- const entry = runtime.publishTranscript({
14414
+ const entry = runtime2.publishTranscript({
13722
14415
  source: "mcp",
13723
14416
  text,
13724
14417
  streamId: stream_id,
@@ -13748,7 +14441,7 @@ function registerTools(server, tabManager, runtime) {
13748
14441
  description: "Clear the in-browser transcript monitor state."
13749
14442
  },
13750
14443
  async () => {
13751
- runtime.clearTranscript();
14444
+ runtime2.clearTranscript();
13752
14445
  return asTextResponse("Cleared browser transcript monitor.");
13753
14446
  }
13754
14447
  );
@@ -13888,7 +14581,7 @@ ${buildScopedContext(pageContent, mode)}`;
13888
14581
  `Navigation blocked: ${url} returned ${preCheck.detail || "dead link"}. Try a different URL or go back and choose another link.`
13889
14582
  );
13890
14583
  }
13891
- return withAction(runtime, tabManager, "navigate", { url }, async () => {
14584
+ return withAction(runtime2, tabManager, "navigate", { url }, async () => {
13892
14585
  const id = tabManager.getActiveTabId();
13893
14586
  tabManager.navigateTab(id, url);
13894
14587
  const { httpStatus } = await waitForLoadWithStatus(
@@ -13918,7 +14611,7 @@ ${buildScopedContext(pageContent, mode)}`;
13918
14611
  return asTextResponse("Error: No active tab");
13919
14612
  }
13920
14613
  return withAction(
13921
- runtime,
14614
+ runtime2,
13922
14615
  tabManager,
13923
14616
  "set_ad_blocking",
13924
14617
  { enabled, tabId, match, reload },
@@ -14007,7 +14700,7 @@ ${buildScopedContext(pageContent, mode)}`;
14007
14700
  async () => {
14008
14701
  const tab = tabManager.getActiveTab();
14009
14702
  if (!tab) return asTextResponse("Error: No active tab");
14010
- return withAction(runtime, tabManager, "go_back", {}, async () => {
14703
+ return withAction(runtime2, tabManager, "go_back", {}, async () => {
14011
14704
  if (!tab.canGoBack()) {
14012
14705
  return "No previous page in history";
14013
14706
  }
@@ -14028,7 +14721,7 @@ ${buildScopedContext(pageContent, mode)}`;
14028
14721
  async () => {
14029
14722
  const tab = tabManager.getActiveTab();
14030
14723
  if (!tab) return asTextResponse("Error: No active tab");
14031
- return withAction(runtime, tabManager, "go_forward", {}, async () => {
14724
+ return withAction(runtime2, tabManager, "go_forward", {}, async () => {
14032
14725
  if (!tab.canGoForward()) {
14033
14726
  return "No forward page in history";
14034
14727
  }
@@ -14049,7 +14742,7 @@ ${buildScopedContext(pageContent, mode)}`;
14049
14742
  async () => {
14050
14743
  const tab = tabManager.getActiveTab();
14051
14744
  if (!tab) return asTextResponse("Error: No active tab");
14052
- return withAction(runtime, tabManager, "reload", {}, async () => {
14745
+ return withAction(runtime2, tabManager, "reload", {}, async () => {
14053
14746
  tabManager.reloadTab(tabManager.getActiveTabId());
14054
14747
  await waitForLoad(tab.view.webContents);
14055
14748
  return `Reloaded ${tab.view.webContents.getURL()}`;
@@ -14070,7 +14763,7 @@ ${buildScopedContext(pageContent, mode)}`;
14070
14763
  const tab = tabManager.getActiveTab();
14071
14764
  if (!tab) return asTextResponse("Error: No active tab");
14072
14765
  return withAction(
14073
- runtime,
14766
+ runtime2,
14074
14767
  tabManager,
14075
14768
  "click",
14076
14769
  { index, selector },
@@ -14099,7 +14792,7 @@ ${buildScopedContext(pageContent, mode)}`;
14099
14792
  const tab = tabManager.getActiveTab();
14100
14793
  if (!tab) return asTextResponse("Error: No active tab");
14101
14794
  return withAction(
14102
- runtime,
14795
+ runtime2,
14103
14796
  tabManager,
14104
14797
  "hover",
14105
14798
  { index, selector },
@@ -14128,7 +14821,7 @@ ${buildScopedContext(pageContent, mode)}`;
14128
14821
  const tab = tabManager.getActiveTab();
14129
14822
  if (!tab) return asTextResponse("Error: No active tab");
14130
14823
  return withAction(
14131
- runtime,
14824
+ runtime2,
14132
14825
  tabManager,
14133
14826
  "focus",
14134
14827
  { index, selector },
@@ -14242,7 +14935,7 @@ ${buildScopedContext(pageContent, mode)}`;
14242
14935
  const tab = tabManager.getActiveTab();
14243
14936
  if (!tab) return asTextResponse("Error: No active tab");
14244
14937
  return withAction(
14245
- runtime,
14938
+ runtime2,
14246
14939
  tabManager,
14247
14940
  "type",
14248
14941
  { index, selector, text, mode },
@@ -14281,7 +14974,7 @@ ${buildScopedContext(pageContent, mode)}`;
14281
14974
  const tab = tabManager.getActiveTab();
14282
14975
  if (!tab) return asTextResponse("Error: No active tab");
14283
14976
  return withAction(
14284
- runtime,
14977
+ runtime2,
14285
14978
  tabManager,
14286
14979
  "type_text",
14287
14980
  { index, selector, text, mode },
@@ -14318,7 +15011,7 @@ ${buildScopedContext(pageContent, mode)}`;
14318
15011
  const tab = tabManager.getActiveTab();
14319
15012
  if (!tab) return asTextResponse("Error: No active tab");
14320
15013
  return withAction(
14321
- runtime,
15014
+ runtime2,
14322
15015
  tabManager,
14323
15016
  "select_option",
14324
15017
  { index, selector, label, value },
@@ -14340,7 +15033,7 @@ ${buildScopedContext(pageContent, mode)}`;
14340
15033
  const tab = tabManager.getActiveTab();
14341
15034
  if (!tab) return asTextResponse("Error: No active tab");
14342
15035
  return withAction(
14343
- runtime,
15036
+ runtime2,
14344
15037
  tabManager,
14345
15038
  "submit_form",
14346
15039
  { index, selector },
@@ -14373,7 +15066,7 @@ ${buildScopedContext(pageContent, mode)}`;
14373
15066
  const tab = tabManager.getActiveTab();
14374
15067
  if (!tab) return asTextResponse("Error: No active tab");
14375
15068
  return withAction(
14376
- runtime,
15069
+ runtime2,
14377
15070
  tabManager,
14378
15071
  "press_key",
14379
15072
  { key, index, selector },
@@ -14409,7 +15102,7 @@ ${buildScopedContext(pageContent, mode)}`;
14409
15102
  const tab = tabManager.getActiveTab();
14410
15103
  if (!tab) return asTextResponse("Error: No active tab");
14411
15104
  return withAction(
14412
- runtime,
15105
+ runtime2,
14413
15106
  tabManager,
14414
15107
  "scroll",
14415
15108
  { direction, amount },
@@ -14432,7 +15125,7 @@ ${buildScopedContext(pageContent, mode)}`;
14432
15125
  const tab = tabManager.getActiveTab();
14433
15126
  if (!tab) return asTextResponse("Error: No active tab");
14434
15127
  return withAction(
14435
- runtime,
15128
+ runtime2,
14436
15129
  tabManager,
14437
15130
  "dismiss_popup",
14438
15131
  {},
@@ -14455,7 +15148,7 @@ ${buildScopedContext(pageContent, mode)}`;
14455
15148
  const tab = tabManager.getActiveTab();
14456
15149
  if (!tab) return asTextResponse("Error: No active tab");
14457
15150
  return withAction(
14458
- runtime,
15151
+ runtime2,
14459
15152
  tabManager,
14460
15153
  "clear_overlays",
14461
15154
  { strategy: strategy || "auto" },
@@ -14481,7 +15174,7 @@ ${buildScopedContext(pageContent, mode)}`;
14481
15174
  const tab = tabManager.getActiveTab();
14482
15175
  if (!tab) return asTextResponse("Error: No active tab");
14483
15176
  return withAction(
14484
- runtime,
15177
+ runtime2,
14485
15178
  tabManager,
14486
15179
  "wait_for",
14487
15180
  { text, selector, timeoutMs },
@@ -14498,7 +15191,7 @@ ${buildScopedContext(pageContent, mode)}`;
14498
15191
  url: zod.z.string().optional().describe("URL to open (defaults to about:blank)")
14499
15192
  }
14500
15193
  },
14501
- async ({ url }) => withAction(runtime, tabManager, "create_tab", { url }, async () => {
15194
+ async ({ url }) => withAction(runtime2, tabManager, "create_tab", { url }, async () => {
14502
15195
  const id = tabManager.createTab(url || "about:blank");
14503
15196
  const tab = tabManager.getActiveTab();
14504
15197
  if (tab) {
@@ -14518,7 +15211,7 @@ ${buildScopedContext(pageContent, mode)}`;
14518
15211
  }
14519
15212
  },
14520
15213
  async ({ tabId, match }) => withAction(
14521
- runtime,
15214
+ runtime2,
14522
15215
  tabManager,
14523
15216
  "switch_tab",
14524
15217
  { tabId, match },
@@ -14541,7 +15234,7 @@ ${buildScopedContext(pageContent, mode)}`;
14541
15234
  tabId: zod.z.string().describe("The tab ID to close")
14542
15235
  }
14543
15236
  },
14544
- async ({ tabId }) => withAction(runtime, tabManager, "close_tab", { tabId }, async () => {
15237
+ async ({ tabId }) => withAction(runtime2, tabManager, "close_tab", { tabId }, async () => {
14545
15238
  tabManager.closeTab(tabId);
14546
15239
  return `Closed tab ${tabId}`;
14547
15240
  })
@@ -14557,12 +15250,12 @@ ${buildScopedContext(pageContent, mode)}`;
14557
15250
  }
14558
15251
  },
14559
15252
  async ({ name, note }) => withAction(
14560
- runtime,
15253
+ runtime2,
14561
15254
  tabManager,
14562
15255
  "create_checkpoint",
14563
15256
  { name, note },
14564
15257
  async () => {
14565
- const checkpoint = runtime.createCheckpoint(name, note);
15258
+ const checkpoint = runtime2.createCheckpoint(name, note);
14566
15259
  return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
14567
15260
  }
14568
15261
  )
@@ -14578,12 +15271,12 @@ ${buildScopedContext(pageContent, mode)}`;
14578
15271
  }
14579
15272
  },
14580
15273
  async ({ name, note }) => withAction(
14581
- runtime,
15274
+ runtime2,
14582
15275
  tabManager,
14583
15276
  "create_checkpoint",
14584
15277
  { name, note },
14585
15278
  async () => {
14586
- const checkpoint = runtime.createCheckpoint(name, note);
15279
+ const checkpoint = runtime2.createCheckpoint(name, note);
14587
15280
  return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
14588
15281
  }
14589
15282
  )
@@ -14599,17 +15292,17 @@ ${buildScopedContext(pageContent, mode)}`;
14599
15292
  }
14600
15293
  },
14601
15294
  async ({ checkpointId, name }) => withAction(
14602
- runtime,
15295
+ runtime2,
14603
15296
  tabManager,
14604
15297
  "restore_checkpoint",
14605
15298
  { checkpointId, name },
14606
15299
  async () => {
14607
- const state2 = runtime.getState();
15300
+ const state2 = runtime2.getState();
14608
15301
  const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
14609
15302
  if (!checkpoint) {
14610
15303
  return "Error: No matching checkpoint found";
14611
15304
  }
14612
- runtime.restoreCheckpoint(checkpoint.id);
15305
+ runtime2.restoreCheckpoint(checkpoint.id);
14613
15306
  return `Restored checkpoint ${checkpoint.name}`;
14614
15307
  }
14615
15308
  )
@@ -14625,17 +15318,17 @@ ${buildScopedContext(pageContent, mode)}`;
14625
15318
  }
14626
15319
  },
14627
15320
  async ({ checkpointId, name }) => withAction(
14628
- runtime,
15321
+ runtime2,
14629
15322
  tabManager,
14630
15323
  "restore_checkpoint",
14631
15324
  { checkpointId, name },
14632
15325
  async () => {
14633
- const state2 = runtime.getState();
15326
+ const state2 = runtime2.getState();
14634
15327
  const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
14635
15328
  if (!checkpoint) {
14636
15329
  return "Error: No matching checkpoint found";
14637
15330
  }
14638
- runtime.restoreCheckpoint(checkpoint.id);
15331
+ runtime2.restoreCheckpoint(checkpoint.id);
14639
15332
  return `Restored checkpoint ${checkpoint.name}`;
14640
15333
  }
14641
15334
  )
@@ -14649,7 +15342,7 @@ ${buildScopedContext(pageContent, mode)}`;
14649
15342
  name: zod.z.string().describe("Session name such as github-logged-in")
14650
15343
  }
14651
15344
  },
14652
- async ({ name }) => withAction(runtime, tabManager, "save_session", { name }, async () => {
15345
+ async ({ name }) => withAction(runtime2, tabManager, "save_session", { name }, async () => {
14653
15346
  const saved = await saveNamedSession(
14654
15347
  tabManager,
14655
15348
  name
@@ -14666,7 +15359,7 @@ ${buildScopedContext(pageContent, mode)}`;
14666
15359
  name: zod.z.string().describe("Previously saved session name")
14667
15360
  }
14668
15361
  },
14669
- async ({ name }) => withAction(runtime, tabManager, "load_session", { name }, async () => {
15362
+ async ({ name }) => withAction(runtime2, tabManager, "load_session", { name }, async () => {
14670
15363
  const loaded = await loadNamedSession(
14671
15364
  tabManager,
14672
15365
  name
@@ -14680,7 +15373,7 @@ ${buildScopedContext(pageContent, mode)}`;
14680
15373
  title: "List Sessions",
14681
15374
  description: "List previously saved named browser sessions with cookie and storage counts."
14682
15375
  },
14683
- async () => withAction(runtime, tabManager, "list_sessions", {}, async () => {
15376
+ async () => withAction(runtime2, tabManager, "list_sessions", {}, async () => {
14684
15377
  const sessions2 = listNamedSessions();
14685
15378
  if (sessions2.length === 0) return "No saved sessions";
14686
15379
  return sessions2.map(
@@ -14698,7 +15391,7 @@ ${buildScopedContext(pageContent, mode)}`;
14698
15391
  }
14699
15392
  },
14700
15393
  async ({ name }) => withAction(
14701
- runtime,
15394
+ runtime2,
14702
15395
  tabManager,
14703
15396
  "delete_session",
14704
15397
  { name },
@@ -14721,7 +15414,7 @@ ${buildScopedContext(pageContent, mode)}`;
14721
15414
  "Error capturing screenshot: active tab has zero-sized bounds"
14722
15415
  );
14723
15416
  }
14724
- const screenshot = await captureScreenshotPayload(tab.view.webContents);
15417
+ const screenshot = await captureScreenshot(tab.view.webContents);
14725
15418
  if (!screenshot.ok) {
14726
15419
  return asTextResponse(
14727
15420
  `Error capturing screenshot: ${screenshot.error}`
@@ -14785,7 +15478,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14785
15478
  if (!tab) return asTextResponse("Error: No active tab");
14786
15479
  const normalizedText = normalizeLooseString(text);
14787
15480
  return withAction(
14788
- runtime,
15481
+ runtime2,
14789
15482
  tabManager,
14790
15483
  "highlight",
14791
15484
  {
@@ -14834,7 +15527,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14834
15527
  const tab = tabManager.getActiveTab();
14835
15528
  if (!tab) return asTextResponse("Error: No active tab");
14836
15529
  return withAction(
14837
- runtime,
15530
+ runtime2,
14838
15531
  tabManager,
14839
15532
  "clear_highlights",
14840
15533
  {},
@@ -14859,7 +15552,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14859
15552
  }
14860
15553
  },
14861
15554
  async ({ url }) => {
14862
- const state2 = getState$1();
15555
+ const state2 = getState$2();
14863
15556
  const activeTab = tabManager.getActiveTab();
14864
15557
  const activeUrl = activeTab ? normalizeUrl(activeTab.view.webContents.getURL()) : null;
14865
15558
  const activeSavedHighlights = activeUrl ? state2.highlights.filter((highlight) => highlight.url === activeUrl) : [];
@@ -14990,7 +15683,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14990
15683
  },
14991
15684
  async ({ name, summary }) => {
14992
15685
  return withAction(
14993
- runtime,
15686
+ runtime2,
14994
15687
  tabManager,
14995
15688
  "create_bookmark_folder",
14996
15689
  { name, summary },
@@ -15052,7 +15745,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15052
15745
  on_duplicate
15053
15746
  }) => {
15054
15747
  return withAction(
15055
- runtime,
15748
+ runtime2,
15056
15749
  tabManager,
15057
15750
  "save_bookmark",
15058
15751
  {
@@ -15131,7 +15824,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15131
15824
  },
15132
15825
  async ({ folder_id, folder_name }) => {
15133
15826
  return withAction(
15134
- runtime,
15827
+ runtime2,
15135
15828
  tabManager,
15136
15829
  "list_bookmarks",
15137
15830
  { folder_id, folder_name },
@@ -15196,7 +15889,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15196
15889
  },
15197
15890
  async (args) => {
15198
15891
  return withAction(
15199
- runtime,
15892
+ runtime2,
15200
15893
  tabManager,
15201
15894
  "organize_bookmark",
15202
15895
  args,
@@ -15263,7 +15956,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15263
15956
  },
15264
15957
  async ({ query }) => {
15265
15958
  return withAction(
15266
- runtime,
15959
+ runtime2,
15267
15960
  tabManager,
15268
15961
  "search_bookmarks",
15269
15962
  { query },
@@ -15294,7 +15987,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15294
15987
  },
15295
15988
  async ({ bookmark_id }) => {
15296
15989
  return withAction(
15297
- runtime,
15990
+ runtime2,
15298
15991
  tabManager,
15299
15992
  "remove_bookmark",
15300
15993
  { bookmark_id },
@@ -15327,7 +16020,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15327
16020
  },
15328
16021
  async ({ bookmark_id, url, title, index, selector, note }) => {
15329
16022
  return withAction(
15330
- runtime,
16023
+ runtime2,
15331
16024
  tabManager,
15332
16025
  "archive_bookmark",
15333
16026
  { bookmark_id, url, title, index, selector, note },
@@ -15397,7 +16090,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15397
16090
  },
15398
16091
  async ({ bookmark_id, new_tab }) => {
15399
16092
  return withAction(
15400
- runtime,
16093
+ runtime2,
15401
16094
  tabManager,
15402
16095
  "open_bookmark",
15403
16096
  { bookmark_id, new_tab },
@@ -15440,7 +16133,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15440
16133
  },
15441
16134
  async ({ folder_id }) => {
15442
16135
  return withAction(
15443
- runtime,
16136
+ runtime2,
15444
16137
  tabManager,
15445
16138
  "remove_bookmark_folder",
15446
16139
  { folder_id },
@@ -15466,7 +16159,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15466
16159
  },
15467
16160
  async ({ folder_id, new_name, summary }) => {
15468
16161
  return withAction(
15469
- runtime,
16162
+ runtime2,
15470
16163
  tabManager,
15471
16164
  "rename_bookmark_folder",
15472
16165
  { folder_id, new_name, summary },
@@ -15503,7 +16196,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15503
16196
  },
15504
16197
  async ({ title, body, folder, tags }) => {
15505
16198
  return withAction(
15506
- runtime,
16199
+ runtime2,
15507
16200
  tabManager,
15508
16201
  "memory_note_create",
15509
16202
  { title, folder, tags },
@@ -15527,7 +16220,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15527
16220
  },
15528
16221
  async ({ note_path, content, heading }) => {
15529
16222
  return withAction(
15530
- runtime,
16223
+ runtime2,
15531
16224
  tabManager,
15532
16225
  "memory_note_append",
15533
16226
  { note_path, heading },
@@ -15554,7 +16247,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15554
16247
  },
15555
16248
  async ({ folder, limit }) => {
15556
16249
  return withAction(
15557
- runtime,
16250
+ runtime2,
15558
16251
  tabManager,
15559
16252
  "memory_note_list",
15560
16253
  { folder, limit },
@@ -15584,7 +16277,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15584
16277
  },
15585
16278
  async ({ query, folder, tags, limit }) => {
15586
16279
  return withAction(
15587
- runtime,
16280
+ runtime2,
15588
16281
  tabManager,
15589
16282
  "memory_note_search",
15590
16283
  { query, folder, tags, limit },
@@ -15617,7 +16310,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15617
16310
  const tab = tabManager.getActiveTab();
15618
16311
  if (!tab) return asTextResponse("Error: No active tab");
15619
16312
  return withAction(
15620
- runtime,
16313
+ runtime2,
15621
16314
  tabManager,
15622
16315
  "memory_page_capture",
15623
16316
  { title, folder, tags },
@@ -15654,7 +16347,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15654
16347
  },
15655
16348
  async ({ bookmark_id, note_path, title, folder, note, tags }) => {
15656
16349
  return withAction(
15657
- runtime,
16350
+ runtime2,
15658
16351
  tabManager,
15659
16352
  "memory_link_bookmark",
15660
16353
  { bookmark_id, note_path, title, folder, tags },
@@ -15693,7 +16386,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15693
16386
  async ({ goal, steps }) => {
15694
16387
  const normalizedSteps = coerceStringArray(steps) ?? [];
15695
16388
  const tab = tabManager.getActiveTab();
15696
- const flow = runtime.startFlow(
16389
+ const flow = runtime2.startFlow(
15697
16390
  goal,
15698
16391
  normalizedSteps,
15699
16392
  tab?.view.webContents.getURL()
@@ -15714,9 +16407,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15714
16407
  }
15715
16408
  },
15716
16409
  async ({ detail }) => {
15717
- const flow = runtime.advanceFlow(detail);
16410
+ const flow = runtime2.advanceFlow(detail);
15718
16411
  if (!flow) return asTextResponse("No active flow to advance");
15719
- const ctx = runtime.getFlowContext();
16412
+ const ctx = runtime2.getFlowContext();
15720
16413
  return asTextResponse(`Step completed.${ctx}`);
15721
16414
  }
15722
16415
  );
@@ -15727,9 +16420,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15727
16420
  description: "Check the current workflow progress."
15728
16421
  },
15729
16422
  async () => {
15730
- const flow = runtime.getFlowState();
16423
+ const flow = runtime2.getFlowState();
15731
16424
  if (!flow) return asTextResponse("No active workflow.");
15732
- return asTextResponse(runtime.getFlowContext());
16425
+ return asTextResponse(runtime2.getFlowContext());
15733
16426
  }
15734
16427
  );
15735
16428
  server.registerTool(
@@ -15739,7 +16432,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15739
16432
  description: "Clear the active workflow tracker."
15740
16433
  },
15741
16434
  async () => {
15742
- runtime.clearFlow();
16435
+ runtime2.clearFlow();
15743
16436
  return asTextResponse("Workflow ended.");
15744
16437
  }
15745
16438
  );
@@ -15768,7 +16461,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15768
16461
  suggestions.push(`Page: ${page.title || "(untitled)"}`);
15769
16462
  suggestions.push(`URL: ${page.url}`);
15770
16463
  suggestions.push("");
15771
- const flowCtx = runtime.getFlowContext();
16464
+ const flowCtx = runtime2.getFlowContext();
15772
16465
  if (flowCtx) {
15773
16466
  suggestions.push(flowCtx);
15774
16467
  suggestions.push("");
@@ -15868,7 +16561,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15868
16561
  const tab = tabManager.getActiveTab();
15869
16562
  if (!tab) return asTextResponse("Error: No active tab");
15870
16563
  return withAction(
15871
- runtime,
16564
+ runtime2,
15872
16565
  tabManager,
15873
16566
  "fill_form",
15874
16567
  { fieldCount: fields.length, submit },
@@ -15925,7 +16618,7 @@ ${results.join("\n")}`;
15925
16618
  const tab = tabManager.getActiveTab();
15926
16619
  if (!tab) return asTextResponse("Error: No active tab");
15927
16620
  return withAction(
15928
- runtime,
16621
+ runtime2,
15929
16622
  tabManager,
15930
16623
  "login",
15931
16624
  { url, username: username.slice(0, 3) + "***" },
@@ -16030,7 +16723,7 @@ ${steps.join("\n")}`;
16030
16723
  `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`
16031
16724
  );
16032
16725
  }
16033
- return withAction(runtime, tabManager, "search", { query }, async () => {
16726
+ return withAction(runtime2, tabManager, "search", { query }, async () => {
16034
16727
  const wc = tab.view.webContents;
16035
16728
  const searchSel = selector || await wc.executeJavaScript(`
16036
16729
  (function() {
@@ -16084,7 +16777,7 @@ ${steps.join("\n")}`;
16084
16777
  const tab = tabManager.getActiveTab();
16085
16778
  if (!tab) return asTextResponse("Error: No active tab");
16086
16779
  return withAction(
16087
- runtime,
16780
+ runtime2,
16088
16781
  tabManager,
16089
16782
  "paginate",
16090
16783
  { direction },
@@ -16135,7 +16828,7 @@ ${steps.join("\n")}`;
16135
16828
  const tab = tabManager.getActiveTab();
16136
16829
  if (!tab) return asTextResponse("Error: No active tab");
16137
16830
  return withAction(
16138
- runtime,
16831
+ runtime2,
16139
16832
  tabManager,
16140
16833
  "vessel_accept_cookies",
16141
16834
  {},
@@ -16193,7 +16886,7 @@ ${steps.join("\n")}`;
16193
16886
  const tab = tabManager.getActiveTab();
16194
16887
  if (!tab) return asTextResponse("Error: No active tab");
16195
16888
  return withAction(
16196
- runtime,
16889
+ runtime2,
16197
16890
  tabManager,
16198
16891
  "vessel_extract_table",
16199
16892
  { index, selector: rawSelector },
@@ -16248,7 +16941,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
16248
16941
  const tab = tabManager.getActiveTab();
16249
16942
  if (!tab) return asTextResponse("Error: No active tab");
16250
16943
  return withAction(
16251
- runtime,
16944
+ runtime2,
16252
16945
  tabManager,
16253
16946
  "vessel_scroll_to_element",
16254
16947
  { index, selector: rawSelector, position },
@@ -16306,7 +16999,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
16306
16999
  const tab = tabManager.getActiveTab();
16307
17000
  if (!tab) return asTextResponse("Error: No active tab");
16308
17001
  return withAction(
16309
- runtime,
17002
+ runtime2,
16310
17003
  tabManager,
16311
17004
  "vessel_wait_for_navigation",
16312
17005
  { timeoutMs },
@@ -16357,7 +17050,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
16357
17050
  inputSchema: zod.z.object({})
16358
17051
  },
16359
17052
  async () => {
16360
- const m = runtime.getMetrics();
17053
+ const m = runtime2.getMetrics();
16361
17054
  const lines = [
16362
17055
  `Session Metrics:`,
16363
17056
  ` Total actions: ${m.totalActions}`,
@@ -16517,16 +17210,16 @@ async function resolveSelector(wc, index, selector) {
16517
17210
  `
16518
17211
  );
16519
17212
  }
16520
- function createMcpServer(tabManager, runtime) {
17213
+ function createMcpServer(tabManager, runtime2) {
16521
17214
  const server = new mcp_js.McpServer({
16522
17215
  name: "vessel-browser",
16523
17216
  version: "0.1.0"
16524
17217
  });
16525
- registerTools(server, tabManager, runtime);
16526
- registerDevTools(server, tabManager, runtime);
17218
+ registerTools(server, tabManager, runtime2);
17219
+ registerDevTools(server, tabManager, runtime2);
16527
17220
  return server;
16528
17221
  }
16529
- function startMcpServer(tabManager, runtime, port) {
17222
+ function startMcpServer(tabManager, runtime2, port) {
16530
17223
  setMcpHealth({
16531
17224
  configuredPort: port,
16532
17225
  activePort: null,
@@ -16534,6 +17227,7 @@ function startMcpServer(tabManager, runtime, port) {
16534
17227
  status: "starting",
16535
17228
  message: `Starting MCP server on port ${port}.`
16536
17229
  });
17230
+ mcpAuthToken = crypto$1.randomBytes(32).toString("hex");
16537
17231
  return new Promise((resolve) => {
16538
17232
  const server = http.createServer(async (req, res) => {
16539
17233
  const url = new URL(req.url || "/", `http://localhost:${port}`);
@@ -16542,22 +17236,28 @@ function startMcpServer(tabManager, runtime, port) {
16542
17236
  res.end("Not found");
16543
17237
  return;
16544
17238
  }
16545
- res.setHeader("Access-Control-Allow-Origin", "*");
17239
+ res.setHeader("Access-Control-Allow-Origin", "null");
16546
17240
  res.setHeader(
16547
17241
  "Access-Control-Allow-Methods",
16548
17242
  "POST, GET, DELETE, OPTIONS"
16549
17243
  );
16550
17244
  res.setHeader(
16551
17245
  "Access-Control-Allow-Headers",
16552
- "Content-Type, mcp-session-id"
17246
+ "Content-Type, mcp-session-id, Authorization"
16553
17247
  );
16554
17248
  if (req.method === "OPTIONS") {
16555
17249
  res.writeHead(204);
16556
17250
  res.end();
16557
17251
  return;
16558
17252
  }
17253
+ const authHeader = req.headers.authorization;
17254
+ if (!authHeader || authHeader !== `Bearer ${mcpAuthToken}`) {
17255
+ res.writeHead(401, { "Content-Type": "application/json" });
17256
+ res.end(JSON.stringify({ error: "Unauthorized — missing or invalid bearer token" }));
17257
+ return;
17258
+ }
16559
17259
  try {
16560
- const mcpServer = createMcpServer(tabManager, runtime);
17260
+ const mcpServer = createMcpServer(tabManager, runtime2);
16561
17261
  const transport = new streamableHttp_js.StreamableHTTPServerTransport({
16562
17262
  sessionIdGenerator: void 0
16563
17263
  });
@@ -16599,6 +17299,7 @@ function startMcpServer(tabManager, runtime, port) {
16599
17299
  configuredPort: port,
16600
17300
  activePort: null,
16601
17301
  endpoint: null,
17302
+ authToken: null,
16602
17303
  error: message
16603
17304
  });
16604
17305
  });
@@ -16615,11 +17316,13 @@ function startMcpServer(tabManager, runtime, port) {
16615
17316
  message: `MCP server listening on ${endpoint}.`
16616
17317
  });
16617
17318
  console.log(`[Vessel MCP] Server listening on ${endpoint}`);
17319
+ console.log(`[Vessel MCP] Auth token: ${mcpAuthToken}`);
16618
17320
  finish({
16619
17321
  ok: true,
16620
17322
  configuredPort: port,
16621
17323
  activePort: actualPort,
16622
- endpoint
17324
+ endpoint,
17325
+ authToken: mcpAuthToken
16623
17326
  });
16624
17327
  });
16625
17328
  });
@@ -16638,6 +17341,7 @@ function stopMcpServer() {
16638
17341
  }
16639
17342
  const server = httpServer;
16640
17343
  httpServer = null;
17344
+ mcpAuthToken = null;
16641
17345
  server.close(() => {
16642
17346
  setMcpHealth({
16643
17347
  activePort: null,
@@ -16651,14 +17355,14 @@ function stopMcpServer() {
16651
17355
  });
16652
17356
  }
16653
17357
  let activeChatProvider = null;
16654
- function registerIpcHandlers(windowState, runtime) {
17358
+ function registerIpcHandlers(windowState, runtime2) {
16655
17359
  const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
16656
17360
  const sendToRendererViews = (channel, ...args) => {
16657
17361
  chromeView.webContents.send(channel, ...args);
16658
17362
  sidebarView.webContents.send(channel, ...args);
16659
17363
  devtoolsPanelView.webContents.send(channel, ...args);
16660
17364
  };
16661
- runtime.setUpdateListener((state2) => {
17365
+ runtime2.setUpdateListener((state2) => {
16662
17366
  sendToRendererViews(Channels.AGENT_RUNTIME_UPDATE, state2);
16663
17367
  });
16664
17368
  electron.ipcMain.handle(Channels.TAB_CREATE, (_, url) => {
@@ -16708,7 +17412,7 @@ function registerIpcHandlers(windowState, runtime) {
16708
17412
  (chunk) => sendToRendererViews(Channels.AI_STREAM_CHUNK, chunk),
16709
17413
  () => sendToRendererViews(Channels.AI_STREAM_END),
16710
17414
  tabManager,
16711
- runtime,
17415
+ runtime2,
16712
17416
  history
16713
17417
  );
16714
17418
  } catch (err) {
@@ -16791,46 +17495,50 @@ function registerIpcHandlers(windowState, runtime) {
16791
17495
  });
16792
17496
  electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
16793
17497
  electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key, value) => {
16794
- const updatedSettings = setSetting(key, value);
17498
+ if (!SETTABLE_KEYS.has(key)) {
17499
+ throw new Error(`Unknown setting key: ${key}`);
17500
+ }
17501
+ const settingsKey = key;
17502
+ const updatedSettings = setSetting(settingsKey, value);
16795
17503
  if (key === "approvalMode") {
16796
- runtime.setApprovalMode(value);
17504
+ runtime2.setApprovalMode(value);
16797
17505
  }
16798
17506
  if (key === "mcpPort") {
16799
17507
  await stopMcpServer();
16800
- await startMcpServer(tabManager, runtime, updatedSettings.mcpPort);
17508
+ await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
16801
17509
  }
16802
17510
  sendToRendererViews(Channels.SETTINGS_UPDATE, updatedSettings);
16803
17511
  return updatedSettings;
16804
17512
  });
16805
- electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime.getState());
16806
- electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime.pause());
16807
- electron.ipcMain.handle(Channels.AGENT_RESUME, () => runtime.resume());
17513
+ electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime2.getState());
17514
+ electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime2.pause());
17515
+ electron.ipcMain.handle(Channels.AGENT_RESUME, () => runtime2.resume());
16808
17516
  electron.ipcMain.handle(
16809
17517
  Channels.AGENT_SET_APPROVAL_MODE,
16810
17518
  (_, mode) => {
16811
17519
  setSetting("approvalMode", mode);
16812
- return runtime.setApprovalMode(mode);
17520
+ return runtime2.setApprovalMode(mode);
16813
17521
  }
16814
17522
  );
16815
17523
  electron.ipcMain.handle(
16816
17524
  Channels.AGENT_APPROVAL_RESOLVE,
16817
- (_, approvalId, approved) => runtime.resolveApproval(approvalId, approved)
17525
+ (_, approvalId, approved) => runtime2.resolveApproval(approvalId, approved)
16818
17526
  );
16819
17527
  electron.ipcMain.handle(
16820
17528
  Channels.AGENT_CHECKPOINT_CREATE,
16821
- (_, name, note) => runtime.createCheckpoint(name, note)
17529
+ (_, name, note) => runtime2.createCheckpoint(name, note)
16822
17530
  );
16823
17531
  electron.ipcMain.handle(
16824
17532
  Channels.AGENT_CHECKPOINT_RESTORE,
16825
- (_, checkpointId) => runtime.restoreCheckpoint(checkpointId)
17533
+ (_, checkpointId) => runtime2.restoreCheckpoint(checkpointId)
16826
17534
  );
16827
17535
  electron.ipcMain.handle(
16828
17536
  Channels.AGENT_SESSION_CAPTURE,
16829
- (_, note) => runtime.captureSession(note)
17537
+ (_, note) => runtime2.captureSession(note)
16830
17538
  );
16831
17539
  electron.ipcMain.handle(
16832
17540
  Channels.AGENT_SESSION_RESTORE,
16833
- (_, snapshot) => runtime.restoreSession(snapshot)
17541
+ (_, snapshot) => runtime2.restoreSession(snapshot)
16834
17542
  );
16835
17543
  electron.ipcMain.handle(Channels.BOOKMARKS_GET, () => {
16836
17544
  return getState();
@@ -16864,36 +17572,12 @@ function registerIpcHandlers(windowState, runtime) {
16864
17572
  return { success: false, message: "No active tab" };
16865
17573
  }
16866
17574
  const wc = activeTab.view.webContents;
16867
- if (wc.isDestroyed()) {
16868
- return { success: false, message: "Tab is not available" };
16869
- }
16870
- const url = wc.getURL();
16871
- if (!url || url === "about:blank") {
16872
- return { success: false, message: "No page loaded" };
16873
- }
16874
- const selectedText = await wc.executeJavaScript(`
16875
- (function() {
16876
- var sel = window.getSelection();
16877
- return sel ? sel.toString().trim() : '';
16878
- })()
16879
- `);
16880
- if (!selectedText) {
16881
- return { success: false, message: "No text selected" };
17575
+ const result = await captureSelectionHighlight(wc);
17576
+ if (result.success && result.text) {
17577
+ await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(() => {
17578
+ });
16882
17579
  }
16883
- const capped = selectedText.length > 5e3 ? selectedText.slice(0, 5e3) : selectedText;
16884
- const highlight = addHighlight(
16885
- url,
16886
- void 0,
16887
- capped,
16888
- void 0,
16889
- "yellow",
16890
- "user"
16891
- );
16892
- await highlightOnPage(wc, null, capped, void 0, void 0, "yellow").catch(
16893
- () => {
16894
- }
16895
- );
16896
- return { success: true, text: capped, id: highlight.id };
17580
+ return result;
16897
17581
  } catch {
16898
17582
  return { success: false, message: "Could not capture selection" };
16899
17583
  }
@@ -16909,24 +17593,11 @@ function registerIpcHandlers(windowState, runtime) {
16909
17593
  if (wc.isDestroyed()) return;
16910
17594
  const tab = tabManager.findTabByWebContentsId(wc.id);
16911
17595
  if (!tab || !tab.highlightModeActive) return;
16912
- const url = wc.getURL();
16913
- if (!url || url === "about:blank") return;
16914
- const capped = text.length > 5e3 ? text.slice(0, 5e3) : text;
16915
- const highlight = addHighlight(
16916
- url,
16917
- void 0,
16918
- capped,
16919
- void 0,
16920
- "yellow",
16921
- "user"
16922
- );
16923
- if (!chromeView.webContents.isDestroyed()) {
16924
- chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, {
16925
- success: true,
16926
- text: capped,
16927
- id: highlight.id
16928
- });
16929
- }
17596
+ void persistAndMarkHighlight(wc, text).then((result) => {
17597
+ if (result.success && !chromeView.webContents.isDestroyed()) {
17598
+ chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, result);
17599
+ }
17600
+ });
16930
17601
  } catch {
16931
17602
  }
16932
17603
  });
@@ -16936,9 +17607,7 @@ function registerIpcHandlers(windowState, runtime) {
16936
17607
  const wc = tab.view.webContents;
16937
17608
  if (wc.isDestroyed()) return 0;
16938
17609
  try {
16939
- return wc.executeJavaScript(
16940
- `document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text').length`
16941
- );
17610
+ return getHighlightCount(wc);
16942
17611
  } catch {
16943
17612
  return 0;
16944
17613
  }
@@ -16949,20 +17618,7 @@ function registerIpcHandlers(windowState, runtime) {
16949
17618
  const wc = tab.view.webContents;
16950
17619
  if (wc.isDestroyed()) return false;
16951
17620
  try {
16952
- return wc.executeJavaScript(`
16953
- (function() {
16954
- var highlights = document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text');
16955
- if (${index} < 0 || ${index} >= highlights.length) return false;
16956
- // Remove focus ring from all highlights
16957
- highlights.forEach(function(h) { h.style.removeProperty('outline'); h.style.removeProperty('outline-offset'); });
16958
- var target = highlights[${index}];
16959
- target.scrollIntoView({ behavior: 'smooth', block: 'center' });
16960
- // Add focus ring to current highlight
16961
- target.style.setProperty('outline', '2px solid rgba(255, 255, 255, 0.9)', 'important');
16962
- target.style.setProperty('outline-offset', '2px', 'important');
16963
- return true;
16964
- })()
16965
- `);
17621
+ return scrollToHighlight(wc, index);
16966
17622
  } catch {
16967
17623
  return false;
16968
17624
  }
@@ -16973,32 +17629,7 @@ function registerIpcHandlers(windowState, runtime) {
16973
17629
  const wc = tab.view.webContents;
16974
17630
  if (wc.isDestroyed()) return false;
16975
17631
  try {
16976
- return wc.executeJavaScript(`
16977
- (function() {
16978
- var highlights = document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text');
16979
- if (${index} < 0 || ${index} >= highlights.length) return false;
16980
- var el = highlights[${index}];
16981
- // Remove associated label if any
16982
- document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) {
16983
- if (b.__vesselAnchor === el) b.remove();
16984
- });
16985
- // Unwrap text highlights, remove class from element highlights
16986
- if (el.tagName === 'MARK' && el.classList.contains('__vessel-highlight-text')) {
16987
- var parent = el.parentNode;
16988
- while (el.firstChild) parent.insertBefore(el.firstChild, el);
16989
- parent.removeChild(el);
16990
- parent.normalize();
16991
- } else {
16992
- el.classList.remove('__vessel-highlight');
16993
- el.style.removeProperty('background');
16994
- el.style.removeProperty('outline-color');
16995
- el.style.removeProperty('box-shadow');
16996
- el.style.removeProperty('outline');
16997
- el.style.removeProperty('outline-offset');
16998
- }
16999
- return true;
17000
- })()
17001
- `);
17632
+ return removeHighlightAtIndex(wc, index);
17002
17633
  } catch {
17003
17634
  return false;
17004
17635
  }
@@ -17009,39 +17640,65 @@ function registerIpcHandlers(windowState, runtime) {
17009
17640
  const wc = tab.view.webContents;
17010
17641
  if (wc.isDestroyed()) return false;
17011
17642
  try {
17012
- return wc.executeJavaScript(`
17013
- (function() {
17014
- // Remove all labels
17015
- document.querySelectorAll('.__vessel-highlight-label[data-vessel-highlight]').forEach(function(b) { b.remove(); });
17016
- // Unwrap text highlights
17017
- document.querySelectorAll('.__vessel-highlight-text').forEach(function(mark) {
17018
- var parent = mark.parentNode;
17019
- while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
17020
- parent.removeChild(mark);
17021
- parent.normalize();
17022
- });
17023
- // Remove element highlights
17024
- document.querySelectorAll('.__vessel-highlight').forEach(function(el) {
17025
- el.classList.remove('__vessel-highlight');
17026
- el.style.removeProperty('background');
17027
- el.style.removeProperty('outline-color');
17028
- el.style.removeProperty('box-shadow');
17029
- el.style.removeProperty('outline');
17030
- el.style.removeProperty('outline-offset');
17031
- });
17032
- return true;
17033
- })()
17034
- `);
17643
+ return clearAllHighlightElements(wc);
17035
17644
  } catch {
17036
17645
  return false;
17037
17646
  }
17038
17647
  });
17648
+ let findWiredWcId = null;
17649
+ function wireFindEvents(wc) {
17650
+ if (findWiredWcId === wc.id) return;
17651
+ if (findWiredWcId !== null) {
17652
+ const prev = tabManager.findTabByWebContentsId(findWiredWcId);
17653
+ if (prev) prev.view.webContents.removeAllListeners("found-in-page");
17654
+ }
17655
+ findWiredWcId = wc.id;
17656
+ wc.on("found-in-page", (_event, result) => {
17657
+ if (!chromeView.webContents.isDestroyed()) {
17658
+ chromeView.webContents.send(Channels.FIND_IN_PAGE_RESULT, result);
17659
+ }
17660
+ });
17661
+ }
17662
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_START, (_, text, options) => {
17663
+ const tab = tabManager.getActiveTab();
17664
+ if (!tab) return null;
17665
+ const wc = tab.view.webContents;
17666
+ if (wc.isDestroyed()) return null;
17667
+ wireFindEvents(wc);
17668
+ return wc.findInPage(text, {
17669
+ forward: options?.forward ?? true,
17670
+ findNext: options?.findNext ?? false
17671
+ });
17672
+ });
17673
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_NEXT, (_, forward) => {
17674
+ const tab = tabManager.getActiveTab();
17675
+ if (!tab) return null;
17676
+ const wc = tab.view.webContents;
17677
+ if (wc.isDestroyed()) return null;
17678
+ return wc.findInPage("", { forward: forward ?? true, findNext: true });
17679
+ });
17680
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_STOP, (_, action) => {
17681
+ const tab = tabManager.getActiveTab();
17682
+ if (!tab) return;
17683
+ const wc = tab.view.webContents;
17684
+ if (wc.isDestroyed()) return;
17685
+ wc.stopFindInPage(action ?? "clearSelection");
17686
+ });
17687
+ electron.ipcMain.handle(Channels.HISTORY_GET, () => {
17688
+ return getState$1();
17689
+ });
17690
+ electron.ipcMain.handle(Channels.HISTORY_SEARCH, (_, query) => {
17691
+ return search(query);
17692
+ });
17693
+ electron.ipcMain.handle(Channels.HISTORY_CLEAR, () => {
17694
+ clearAll$1();
17695
+ });
17039
17696
  electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => {
17040
17697
  windowState.uiState.devtoolsPanelOpen = !windowState.uiState.devtoolsPanelOpen;
17041
17698
  layoutViews(windowState);
17042
17699
  return { open: windowState.uiState.devtoolsPanelOpen };
17043
17700
  });
17044
- electron.ipcMain.handle("devtools-panel:resize", (_, height) => {
17701
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE, (_, height) => {
17045
17702
  const clamped = Math.max(MIN_DEVTOOLS_PANEL, Math.min(MAX_DEVTOOLS_PANEL, Math.round(height)));
17046
17703
  windowState.uiState.devtoolsPanelHeight = clamped;
17047
17704
  layoutViews(windowState);
@@ -17062,6 +17719,7 @@ function registerIpcHandlers(windowState, runtime) {
17062
17719
  });
17063
17720
  }
17064
17721
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
17722
+ const PERSIST_DEBOUNCE_MS = 500;
17065
17723
  function clone(value) {
17066
17724
  return JSON.parse(JSON.stringify(value));
17067
17725
  }
@@ -17139,7 +17797,7 @@ class AgentRuntime {
17139
17797
  createCheckpoint(name, note) {
17140
17798
  const snapshot = this.captureSession(note);
17141
17799
  const checkpoint = {
17142
- id: node_crypto.randomUUID(),
17800
+ id: crypto$1.randomUUID(),
17143
17801
  name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
17144
17802
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
17145
17803
  note: note?.trim() || void 0,
@@ -17193,7 +17851,7 @@ class AgentRuntime {
17193
17851
  }
17194
17852
  }
17195
17853
  const entry = {
17196
- id: node_crypto.randomUUID(),
17854
+ id: crypto$1.randomUUID(),
17197
17855
  source: input.source,
17198
17856
  kind,
17199
17857
  title: input.title?.trim() || void 0,
@@ -17217,7 +17875,7 @@ class AgentRuntime {
17217
17875
  // --- Speedee Flow State ---
17218
17876
  startFlow(goal, steps, startUrl) {
17219
17877
  const flow = {
17220
- id: node_crypto.randomUUID(),
17878
+ id: crypto$1.randomUUID(),
17221
17879
  goal,
17222
17880
  steps: steps.map((label) => ({ label, status: "pending" })),
17223
17881
  currentStepIndex: 0,
@@ -17300,7 +17958,6 @@ ${progress}
17300
17958
  mode: "replace"
17301
17959
  });
17302
17960
  const approvalReason = this.getApprovalReason(dangerous);
17303
- console.log(`[Vessel Runtime] action=${name} dangerous=${dangerous} approvalReason=${approvalReason} mode=${this.state.supervisor.approvalMode}`);
17304
17961
  if (approvalReason) {
17305
17962
  this.publishTranscript({
17306
17963
  source,
@@ -17310,9 +17967,7 @@ ${progress}
17310
17967
  streamId: transcriptStreamId,
17311
17968
  mode: "replace"
17312
17969
  });
17313
- console.log(`[Vessel Runtime] awaiting approval for ${name}...`);
17314
17970
  const approved = await this.awaitApproval(action, approvalReason);
17315
- console.log(`[Vessel Runtime] approval result for ${name}: ${approved}`);
17316
17971
  if (!approved) {
17317
17972
  this.publishTranscript({
17318
17973
  source,
@@ -17403,7 +18058,14 @@ ${progress}
17403
18058
  return sanitizePersistence(null);
17404
18059
  }
17405
18060
  }
17406
- persist() {
18061
+ persistTimer = null;
18062
+ persistDirty = false;
18063
+ persistNow() {
18064
+ this.persistDirty = false;
18065
+ if (this.persistTimer) {
18066
+ clearTimeout(this.persistTimer);
18067
+ this.persistTimer = null;
18068
+ }
17407
18069
  const persisted = {
17408
18070
  session: this.state.session,
17409
18071
  supervisor: {
@@ -17414,20 +18076,36 @@ ${progress}
17414
18076
  actions: this.state.actions.slice(-120),
17415
18077
  checkpoints: this.state.checkpoints.slice(-20)
17416
18078
  };
17417
- fs$1.mkdirSync(path$1.dirname(getRuntimeStatePath()), { recursive: true });
17418
- fs$1.writeFileSync(
17419
- getRuntimeStatePath(),
17420
- JSON.stringify(persisted, null, 2),
17421
- "utf-8"
17422
- );
18079
+ try {
18080
+ fs$1.mkdirSync(path$1.dirname(getRuntimeStatePath()), { recursive: true });
18081
+ fs$1.writeFileSync(
18082
+ getRuntimeStatePath(),
18083
+ JSON.stringify(persisted, null, 2),
18084
+ "utf-8"
18085
+ );
18086
+ } catch (err) {
18087
+ console.error("[Vessel] Failed to persist runtime state:", err);
18088
+ }
18089
+ }
18090
+ schedulePersist() {
18091
+ this.persistDirty = true;
18092
+ if (this.persistTimer) return;
18093
+ this.persistTimer = setTimeout(() => {
18094
+ this.persistTimer = null;
18095
+ if (this.persistDirty) this.persistNow();
18096
+ }, PERSIST_DEBOUNCE_MS);
18097
+ }
18098
+ /** Flush any pending debounced persist to disk immediately. Call on shutdown. */
18099
+ flushPersist() {
18100
+ if (this.persistDirty) this.persistNow();
17423
18101
  }
17424
18102
  emit() {
17425
- this.persist();
18103
+ this.schedulePersist();
17426
18104
  this.updateListener?.(this.getState());
17427
18105
  }
17428
18106
  startAction(input) {
17429
18107
  const action = {
17430
- id: node_crypto.randomUUID(),
18108
+ id: crypto$1.randomUUID(),
17431
18109
  source: input.source,
17432
18110
  name: input.name,
17433
18111
  args: clone(input.args),
@@ -17461,7 +18139,7 @@ ${progress}
17461
18139
  /** Aggregate metrics for all completed actions in this session. */
17462
18140
  getMetrics() {
17463
18141
  const completed = this.state.actions.filter((a) => a.status === "completed");
17464
- const failed = this.state.actions.filter((a) => a.status === "error");
18142
+ const failed = this.state.actions.filter((a) => a.status === "failed");
17465
18143
  const durations = completed.filter((a) => a.durationMs != null).map((a) => a.durationMs);
17466
18144
  const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
17467
18145
  const toolBreakdown = {};
@@ -17470,7 +18148,7 @@ ${progress}
17470
18148
  if (!toolBreakdown[name]) toolBreakdown[name] = { count: 0, totalMs: 0, avgMs: 0, errors: 0 };
17471
18149
  toolBreakdown[name].count++;
17472
18150
  if (action.durationMs != null) toolBreakdown[name].totalMs += action.durationMs;
17473
- if (action.status === "error") toolBreakdown[name].errors++;
18151
+ if (action.status === "failed") toolBreakdown[name].errors++;
17474
18152
  }
17475
18153
  for (const entry of Object.values(toolBreakdown)) {
17476
18154
  entry.avgMs = entry.count > 0 ? Math.round(entry.totalMs / entry.count) : 0;
@@ -17499,7 +18177,7 @@ ${progress}
17499
18177
  }
17500
18178
  awaitApproval(action, reason) {
17501
18179
  const approval = {
17502
- id: node_crypto.randomUUID(),
18180
+ id: crypto$1.randomUUID(),
17503
18181
  actionId: action.id,
17504
18182
  source: action.source,
17505
18183
  name: action.name,
@@ -17625,6 +18303,41 @@ function installAdBlocking(tabManager) {
17625
18303
  callback({ cancel: shouldBlockRequest(details) });
17626
18304
  });
17627
18305
  }
18306
+ function installDownloadHandler(chromeView) {
18307
+ electron.session.defaultSession.on("will-download", (_event, item) => {
18308
+ const settings2 = loadSettings();
18309
+ const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
18310
+ const filename = item.getFilename();
18311
+ const savePath = path.join(downloadDir, filename);
18312
+ item.setSavePath(savePath);
18313
+ const info = {
18314
+ filename,
18315
+ savePath,
18316
+ totalBytes: item.getTotalBytes(),
18317
+ receivedBytes: 0,
18318
+ state: "progressing"
18319
+ };
18320
+ if (!chromeView.webContents.isDestroyed()) {
18321
+ chromeView.webContents.send(Channels.DOWNLOAD_STARTED, info);
18322
+ }
18323
+ item.on("updated", (_event2, state2) => {
18324
+ info.receivedBytes = item.getReceivedBytes();
18325
+ info.totalBytes = item.getTotalBytes();
18326
+ info.state = state2 === "progressing" ? "progressing" : "interrupted";
18327
+ if (!chromeView.webContents.isDestroyed()) {
18328
+ chromeView.webContents.send(Channels.DOWNLOAD_PROGRESS, info);
18329
+ }
18330
+ });
18331
+ item.once("done", (_event2, state2) => {
18332
+ info.receivedBytes = item.getReceivedBytes();
18333
+ info.state = state2 === "completed" ? "completed" : "cancelled";
18334
+ if (!chromeView.webContents.isDestroyed()) {
18335
+ chromeView.webContents.send(Channels.DOWNLOAD_DONE, info);
18336
+ }
18337
+ });
18338
+ });
18339
+ }
18340
+ let runtime = null;
17628
18341
  function rendererUrlFor(view) {
17629
18342
  if (!process.env.ELECTRON_RENDERER_URL) return null;
17630
18343
  const url = new URL(process.env.ELECTRON_RENDERER_URL);
@@ -17705,7 +18418,6 @@ async function bootstrap() {
17705
18418
  if (settings2.clearBookmarksOnLaunch) {
17706
18419
  clearAll();
17707
18420
  }
17708
- let runtime = null;
17709
18421
  const windowState = createMainWindow((tabs, activeId) => {
17710
18422
  windowState.chromeView.webContents.send(
17711
18423
  Channels.TAB_STATE_UPDATE,
@@ -17727,15 +18439,10 @@ async function bootstrap() {
17727
18439
  const registerHighlightShortcut = () => {
17728
18440
  electron.globalShortcut.unregister("CommandOrControl+H");
17729
18441
  const success = electron.globalShortcut.register("CommandOrControl+H", () => {
17730
- console.log("[Vessel] Ctrl+H shortcut triggered");
17731
18442
  const activeTab = tabManager.getActiveTab();
17732
- if (!activeTab) {
17733
- console.log("[Vessel] No active tab");
17734
- return;
17735
- }
18443
+ if (!activeTab) return;
17736
18444
  tabManager.captureHighlightFromActiveTab();
17737
18445
  });
17738
- console.log("[Vessel] Ctrl+H shortcut registered:", success);
17739
18446
  if (!success) {
17740
18447
  console.warn("[Vessel] Failed to register Ctrl+H shortcut");
17741
18448
  }
@@ -17761,6 +18468,11 @@ async function bootstrap() {
17761
18468
  chromeView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
17762
18469
  sidebarView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
17763
18470
  });
18471
+ subscribe$1((state2) => {
18472
+ chromeView.webContents.send(Channels.HISTORY_UPDATE, state2);
18473
+ sidebarView.webContents.send(Channels.HISTORY_UPDATE, state2);
18474
+ });
18475
+ installDownloadHandler(chromeView);
17764
18476
  const chromeUrl = rendererUrlFor("chrome");
17765
18477
  const sidebarUrl = rendererUrlFor("sidebar");
17766
18478
  const devtoolsUrl = rendererUrlFor("devtools");
@@ -17800,6 +18512,7 @@ electron.app.whenReady().then(bootstrap).catch((error) => {
17800
18512
  });
17801
18513
  electron.app.on("window-all-closed", () => {
17802
18514
  electron.globalShortcut.unregisterAll();
18515
+ runtime?.flushPersist();
17803
18516
  void stopMcpServer().finally(() => {
17804
18517
  electron.app.quit();
17805
18518
  });