@quanta-intellect/vessel-browser 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/main/index.js CHANGED
@@ -8,11 +8,125 @@ const Anthropic = require("@anthropic-ai/sdk");
8
8
  const OpenAI = require("openai");
9
9
  const zod = require("zod");
10
10
  const path$1 = require("node:path");
11
- const node_crypto = require("node:crypto");
11
+ const crypto$1 = require("node:crypto");
12
12
  const http = require("node:http");
13
13
  const os = require("node:os");
14
14
  const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
15
15
  const streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
16
+ const defaults = {
17
+ defaultUrl: "https://start.duckduckgo.com",
18
+ theme: "dark",
19
+ sidebarWidth: 340,
20
+ mcpPort: 3100,
21
+ autoRestoreSession: true,
22
+ clearBookmarksOnLaunch: false,
23
+ obsidianVaultPath: "",
24
+ approvalMode: "confirm-dangerous",
25
+ agentTranscriptMode: "summary",
26
+ chatProvider: null,
27
+ maxToolIterations: 200,
28
+ domainPolicy: { allowedDomains: [], blockedDomains: [] },
29
+ downloadPath: ""
30
+ };
31
+ const SETTABLE_KEYS = new Set(Object.keys(defaults));
32
+ let settings = null;
33
+ let settingsIssues = [];
34
+ function getSettingsPath() {
35
+ return path.join(electron.app.getPath("userData"), "vessel-settings.json");
36
+ }
37
+ function getSettingsLoadIssues() {
38
+ return settingsIssues.map((issue) => ({ ...issue }));
39
+ }
40
+ function sanitizePort(value) {
41
+ const parsed = Number(value);
42
+ if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) {
43
+ return parsed;
44
+ }
45
+ settingsIssues.push({
46
+ code: "settings-invalid-mcp-port",
47
+ severity: "warning",
48
+ title: "Invalid MCP port in settings",
49
+ detail: `Expected an integer between 1 and 65535 but found ${JSON.stringify(value)}.`,
50
+ action: `Using default port ${defaults.mcpPort} instead.`
51
+ });
52
+ return defaults.mcpPort;
53
+ }
54
+ function loadSettings() {
55
+ if (settings) return settings;
56
+ settingsIssues = [];
57
+ try {
58
+ const raw = fs.readFileSync(getSettingsPath(), "utf-8");
59
+ const parsed = JSON.parse(raw);
60
+ delete parsed.apiKey;
61
+ delete parsed.provider;
62
+ settings = {
63
+ ...defaults,
64
+ ...parsed,
65
+ mcpPort: sanitizePort(parsed.mcpPort ?? defaults.mcpPort),
66
+ agentTranscriptMode: parsed.agentTranscriptMode === "off" || parsed.agentTranscriptMode === "summary" || parsed.agentTranscriptMode === "full" ? parsed.agentTranscriptMode : parsed.showAgentTranscript === false ? "off" : defaults.agentTranscriptMode
67
+ };
68
+ } catch (error) {
69
+ if (fs.existsSync(getSettingsPath())) {
70
+ settingsIssues.push({
71
+ code: "settings-read-failed",
72
+ severity: "warning",
73
+ title: "Could not read Vessel settings",
74
+ detail: error instanceof Error ? error.message : "Unknown settings error.",
75
+ action: "Falling back to built-in defaults for this launch."
76
+ });
77
+ }
78
+ settings = { ...defaults };
79
+ }
80
+ return settings;
81
+ }
82
+ function saveSettings() {
83
+ try {
84
+ fs.mkdirSync(path.dirname(getSettingsPath()), { recursive: true });
85
+ fs.writeFileSync(getSettingsPath(), JSON.stringify(settings, null, 2));
86
+ } catch (err) {
87
+ console.error("[Vessel] Failed to save settings:", err);
88
+ }
89
+ }
90
+ function setSetting(key, value) {
91
+ loadSettings();
92
+ if (key === "mcpPort") {
93
+ settings.mcpPort = sanitizePort(value);
94
+ } else {
95
+ settings[key] = value;
96
+ }
97
+ saveSettings();
98
+ return { ...settings };
99
+ }
100
+ function checkDomainPolicy(url) {
101
+ if (!url || url.startsWith("about:")) return null;
102
+ const settings2 = loadSettings();
103
+ const policy = settings2.domainPolicy;
104
+ if (policy.allowedDomains.length === 0 && policy.blockedDomains.length === 0) {
105
+ return null;
106
+ }
107
+ let hostname;
108
+ try {
109
+ hostname = new URL(url).hostname.toLowerCase();
110
+ } catch {
111
+ return null;
112
+ }
113
+ if (policy.allowedDomains.length > 0) {
114
+ const allowed = policy.allowedDomains.some(
115
+ (d) => matchesDomain(hostname, d.toLowerCase())
116
+ );
117
+ return allowed ? null : `Navigation blocked by domain policy: ${hostname} is not in the allowed domains list.`;
118
+ }
119
+ if (policy.blockedDomains.length > 0) {
120
+ const blocked = policy.blockedDomains.some(
121
+ (d) => matchesDomain(hostname, d.toLowerCase())
122
+ );
123
+ return blocked ? `Navigation blocked by domain policy: ${hostname} is in the blocked domains list.` : null;
124
+ }
125
+ return null;
126
+ }
127
+ function matchesDomain(hostname, policyDomain) {
128
+ return hostname === policyDomain || hostname.endsWith("." + policyDomain);
129
+ }
16
130
  const MAX_CUSTOM_HISTORY = 50;
17
131
  class Tab {
18
132
  id;
@@ -60,7 +174,8 @@ class Tab {
60
174
  canGoBack: false,
61
175
  canGoForward: false,
62
176
  isReaderMode: false,
63
- adBlockingEnabled: options?.adBlockingEnabled ?? true
177
+ adBlockingEnabled: options?.adBlockingEnabled ?? true,
178
+ role: options?.role
64
179
  };
65
180
  this.view.webContents.on("before-input-event", (_event, input) => {
66
181
  if (!input.control && !input.meta) return;
@@ -250,7 +365,13 @@ class Tab {
250
365
  url = `https://duckduckgo.com/?q=${encodeURIComponent(url)}`;
251
366
  }
252
367
  }
368
+ if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
369
+ return `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`;
370
+ }
371
+ const policyError = checkDomainPolicy(url);
372
+ if (policyError) return policyError;
253
373
  this.view.webContents.loadURL(url);
374
+ return null;
254
375
  }
255
376
  goBack() {
256
377
  const previousUrl = this.urlHistory.pop();
@@ -388,31 +509,35 @@ class Tab {
388
509
  this.view.webContents.close();
389
510
  }
390
511
  }
391
- let state$2 = null;
392
- const listeners$1 = /* @__PURE__ */ new Set();
512
+ let state$3 = null;
513
+ const listeners$2 = /* @__PURE__ */ new Set();
393
514
  function getHighlightsPath() {
394
515
  return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
395
516
  }
396
- function load$1() {
397
- if (state$2) return state$2;
517
+ function load$2() {
518
+ if (state$3) return state$3;
398
519
  try {
399
520
  const raw = fs.readFileSync(getHighlightsPath(), "utf-8");
400
521
  const parsed = JSON.parse(raw);
401
- state$2 = {
522
+ state$3 = {
402
523
  highlights: Array.isArray(parsed.highlights) ? parsed.highlights : []
403
524
  };
404
525
  } catch {
405
- state$2 = { highlights: [] };
526
+ state$3 = { highlights: [] };
406
527
  }
407
- return state$2;
528
+ return state$3;
408
529
  }
409
- function save$1() {
410
- fs.writeFileSync(getHighlightsPath(), JSON.stringify(state$2, null, 2), "utf-8");
530
+ function save$2() {
531
+ try {
532
+ fs.writeFileSync(getHighlightsPath(), JSON.stringify(state$3, null, 2), "utf-8");
533
+ } catch (err) {
534
+ console.error("[Vessel] Failed to save highlights:", err);
535
+ }
411
536
  }
412
- function emit$1() {
413
- if (!state$2) return;
414
- const snapshot = { highlights: [...state$2.highlights] };
415
- for (const listener of listeners$1) {
537
+ function emit$2() {
538
+ if (!state$3) return;
539
+ const snapshot = { highlights: [...state$3.highlights] };
540
+ for (const listener of listeners$2) {
416
541
  listener(snapshot);
417
542
  }
418
543
  }
@@ -425,17 +550,17 @@ function normalizeUrl(rawUrl) {
425
550
  return rawUrl;
426
551
  }
427
552
  }
428
- function getState$1() {
429
- load$1();
430
- return { highlights: [...state$2.highlights] };
553
+ function getState$2() {
554
+ load$2();
555
+ return { highlights: [...state$3.highlights] };
431
556
  }
432
557
  function getHighlightsForUrl(url) {
433
- load$1();
558
+ load$2();
434
559
  const normalized = normalizeUrl(url);
435
- return state$2.highlights.filter((h) => h.url === normalized);
560
+ return state$3.highlights.filter((h) => h.url === normalized);
436
561
  }
437
562
  function addHighlight(url, selector, text, label, color, source) {
438
- load$1();
563
+ load$2();
439
564
  const highlight = {
440
565
  id: crypto.randomUUID(),
441
566
  url: normalizeUrl(url),
@@ -446,45 +571,45 @@ function addHighlight(url, selector, text, label, color, source) {
446
571
  source: source || void 0,
447
572
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
448
573
  };
449
- state$2.highlights.push(highlight);
450
- save$1();
451
- emit$1();
574
+ state$3.highlights.push(highlight);
575
+ save$2();
576
+ emit$2();
452
577
  return highlight;
453
578
  }
454
579
  function removeHighlight(id) {
455
- load$1();
456
- const index = state$2.highlights.findIndex((h) => h.id === id);
580
+ load$2();
581
+ const index = state$3.highlights.findIndex((h) => h.id === id);
457
582
  if (index === -1) return null;
458
- const [removed] = state$2.highlights.splice(index, 1);
459
- save$1();
460
- emit$1();
583
+ const [removed] = state$3.highlights.splice(index, 1);
584
+ save$2();
585
+ emit$2();
461
586
  return removed;
462
587
  }
463
588
  function findHighlightByText(url, text) {
464
- load$1();
589
+ load$2();
465
590
  const normalized = normalizeUrl(url);
466
- return state$2.highlights.find(
591
+ return state$3.highlights.find(
467
592
  (h) => h.url === normalized && h.text && h.text === text
468
593
  ) ?? null;
469
594
  }
470
595
  function updateHighlightColor(id, color) {
471
- load$1();
472
- const highlight = state$2.highlights.find((h) => h.id === id);
596
+ load$2();
597
+ const highlight = state$3.highlights.find((h) => h.id === id);
473
598
  if (!highlight) return null;
474
599
  highlight.color = color;
475
- save$1();
476
- emit$1();
600
+ save$2();
601
+ emit$2();
477
602
  return highlight;
478
603
  }
479
604
  function clearHighlightsForUrl(url) {
480
- load$1();
605
+ load$2();
481
606
  const normalized = normalizeUrl(url);
482
- const before = state$2.highlights.length;
483
- state$2.highlights = state$2.highlights.filter((h) => h.url !== normalized);
484
- const removed = before - state$2.highlights.length;
607
+ const before = state$3.highlights.length;
608
+ state$3.highlights = state$3.highlights.filter((h) => h.url !== normalized);
609
+ const removed = before - state$3.highlights.length;
485
610
  if (removed > 0) {
486
- save$1();
487
- emit$1();
611
+ save$2();
612
+ emit$2();
488
613
  }
489
614
  return removed;
490
615
  }
@@ -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,
@@ -1836,130 +2217,53 @@ class TabManager {
1836
2217
  }
1837
2218
  const normalized = normalizeUrl(url);
1838
2219
  for (const id of this.order) {
1839
- const tab = this.tabs.get(id);
1840
- if (!tab) continue;
1841
- const wc = tab.view.webContents;
1842
- if (wc.isDestroyed()) continue;
1843
- try {
1844
- const tabUrl = normalizeUrl(wc.getURL());
1845
- if (tabUrl === normalized) {
1846
- void this.removeHighlightMarksForText(wc, text).then(() => {
1847
- void highlightOnPage(
1848
- wc,
1849
- null,
1850
- text,
1851
- void 0,
1852
- void 0,
1853
- color
1854
- ).catch(() => {
1855
- });
1856
- });
1857
- }
1858
- } catch {
1859
- }
1860
- }
1861
- this.highlightCaptureCallback?.({
1862
- success: true,
1863
- message: `Color changed to ${color}`
1864
- });
1865
- }
1866
- async removeHighlightMarksForText(wc, text) {
1867
- await wc.executeJavaScript(
1868
- `(function() {
1869
- var marks = document.querySelectorAll('mark.__vessel-highlight-text[data-vessel-highlight]');
1870
- marks.forEach(function(m) {
1871
- if (m.textContent === ${JSON.stringify(text)}) {
1872
- var parent = m.parentNode;
1873
- while (m.firstChild) parent.insertBefore(m.firstChild, m);
1874
- m.remove();
1875
- if (parent) parent.normalize();
1876
- }
1877
- });
1878
- })()`
1879
- ).catch(() => {
1880
- });
1881
- }
1882
- broadcastState() {
1883
- const states = this.getAllStates();
1884
- 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
- });
2220
+ const tab = this.tabs.get(id);
2221
+ if (!tab) continue;
2222
+ const wc = tab.view.webContents;
2223
+ if (wc.isDestroyed()) continue;
2224
+ try {
2225
+ const tabUrl = normalizeUrl(wc.getURL());
2226
+ if (tabUrl === normalized) {
2227
+ void this.removeHighlightMarksForText(wc, text).then(() => {
2228
+ void highlightOnPage(
2229
+ wc,
2230
+ null,
2231
+ text,
2232
+ void 0,
2233
+ void 0,
2234
+ color
2235
+ ).catch(() => {
2236
+ });
2237
+ });
2238
+ }
2239
+ } catch {
2240
+ }
1945
2241
  }
1946
- settings = { ...defaults };
2242
+ this.highlightCaptureCallback?.({
2243
+ success: true,
2244
+ message: `Color changed to ${color}`
2245
+ });
1947
2246
  }
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;
2247
+ async removeHighlightMarksForText(wc, text) {
2248
+ await wc.executeJavaScript(
2249
+ `(function() {
2250
+ var marks = document.querySelectorAll('mark.__vessel-highlight-text[data-vessel-highlight]');
2251
+ marks.forEach(function(m) {
2252
+ if (m.textContent === ${JSON.stringify(text)}) {
2253
+ var parent = m.parentNode;
2254
+ while (m.firstChild) parent.insertBefore(m.firstChild, m);
2255
+ m.remove();
2256
+ if (parent) parent.normalize();
2257
+ }
2258
+ });
2259
+ })()`
2260
+ ).catch(() => {
2261
+ });
2262
+ }
2263
+ broadcastState() {
2264
+ const states = this.getAllStates();
2265
+ this.onStateChange(states, this.activeTabId || "");
1960
2266
  }
1961
- saveSettings();
1962
- return { ...settings };
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,7 +4401,9 @@ 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
  }
4065
4409
  const DEFAULT_MAX_ITERATIONS$1 = 200;
@@ -6356,6 +6700,7 @@ const TOOL_DEFINITIONS = [
6356
6700
  description: "Read the current page using a scoped mode. Defaults to a minimal navigation-focused brief; use mode='debug' only when narrower modes are insufficient.",
6357
6701
  inputSchema: {
6358
6702
  mode: zod.z.enum([
6703
+ "glance",
6359
6704
  "summary",
6360
6705
  "interactives_only",
6361
6706
  "forms_only",
@@ -6365,7 +6710,7 @@ const TOOL_DEFINITIONS = [
6365
6710
  "full",
6366
6711
  "debug"
6367
6712
  ]).optional().describe(
6368
- "Read mode: visible_only/results_only/forms_only/summary/text_only for narrow reads, full/debug for the complete page dump"
6713
+ "Read mode: glance (fastest — viewport snapshot, no JS extraction, ideal for heavy pages), visible_only/results_only/forms_only/summary/text_only for narrow reads, full/debug for the complete page dump"
6369
6714
  )
6370
6715
  },
6371
6716
  tier: 0
@@ -7069,8 +7414,12 @@ function load() {
7069
7414
  return state;
7070
7415
  }
7071
7416
  function save() {
7072
- fs.mkdirSync(path.dirname(getBookmarksPath()), { recursive: true });
7073
- fs.writeFileSync(getBookmarksPath(), JSON.stringify(state, null, 2), "utf-8");
7417
+ try {
7418
+ fs.mkdirSync(path.dirname(getBookmarksPath()), { recursive: true });
7419
+ fs.writeFileSync(getBookmarksPath(), JSON.stringify(state, null, 2), "utf-8");
7420
+ } catch (err) {
7421
+ console.error("[Vessel] Failed to save bookmarks:", err);
7422
+ }
7074
7423
  }
7075
7424
  function emit() {
7076
7425
  if (!state) return;
@@ -7496,6 +7845,22 @@ function formatDeadLinkMessage(label, result) {
7496
7845
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
7497
7846
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
7498
7847
  }
7848
+ const ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
7849
+ function isSafeNavigationURL(url) {
7850
+ try {
7851
+ const parsed = new URL(url);
7852
+ return ALLOWED_SCHEMES.has(parsed.protocol);
7853
+ } catch {
7854
+ return false;
7855
+ }
7856
+ }
7857
+ function assertSafeURL(url) {
7858
+ if (!isSafeNavigationURL(url)) {
7859
+ throw new Error(
7860
+ `Blocked navigation to disallowed URL scheme: ${url.slice(0, 80)}`
7861
+ );
7862
+ }
7863
+ }
7499
7864
  const SESSION_VERSION = 1;
7500
7865
  function getSessionsDir() {
7501
7866
  return path$1.join(electron.app.getPath("userData"), "named-sessions");
@@ -7515,7 +7880,7 @@ function normalizeSessionName(name) {
7515
7880
  function sessionFileName(name) {
7516
7881
  const normalized = normalizeSessionName(name).toLowerCase();
7517
7882
  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);
7883
+ const hash = crypto$1.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
7519
7884
  return `${slug}-${hash}.json`;
7520
7885
  }
7521
7886
  function getSessionPath(name) {
@@ -7780,10 +8145,148 @@ const PAGE_SCRIPT_TIMEOUT = /* @__PURE__ */ Symbol("page-script-timeout");
7780
8145
  function pageBusyError(action) {
7781
8146
  return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
7782
8147
  }
8148
+ async function glanceExtract(wc) {
8149
+ const startMs = Date.now();
8150
+ const result = await executePageScript(
8151
+ wc,
8152
+ `(function() {
8153
+ var vw = window.innerWidth || document.documentElement.clientWidth || 0;
8154
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
8155
+ var sy = window.scrollY || window.pageYOffset || 0;
8156
+
8157
+ function inViewport(el) {
8158
+ var r = el.getBoundingClientRect();
8159
+ return r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw && r.width > 0 && r.height > 0;
8160
+ }
8161
+
8162
+ function label(el) {
8163
+ return (el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 120);
8164
+ }
8165
+
8166
+ // Headings visible on screen
8167
+ var headings = [];
8168
+ document.querySelectorAll('h1, h2, h3, h4').forEach(function(h) {
8169
+ if (!inViewport(h)) return;
8170
+ var t = (h.textContent || '').trim();
8171
+ if (t && t.length < 200) headings.push(h.tagName.toLowerCase() + ': ' + t);
8172
+ });
8173
+
8174
+ // Links visible on screen (deduplicated by text)
8175
+ var links = [];
8176
+ var seenLinks = {};
8177
+ var idx = 1;
8178
+ document.querySelectorAll('a[href]').forEach(function(a) {
8179
+ if (!inViewport(a)) return;
8180
+ var t = (a.textContent || '').trim().slice(0, 100);
8181
+ if (!t || t.length < 2 || seenLinks[t]) return;
8182
+ seenLinks[t] = true;
8183
+ links.push({ text: t, href: (a.href || '').slice(0, 200), index: idx++ });
8184
+ });
8185
+
8186
+ // Buttons visible on screen
8187
+ var buttons = [];
8188
+ document.querySelectorAll('button, [role="button"], input[type="submit"], input[type="button"]').forEach(function(b) {
8189
+ if (!inViewport(b)) return;
8190
+ var t = label(b);
8191
+ if (!t || t.length < 1) return;
8192
+ buttons.push({ text: t, index: idx++ });
8193
+ });
8194
+
8195
+ // Input fields visible on screen
8196
+ var inputs = [];
8197
+ document.querySelectorAll('input:not([type="hidden"]):not([type="submit"]):not([type="button"]), select, textarea').forEach(function(inp) {
8198
+ if (!inViewport(inp)) return;
8199
+ var type = (inp.type || inp.tagName.toLowerCase() || '').toLowerCase();
8200
+ var lbl = (inp.getAttribute('aria-label') || inp.getAttribute('placeholder') || inp.name || '').trim();
8201
+ inputs.push({ type: type, label: lbl.slice(0, 80), placeholder: (inp.getAttribute('placeholder') || '').slice(0, 80), index: idx++ });
8202
+ });
8203
+
8204
+ // Content snapshot from main content area using textContent (instant, no reflow)
8205
+ var roots = ['main', 'article', '[role="main"]', '#content', '.content', '.story-body'];
8206
+ var contentRoot = null;
8207
+ for (var i = 0; i < roots.length; i++) {
8208
+ contentRoot = document.querySelector(roots[i]);
8209
+ if (contentRoot && contentRoot.textContent.trim().length > 50) break;
8210
+ contentRoot = null;
8211
+ }
8212
+ var snippet = '';
8213
+ if (contentRoot) {
8214
+ snippet = contentRoot.textContent.replace(/[ \\t]+/g, ' ').replace(/(\\n\\s*){3,}/g, '\\n\\n').trim().slice(0, 8000);
8215
+ } else {
8216
+ // Fallback: grab text from visible elements only
8217
+ var parts = [];
8218
+ document.querySelectorAll('h1, h2, h3, p, li, td, span, div').forEach(function(el) {
8219
+ if (parts.length > 100 || !inViewport(el)) return;
8220
+ var t = (el.textContent || '').trim();
8221
+ if (t.length > 10 && t.length < 500) parts.push(t);
8222
+ });
8223
+ snippet = parts.join('\\n').slice(0, 8000);
8224
+ }
8225
+
8226
+ return {
8227
+ title: document.title || '',
8228
+ url: location.href,
8229
+ headings: headings.slice(0, 20),
8230
+ links: links.slice(0, 40),
8231
+ buttons: buttons.slice(0, 20),
8232
+ inputs: inputs.slice(0, 15),
8233
+ contentSnippet: snippet,
8234
+ viewportHeight: vh,
8235
+ viewportWidth: vw,
8236
+ scrollY: Math.round(sy),
8237
+ };
8238
+ })()`,
8239
+ { timeoutMs: 2500, label: "glance-extract" }
8240
+ );
8241
+ const elapsed = Date.now() - startMs;
8242
+ if (!result || result === PAGE_SCRIPT_TIMEOUT) {
8243
+ return [
8244
+ `# ${wc.getTitle() || "(untitled)"}`,
8245
+ `URL: ${wc.getURL()}`,
8246
+ "",
8247
+ "[read_page mode=glance — page JS thread is completely blocked, no content available]",
8248
+ "[Try: click or type_text to interact directly, or wait a few seconds and retry]"
8249
+ ].join("\n");
8250
+ }
8251
+ const sections = [
8252
+ `# ${result.title}`,
8253
+ `URL: ${result.url}`,
8254
+ `Viewport: ${result.viewportWidth}×${result.viewportHeight} scrollY=${result.scrollY}`,
8255
+ `[read_page mode=glance — ${elapsed}ms, showing what's visible on screen]`
8256
+ ];
8257
+ if (result.headings.length > 0) {
8258
+ sections.push("", "## Headings", ...result.headings);
8259
+ }
8260
+ if (result.inputs.length > 0) {
8261
+ sections.push("", "## Input Fields");
8262
+ for (const inp of result.inputs) {
8263
+ const desc = inp.label || inp.placeholder || inp.type;
8264
+ sections.push(` [#${inp.index}] ${inp.type}: ${desc}`);
8265
+ }
8266
+ }
8267
+ if (result.buttons.length > 0) {
8268
+ sections.push("", "## Buttons");
8269
+ for (const btn of result.buttons) {
8270
+ sections.push(` [#${btn.index}] ${btn.text}`);
8271
+ }
8272
+ }
8273
+ if (result.links.length > 0) {
8274
+ sections.push("", "## Visible Links");
8275
+ for (const link of result.links) {
8276
+ sections.push(` [#${link.index}] ${link.text}`);
8277
+ }
8278
+ }
8279
+ if (result.contentSnippet) {
8280
+ const truncated = result.contentSnippet.length > 6e3 ? result.contentSnippet.slice(0, 6e3) + "\n[truncated]" : result.contentSnippet;
8281
+ sections.push("", "## Page Content (viewport)", "", truncated);
8282
+ }
8283
+ return sections.join("\n");
8284
+ }
7783
8285
  function normalizeReadPageMode(mode, pageContent) {
7784
8286
  if (typeof mode === "string") {
7785
8287
  const normalized = mode.trim().toLowerCase();
7786
8288
  if (normalized === "debug") return "debug";
8289
+ if (normalized === "glance") return "glance";
7787
8290
  if (normalized === "full" || normalized === "summary" || normalized === "interactives_only" || normalized === "forms_only" || normalized === "text_only" || normalized === "visible_only" || normalized === "results_only") {
7788
8291
  return normalized;
7789
8292
  }
@@ -7911,10 +8414,62 @@ function waitForPotentialNavigation$1(wc, beforeUrl, timeout = 2500) {
7911
8414
  wc.on("page-title-updated", onNativeChange);
7912
8415
  });
7913
8416
  }
7914
- function getPostNavSummary(wc) {
8417
+ async function getPostNavSummary(wc) {
7915
8418
  const title = wc.getTitle();
7916
- return title ? `
8419
+ const titleLine = title ? `
7917
8420
  Page title: ${title}` : "";
8421
+ const overlaySignal = await executePageScript(
8422
+ wc,
8423
+ `(function() {
8424
+ var signals = [];
8425
+ // Body scroll lock is a strong overlay signal
8426
+ var bodyStyle = window.getComputedStyle(document.body);
8427
+ var htmlStyle = window.getComputedStyle(document.documentElement);
8428
+ if (bodyStyle.overflow === 'hidden' || htmlStyle.overflow === 'hidden') {
8429
+ signals.push('body-scroll-locked');
8430
+ }
8431
+ // Check for known consent manager containers
8432
+ var consentSelectors = [
8433
+ '#onetrust-consent-sdk', '#CybotCookiebotDialog', '[class*="consent-banner"]',
8434
+ '[class*="cookie-banner"]', '[class*="privacy-banner"]', '[id*="consent"]',
8435
+ '[class*="gdpr"]', '[data-testid*="consent"]', '[data-testid*="cookie"]',
8436
+ '.fc-consent-root', '#sp_message_container_', '[id*="trustarc"]',
8437
+ '[class*="cmp-"]', '[id*="cmp-"]'
8438
+ ];
8439
+ for (var i = 0; i < consentSelectors.length; i++) {
8440
+ try {
8441
+ var el = document.querySelector(consentSelectors[i]);
8442
+ if (el && el.offsetHeight > 50) {
8443
+ signals.push('consent-banner:' + consentSelectors[i]);
8444
+ break;
8445
+ }
8446
+ } catch(e) {}
8447
+ }
8448
+ // Check for large fixed/sticky elements covering viewport
8449
+ var vw = window.innerWidth || 0;
8450
+ var vh = window.innerHeight || 0;
8451
+ var vpArea = Math.max(1, vw * vh);
8452
+ var els = document.querySelectorAll('dialog[open], [role="dialog"], [aria-modal="true"]');
8453
+ if (els.length > 0) signals.push('dialog-open');
8454
+ if (signals.length === 0) {
8455
+ var fixed = document.querySelectorAll('div[style*="position: fixed"], div[style*="position:fixed"]');
8456
+ for (var j = 0; j < fixed.length && j < 20; j++) {
8457
+ var r = fixed[j].getBoundingClientRect();
8458
+ if ((r.width * r.height) / vpArea > 0.3) {
8459
+ signals.push('large-fixed-overlay');
8460
+ break;
8461
+ }
8462
+ }
8463
+ }
8464
+ return signals.length > 0 ? signals.join(', ') : null;
8465
+ })()`,
8466
+ { timeoutMs: 1500, label: "overlay-probe" }
8467
+ );
8468
+ if (overlaySignal && overlaySignal !== PAGE_SCRIPT_TIMEOUT) {
8469
+ return `${titleLine}
8470
+ WARNING: Blocking overlay detected (${overlaySignal}). Call clear_overlays or accept_cookies before reading the page.`;
8471
+ }
8472
+ return titleLine;
7918
8473
  }
7919
8474
  async function scrollPage$1(wc, deltaY) {
7920
8475
  const getScrollY = async () => {
@@ -8363,6 +8918,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
8363
8918
  }
8364
8919
  if (snapshot.url && snapshot.url !== wc.getURL()) {
8365
8920
  try {
8921
+ assertSafeURL(snapshot.url);
8366
8922
  await wc.loadURL(snapshot.url);
8367
8923
  await waitForLoad$1(wc, 3e3);
8368
8924
  return;
@@ -9007,6 +9563,71 @@ async function clickOverlayCandidate(wc, action) {
9007
9563
  const result = await clickResolvedSelector$1(wc, action.selector);
9008
9564
  return `${action.label || action.selector}: ${result}`;
9009
9565
  }
9566
+ async function tryDismissConsentIframe(wc) {
9567
+ try {
9568
+ const hasSignal = await executePageScript(
9569
+ wc,
9570
+ `(function() {
9571
+ var bs = window.getComputedStyle(document.body);
9572
+ var hs = window.getComputedStyle(document.documentElement);
9573
+ if (bs.overflow === 'hidden' || hs.overflow === 'hidden') return true;
9574
+ var sels = '#onetrust-consent-sdk, [class*="consent"], [class*="cookie-banner"], [id*="consent"], [id*="sp_message"], .fc-consent-root, [class*="cmp-"]';
9575
+ var el = document.querySelector(sels);
9576
+ return !!(el && el.offsetHeight > 20);
9577
+ })()`,
9578
+ { timeoutMs: 1e3, label: "iframe-consent-signal" }
9579
+ );
9580
+ if (!hasSignal || hasSignal === PAGE_SCRIPT_TIMEOUT) return null;
9581
+ const frames = wc.mainFrame.framesInSubtree;
9582
+ for (const frame of frames) {
9583
+ if (frame === wc.mainFrame) continue;
9584
+ try {
9585
+ const result = await frame.executeJavaScript(`
9586
+ (function() {
9587
+ var selectors = [
9588
+ 'button[title*="Accept"], button[title*="Agree"], button[title*="OK"]',
9589
+ '[class*="accept"], [class*="agree"], [class*="consent-accept"]',
9590
+ 'button[aria-label*="accept" i], button[aria-label*="agree" i]',
9591
+ '.sp_choice_type_11', '.message-component.message-button',
9592
+ ];
9593
+ // Try selectors first
9594
+ for (var i = 0; i < selectors.length; i++) {
9595
+ try {
9596
+ var els = document.querySelectorAll(selectors[i]);
9597
+ for (var j = 0; j < els.length; j++) {
9598
+ var el = els[j];
9599
+ if (!(el instanceof HTMLElement)) continue;
9600
+ var text = (el.textContent || '').trim().toLowerCase();
9601
+ if (/accept|agree|consent|got it|ok|continue|i understand/i.test(text) || el.offsetHeight > 0) {
9602
+ el.click();
9603
+ return 'Clicked iframe consent button: ' + text.slice(0, 60);
9604
+ }
9605
+ }
9606
+ } catch(e) {}
9607
+ }
9608
+ // Text-match fallback on all buttons
9609
+ var buttons = document.querySelectorAll('button, [role="button"], a.message-component');
9610
+ for (var k = 0; k < buttons.length; k++) {
9611
+ var btn = buttons[k];
9612
+ var label = (btn.textContent || '').trim().toLowerCase();
9613
+ if (/^(accept|agree|accept all|i agree|i accept|ok|got it|allow|continue|yes)$/i.test(label) ||
9614
+ /accept all|agree and|accept & continue|accept and continue/i.test(label)) {
9615
+ btn.click();
9616
+ return 'Clicked iframe consent button: ' + label.slice(0, 60);
9617
+ }
9618
+ }
9619
+ return null;
9620
+ })()
9621
+ `);
9622
+ if (result) return result;
9623
+ } catch {
9624
+ continue;
9625
+ }
9626
+ }
9627
+ } catch {
9628
+ }
9629
+ return null;
9630
+ }
9010
9631
  async function clearOverlays(wc, strategy = "auto") {
9011
9632
  const steps = [];
9012
9633
  let cleared = 0;
@@ -9018,7 +9639,15 @@ async function clearOverlays(wc, strategy = "auto") {
9018
9639
  (overlay2) => overlay2.blocksInteraction
9019
9640
  );
9020
9641
  if (blockingOverlays.length === 0) {
9021
- if (cleared === 0) return "No blocking overlays detected";
9642
+ if (cleared === 0) {
9643
+ const iframeResult = await tryDismissConsentIframe(wc);
9644
+ if (iframeResult) {
9645
+ steps.push(`Iframe consent: ${iframeResult}`);
9646
+ await sleep$1(500);
9647
+ return steps.join("\n");
9648
+ }
9649
+ return "No blocking overlays detected";
9650
+ }
9022
9651
  steps.push(`Overlays remaining: ${beforeState.total}`);
9023
9652
  steps.push("Page still blocked: false");
9024
9653
  return steps.join("\n");
@@ -9870,6 +10499,7 @@ async function submitForm$1(wc, args) {
9870
10499
  if (formInfo.params) {
9871
10500
  url.search = formInfo.params;
9872
10501
  }
10502
+ assertSafeURL(url.toString());
9873
10503
  wc.loadURL(url.toString());
9874
10504
  await waitForPotentialNavigation$1(wc, beforeUrl);
9875
10505
  const afterUrl = wc.getURL();
@@ -10164,7 +10794,7 @@ async function executeAction(name, args, ctx) {
10164
10794
  const created = ctx.tabManager.getActiveTab();
10165
10795
  if (created) {
10166
10796
  await waitForLoad$1(created.view.webContents);
10167
- return `Created tab ${createdId}${getPostNavSummary(created.view.webContents)}`;
10797
+ return `Created tab ${createdId}${await getPostNavSummary(created.view.webContents)}`;
10168
10798
  }
10169
10799
  return `Created tab ${createdId}`;
10170
10800
  }
@@ -10176,7 +10806,7 @@ async function executeAction(name, args, ctx) {
10176
10806
  }
10177
10807
  ctx.tabManager.navigateTab(tabId, args.url);
10178
10808
  await waitForLoad$1(wc);
10179
- return `Navigated to ${wc.getURL()}${getPostNavSummary(wc)}`;
10809
+ return `Navigated to ${wc.getURL()}${await getPostNavSummary(wc)}`;
10180
10810
  }
10181
10811
  case "go_back": {
10182
10812
  if (!tab || !wc || !tabId) return "Error: No active tab";
@@ -10187,7 +10817,7 @@ async function executeAction(name, args, ctx) {
10187
10817
  ctx.tabManager.goBack(tabId);
10188
10818
  await waitForLoad$1(wc);
10189
10819
  const afterUrl = wc.getURL();
10190
- return afterUrl !== beforeUrl ? `Went back to ${afterUrl}${getPostNavSummary(wc)}` : `Back action completed but page stayed on ${afterUrl}`;
10820
+ return afterUrl !== beforeUrl ? `Went back to ${afterUrl}${await getPostNavSummary(wc)}` : `Back action completed but page stayed on ${afterUrl}`;
10191
10821
  }
10192
10822
  case "go_forward": {
10193
10823
  if (!tab || !wc || !tabId) return "Error: No active tab";
@@ -10198,7 +10828,7 @@ async function executeAction(name, args, ctx) {
10198
10828
  ctx.tabManager.goForward(tabId);
10199
10829
  await waitForLoad$1(wc);
10200
10830
  const afterUrl = wc.getURL();
10201
- return afterUrl !== beforeUrl ? `Went forward to ${afterUrl}${getPostNavSummary(wc)}` : `Forward action completed but page stayed on ${afterUrl}`;
10831
+ return afterUrl !== beforeUrl ? `Went forward to ${afterUrl}${await getPostNavSummary(wc)}` : `Forward action completed but page stayed on ${afterUrl}`;
10202
10832
  }
10203
10833
  case "reload": {
10204
10834
  if (!wc || !tabId) return "Error: No active tab";
@@ -10317,6 +10947,10 @@ async function executeAction(name, args, ctx) {
10317
10947
  }
10318
10948
  case "read_page": {
10319
10949
  if (!wc) return "Error: No active tab";
10950
+ const requestedGlance = typeof args.mode === "string" && args.mode.trim().toLowerCase() === "glance";
10951
+ if (requestedGlance) {
10952
+ return glanceExtract(wc);
10953
+ }
10320
10954
  console.log("[Vessel read_page] starting extraction with 6s timeout");
10321
10955
  let content = null;
10322
10956
  try {
@@ -10335,7 +10969,29 @@ async function executeAction(name, args, ctx) {
10335
10969
  console.log(
10336
10970
  `[Vessel read_page] extraction result: ${content ? `content=${content.content.length}` : "null (timeout)"}`
10337
10971
  );
10338
- if (content) {
10972
+ if (!content || content.content.length === 0) {
10973
+ console.log("[Vessel read_page] content empty/null, trying quick iframe dismiss");
10974
+ try {
10975
+ const iframeResult = await Promise.race([
10976
+ tryDismissConsentIframe(wc),
10977
+ new Promise((resolve) => setTimeout(() => resolve(null), 2e3))
10978
+ ]);
10979
+ if (iframeResult) {
10980
+ console.log(`[Vessel read_page] iframe dismiss: ${iframeResult}`);
10981
+ await sleep$1(500);
10982
+ try {
10983
+ content = await Promise.race([
10984
+ extractContent(wc),
10985
+ new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
10986
+ ]);
10987
+ } catch {
10988
+ content = null;
10989
+ }
10990
+ }
10991
+ } catch {
10992
+ }
10993
+ }
10994
+ if (content && content.content.length > 0) {
10339
10995
  const liveSelectionSection = formatLiveSelectionSection(
10340
10996
  await captureLiveHighlightSnapshot(
10341
10997
  wc,
@@ -10367,16 +11023,8 @@ ${truncated}`;
10367
11023
  `Need more detail? Escalate with read_page(mode="debug") only if the narrow modes are insufficient.`
10368
11024
  ].filter(Boolean).join("\n\n");
10369
11025
  }
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");
11026
+ console.log("[Vessel read_page] falling back to glance mode");
11027
+ return glanceExtract(wc);
10380
11028
  }
10381
11029
  case "wait_for": {
10382
11030
  if (!wc) return "Error: No active tab";
@@ -11078,6 +11726,7 @@ ${steps.join("\n")}`;
11078
11726
  try {
11079
11727
  const url = new URL(searchInfo.formAction);
11080
11728
  url.searchParams.set(searchInfo.inputName || "q", query);
11729
+ assertSafeURL(url.toString());
11081
11730
  wc.loadURL(url.toString());
11082
11731
  await waitForPotentialNavigation$1(wc, beforeUrl);
11083
11732
  afterUrl = wc.getURL();
@@ -11147,9 +11796,20 @@ ${steps.join("\n")}`;
11147
11796
  '[aria-label="Accept cookies"]',
11148
11797
  '[aria-label="Accept all cookies"]',
11149
11798
  '[data-testid="cookie-accept"]',
11799
+ // CNN / WarnerMedia / common consent SDKs
11800
+ '[data-testid="consent-accept"]',
11801
+ '[data-testid="accept-all"]',
11802
+ 'button[class*="consent"][class*="accept"]',
11803
+ 'button[class*="privacy"][class*="accept"]',
11804
+ '.fc-cta-consent',
11805
+ '#sp_choice_button_accept',
11806
+ '.message-component.message-button.no-children.focusable.sp_choice_type_11',
11807
+ '[class*="truste"] [class*="accept"]',
11808
+ '[id*="consent-accept"]',
11809
+ '[class*="cmp-accept"]',
11150
11810
  ];
11151
11811
  // Also try text-matching on buttons
11152
- var textPatterns = ['accept all', 'accept cookies', 'allow all', 'allow cookies', 'agree', 'got it', 'ok', 'i agree', 'consent'];
11812
+ var textPatterns = ['accept all', 'accept cookies', 'allow all', 'allow cookies', 'agree', 'got it', 'ok', 'i agree', 'i accept', 'consent', 'continue', 'accept and continue', 'accept & continue'];
11153
11813
  for (var i = 0; i < selectors.length; i++) {
11154
11814
  var el = document.querySelector(selectors[i]);
11155
11815
  if (el && el instanceof HTMLElement) { el.click(); return "Dismissed cookie banner via: " + selectors[i]; }
@@ -11175,7 +11835,10 @@ ${steps.join("\n")}`;
11175
11835
  if (dismissed === PAGE_SCRIPT_TIMEOUT) {
11176
11836
  return pageBusyError("accept_cookies");
11177
11837
  }
11178
- return dismissed || "No cookie consent banner detected. Try dismiss_popup for other overlays.";
11838
+ if (dismissed) return dismissed;
11839
+ const iframeResult = await tryDismissConsentIframe(wc);
11840
+ if (iframeResult) return iframeResult;
11841
+ return "No cookie consent banner detected. Try dismiss_popup for other overlays.";
11179
11842
  }
11180
11843
  case "extract_table": {
11181
11844
  if (!wc) return "Error: No active tab";
@@ -11273,10 +11936,10 @@ ${JSON.stringify(tableJson, null, 2)}`;
11273
11936
  const flowCtx = ctx.runtime.getFlowContext();
11274
11937
  return result + await getPostActionState$1(ctx, name) + flowCtx;
11275
11938
  }
11276
- async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime, history) {
11939
+ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history) {
11277
11940
  const lowerQuery = query.toLowerCase().trim();
11278
11941
  const isSummarize = lowerQuery.startsWith("summarize") || lowerQuery.startsWith("tldr") || lowerQuery === "summary";
11279
- if (provider.streamAgentQuery && tabManager && activeWebContents && runtime) {
11942
+ if (provider.streamAgentQuery && tabManager && activeWebContents && runtime2) {
11280
11943
  try {
11281
11944
  const extractStart = Date.now();
11282
11945
  const pageContent = await extractContent(activeWebContents);
@@ -11289,7 +11952,7 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
11289
11952
  pageContent,
11290
11953
  defaultReadMode
11291
11954
  );
11292
- const runtimeState = runtime.getState();
11955
+ const runtimeState = runtime2.getState();
11293
11956
  const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
11294
11957
  const activeTabTitle = pageContent.title || "(untitled)";
11295
11958
  const activeTabUrl = pageContent.url || activeWebContents.getURL();
@@ -11332,8 +11995,10 @@ Instructions:
11332
11995
  - After navigating to a new site, DO NOT call read_page immediately. Instead, act on what you already know: use the search tool to search the site, type_text to enter queries in search bars, or click on known navigation patterns. You know what major sites look like — use that knowledge. Only call read_page if you're genuinely stuck and need to discover unfamiliar page structure.
11333
11996
  - The page brief you start with is intentionally sparse. It is optimized for navigation speed, not completeness.
11334
11997
  - 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.
11998
+ - Escalate page reads progressively: read_page(mode="glance") for a fast viewport snapshot on heavy/slow pages, then read_page(mode="visible_only"), read_page(mode="results_only"), read_page(mode="forms_only"), read_page(mode="summary"), or read_page(mode="text_only") depending on what you need.
11999
+ - Use read_page(mode="glance") when a page is slow to load or extraction times out — it shows what's on screen (headings, links, buttons, inputs) without waiting for heavy JS. It's what a human would see by just looking at the page.
11336
12000
  - Use read_page(mode="debug") only as a last resort when the narrower modes are insufficient.
12001
+ - If read_page returns empty or times out, do NOT retry with the same mode. Switch to read_page(mode="glance") or interact directly with click/type_text.
11337
12002
  - VIEWPORT SYNC: Treat scrolling as a real, user-visible browser action. If you say you are going to scroll, call scroll or scroll_to_element so the human sees the page move too.
11338
12003
  - read_page inspects the page without moving the human-visible viewport. Do not describe read_page as scrolling. If you want more context without changing the user's view, say you're reading the page; if you want the user to follow along lower on the page, actually scroll first.
11339
12004
  - After clicking or submitting a form, prefer wait_for on a specific result signal or a narrow read_page mode. Do not jump straight to read_page(mode="debug").
@@ -11351,7 +12016,7 @@ Instructions:
11351
12016
  - ACT, DON'T HEDGE: You have a full browser — you can navigate to any website, see live content, search, click, add to cart, fill forms, and interact with real pages in real time. Never claim you "don't have access" to a website's inventory, pricing, or content. If the user asks you to go somewhere and do something, start doing it immediately. Don't ask for permission to do what the user just asked you to do — that's redundant and frustrating. Jump straight into action.
11352
12017
  - USE YOUR KNOWLEDGE: You have broad, practical knowledge about technology, products, cooking, travel, finance, and countless other domains. When the user asks for recommendations, GIVE them — don't deflect to Reddit, YouTubers, or other sources. You know enough to recommend PC parts, suggest restaurants, pick a good laptop, or advise on most consumer decisions. Make a clear recommendation, explain your reasoning briefly, and then execute. If there's genuine ambiguity (e.g. AMD vs Intel is preference-dependent), state your pick and why, then ask only the questions that would actually change your recommendation. Never refuse a recommendation by claiming you're "not an expert" — the user chose to ask you, so help them.
11353
12018
  - NEVER USE EMOJIS unless the user uses them first.`;
11354
- const actionCtx = { tabManager, runtime };
12019
+ const actionCtx = { tabManager, runtime: runtime2 };
11355
12020
  const contextualTools = pruneToolsForContext(
11356
12021
  AGENT_TOOLS,
11357
12022
  pageType,
@@ -11779,7 +12444,7 @@ function broadcastState(tabManager) {
11779
12444
  const tabId = tabManager.getActiveTabId();
11780
12445
  stateListener(getDevToolsPanelState(tabId));
11781
12446
  }
11782
- async function withDevToolsAction(runtime, tabManager, name, args, executor) {
12447
+ async function withDevToolsAction(runtime2, tabManager, name, args, executor) {
11783
12448
  const activityEntry = {
11784
12449
  id: ++activityCounter,
11785
12450
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11796,7 +12461,7 @@ async function withDevToolsAction(runtime, tabManager, name, args, executor) {
11796
12461
  broadcastState(tabManager);
11797
12462
  const startTime = Date.now();
11798
12463
  try {
11799
- const result = await runtime.runControlledAction({
12464
+ const result = await runtime2.runControlledAction({
11800
12465
  source: "mcp",
11801
12466
  name,
11802
12467
  args,
@@ -11818,7 +12483,7 @@ async function withDevToolsAction(runtime, tabManager, name, args, executor) {
11818
12483
  return asTextResponse$1(`Error: ${message}`);
11819
12484
  }
11820
12485
  }
11821
- function registerDevTools(server, tabManager, runtime) {
12486
+ function registerDevTools(server, tabManager, runtime2) {
11822
12487
  server.registerTool(
11823
12488
  "vessel_devtools_console_logs",
11824
12489
  {
@@ -11830,16 +12495,16 @@ function registerDevTools(server, tabManager, runtime) {
11830
12495
  search: zod.z.string().optional().describe("Filter entries containing this text (case-insensitive)")
11831
12496
  }
11832
12497
  },
11833
- async ({ level, limit, search }) => {
12498
+ async ({ level, limit, search: search2 }) => {
11834
12499
  return withDevToolsAction(
11835
- runtime,
12500
+ runtime2,
11836
12501
  tabManager,
11837
12502
  "devtools_console_logs",
11838
- { level, limit, search },
12503
+ { level, limit, search: search2 },
11839
12504
  async () => {
11840
12505
  const session = getOrCreateSession(tabManager);
11841
12506
  await session.ensureConsoleDomain();
11842
- const entries = session.getConsoleLogs({ level, limit, search });
12507
+ const entries = session.getConsoleLogs({ level, limit, search: search2 });
11843
12508
  if (entries.length === 0) {
11844
12509
  return "No console entries captured yet. Console monitoring is now active — new entries will be captured as they occur.";
11845
12510
  }
@@ -11856,7 +12521,7 @@ function registerDevTools(server, tabManager, runtime) {
11856
12521
  },
11857
12522
  async () => {
11858
12523
  return withDevToolsAction(
11859
- runtime,
12524
+ runtime2,
11860
12525
  tabManager,
11861
12526
  "devtools_console_clear",
11862
12527
  {},
@@ -11883,7 +12548,7 @@ function registerDevTools(server, tabManager, runtime) {
11883
12548
  },
11884
12549
  async ({ url_pattern, method, status_min, status_max, limit }) => {
11885
12550
  return withDevToolsAction(
11886
- runtime,
12551
+ runtime2,
11887
12552
  tabManager,
11888
12553
  "devtools_network_log",
11889
12554
  { url_pattern, method, status_min, status_max, limit },
@@ -11915,7 +12580,7 @@ function registerDevTools(server, tabManager, runtime) {
11915
12580
  },
11916
12581
  async ({ request_id }) => {
11917
12582
  return withDevToolsAction(
11918
- runtime,
12583
+ runtime2,
11919
12584
  tabManager,
11920
12585
  "devtools_network_response_body",
11921
12586
  { request_id },
@@ -11941,7 +12606,7 @@ function registerDevTools(server, tabManager, runtime) {
11941
12606
  },
11942
12607
  async () => {
11943
12608
  return withDevToolsAction(
11944
- runtime,
12609
+ runtime2,
11945
12610
  tabManager,
11946
12611
  "devtools_network_clear",
11947
12612
  {},
@@ -11965,7 +12630,7 @@ function registerDevTools(server, tabManager, runtime) {
11965
12630
  },
11966
12631
  async ({ selector, include_html }) => {
11967
12632
  return withDevToolsAction(
11968
- runtime,
12633
+ runtime2,
11969
12634
  tabManager,
11970
12635
  "devtools_query_dom",
11971
12636
  { selector, include_html },
@@ -11996,7 +12661,7 @@ function registerDevTools(server, tabManager, runtime) {
11996
12661
  },
11997
12662
  async ({ selector, properties }) => {
11998
12663
  return withDevToolsAction(
11999
- runtime,
12664
+ runtime2,
12000
12665
  tabManager,
12001
12666
  "devtools_get_styles",
12002
12667
  { selector, properties },
@@ -12024,7 +12689,7 @@ function registerDevTools(server, tabManager, runtime) {
12024
12689
  },
12025
12690
  async ({ selector, attribute, value }) => {
12026
12691
  return withDevToolsAction(
12027
- runtime,
12692
+ runtime2,
12028
12693
  tabManager,
12029
12694
  "devtools_modify_dom",
12030
12695
  { selector, attribute, value },
@@ -12046,7 +12711,7 @@ function registerDevTools(server, tabManager, runtime) {
12046
12711
  },
12047
12712
  async ({ expression }) => {
12048
12713
  return withDevToolsAction(
12049
- runtime,
12714
+ runtime2,
12050
12715
  tabManager,
12051
12716
  "devtools_execute_js",
12052
12717
  { expression: expression.slice(0, 200) },
@@ -12074,7 +12739,7 @@ Exception: ${result.exceptionDetails}`);
12074
12739
  },
12075
12740
  async ({ type }) => {
12076
12741
  return withDevToolsAction(
12077
- runtime,
12742
+ runtime2,
12078
12743
  tabManager,
12079
12744
  "devtools_get_storage",
12080
12745
  { type },
@@ -12103,7 +12768,7 @@ Exception: ${result.exceptionDetails}`);
12103
12768
  },
12104
12769
  async ({ type, key, value }) => {
12105
12770
  return withDevToolsAction(
12106
- runtime,
12771
+ runtime2,
12107
12772
  tabManager,
12108
12773
  "devtools_set_storage",
12109
12774
  { type, key, value: value ? value.slice(0, 100) : null },
@@ -12122,7 +12787,7 @@ Exception: ${result.exceptionDetails}`);
12122
12787
  },
12123
12788
  async () => {
12124
12789
  return withDevToolsAction(
12125
- runtime,
12790
+ runtime2,
12126
12791
  tabManager,
12127
12792
  "devtools_performance",
12128
12793
  {},
@@ -12146,7 +12811,7 @@ Exception: ${result.exceptionDetails}`);
12146
12811
  },
12147
12812
  async ({ type, limit }) => {
12148
12813
  return withDevToolsAction(
12149
- runtime,
12814
+ runtime2,
12150
12815
  tabManager,
12151
12816
  "devtools_get_errors",
12152
12817
  { type, limit },
@@ -12170,7 +12835,7 @@ Exception: ${result.exceptionDetails}`);
12170
12835
  },
12171
12836
  async () => {
12172
12837
  return withDevToolsAction(
12173
- runtime,
12838
+ runtime2,
12174
12839
  tabManager,
12175
12840
  "devtools_clear_errors",
12176
12841
  {},
@@ -12184,6 +12849,7 @@ Exception: ${result.exceptionDetails}`);
12184
12849
  );
12185
12850
  }
12186
12851
  let httpServer = null;
12852
+ let mcpAuthToken = null;
12187
12853
  function asTextResponse(text) {
12188
12854
  return { content: [{ type: "text", text }] };
12189
12855
  }
@@ -13121,9 +13787,9 @@ async function getPostActionState(tabManager, name) {
13121
13787
  }
13122
13788
  return "";
13123
13789
  }
13124
- async function withAction(runtime, tabManager, name, args, executor) {
13790
+ async function withAction(runtime2, tabManager, name, args, executor) {
13125
13791
  try {
13126
- const result = await runtime.runControlledAction({
13792
+ const result = await runtime2.runControlledAction({
13127
13793
  source: "mcp",
13128
13794
  name,
13129
13795
  args,
@@ -13132,7 +13798,7 @@ async function withAction(runtime, tabManager, name, args, executor) {
13132
13798
  executor
13133
13799
  });
13134
13800
  const stateInfo = await getPostActionState(tabManager, name);
13135
- const flowCtx = runtime.getFlowContext();
13801
+ const flowCtx = runtime2.getFlowContext();
13136
13802
  return asTextResponse(result + stateInfo + flowCtx);
13137
13803
  } catch (error) {
13138
13804
  return asTextResponse(
@@ -13426,6 +14092,7 @@ async function submitForm(wc, index, selector) {
13426
14092
  if (formInfo.params) {
13427
14093
  url.search = formInfo.params;
13428
14094
  }
14095
+ assertSafeURL(url.toString());
13429
14096
  wc.loadURL(url.toString());
13430
14097
  await waitForPotentialNavigation(wc, beforeUrl);
13431
14098
  const afterUrl = wc.getURL();
@@ -13567,7 +14234,7 @@ async function captureScreenshotPayload(wc) {
13567
14234
  }
13568
14235
  return { ok: false, error: "page image was empty after 3 attempts" };
13569
14236
  }
13570
- function registerTools(server, tabManager, runtime) {
14237
+ function registerTools(server, tabManager, runtime2) {
13571
14238
  server.registerPrompt(
13572
14239
  "vessel-supervisor-brief",
13573
14240
  {
@@ -13575,7 +14242,7 @@ function registerTools(server, tabManager, runtime) {
13575
14242
  description: "A reusable prompt for reviewing the current Vessel runtime state."
13576
14243
  },
13577
14244
  async () => {
13578
- const state2 = runtime.getState();
14245
+ const state2 = runtime2.getState();
13579
14246
  const activeTab = getActiveTabSummary(tabManager);
13580
14247
  return asPromptResponse(
13581
14248
  [
@@ -13602,7 +14269,7 @@ function registerTools(server, tabManager, runtime) {
13602
14269
  contents: [
13603
14270
  {
13604
14271
  uri: "vessel://runtime/state",
13605
- text: JSON.stringify(runtime.getState(), null, 2)
14272
+ text: JSON.stringify(runtime2.getState(), null, 2)
13606
14273
  }
13607
14274
  ]
13608
14275
  })
@@ -13718,7 +14385,7 @@ function registerTools(server, tabManager, runtime) {
13718
14385
  }
13719
14386
  },
13720
14387
  async ({ text, stream_id, mode, kind, title }) => {
13721
- const entry = runtime.publishTranscript({
14388
+ const entry = runtime2.publishTranscript({
13722
14389
  source: "mcp",
13723
14390
  text,
13724
14391
  streamId: stream_id,
@@ -13748,7 +14415,7 @@ function registerTools(server, tabManager, runtime) {
13748
14415
  description: "Clear the in-browser transcript monitor state."
13749
14416
  },
13750
14417
  async () => {
13751
- runtime.clearTranscript();
14418
+ runtime2.clearTranscript();
13752
14419
  return asTextResponse("Cleared browser transcript monitor.");
13753
14420
  }
13754
14421
  );
@@ -13888,7 +14555,7 @@ ${buildScopedContext(pageContent, mode)}`;
13888
14555
  `Navigation blocked: ${url} returned ${preCheck.detail || "dead link"}. Try a different URL or go back and choose another link.`
13889
14556
  );
13890
14557
  }
13891
- return withAction(runtime, tabManager, "navigate", { url }, async () => {
14558
+ return withAction(runtime2, tabManager, "navigate", { url }, async () => {
13892
14559
  const id = tabManager.getActiveTabId();
13893
14560
  tabManager.navigateTab(id, url);
13894
14561
  const { httpStatus } = await waitForLoadWithStatus(
@@ -13918,7 +14585,7 @@ ${buildScopedContext(pageContent, mode)}`;
13918
14585
  return asTextResponse("Error: No active tab");
13919
14586
  }
13920
14587
  return withAction(
13921
- runtime,
14588
+ runtime2,
13922
14589
  tabManager,
13923
14590
  "set_ad_blocking",
13924
14591
  { enabled, tabId, match, reload },
@@ -14007,7 +14674,7 @@ ${buildScopedContext(pageContent, mode)}`;
14007
14674
  async () => {
14008
14675
  const tab = tabManager.getActiveTab();
14009
14676
  if (!tab) return asTextResponse("Error: No active tab");
14010
- return withAction(runtime, tabManager, "go_back", {}, async () => {
14677
+ return withAction(runtime2, tabManager, "go_back", {}, async () => {
14011
14678
  if (!tab.canGoBack()) {
14012
14679
  return "No previous page in history";
14013
14680
  }
@@ -14028,7 +14695,7 @@ ${buildScopedContext(pageContent, mode)}`;
14028
14695
  async () => {
14029
14696
  const tab = tabManager.getActiveTab();
14030
14697
  if (!tab) return asTextResponse("Error: No active tab");
14031
- return withAction(runtime, tabManager, "go_forward", {}, async () => {
14698
+ return withAction(runtime2, tabManager, "go_forward", {}, async () => {
14032
14699
  if (!tab.canGoForward()) {
14033
14700
  return "No forward page in history";
14034
14701
  }
@@ -14049,7 +14716,7 @@ ${buildScopedContext(pageContent, mode)}`;
14049
14716
  async () => {
14050
14717
  const tab = tabManager.getActiveTab();
14051
14718
  if (!tab) return asTextResponse("Error: No active tab");
14052
- return withAction(runtime, tabManager, "reload", {}, async () => {
14719
+ return withAction(runtime2, tabManager, "reload", {}, async () => {
14053
14720
  tabManager.reloadTab(tabManager.getActiveTabId());
14054
14721
  await waitForLoad(tab.view.webContents);
14055
14722
  return `Reloaded ${tab.view.webContents.getURL()}`;
@@ -14070,7 +14737,7 @@ ${buildScopedContext(pageContent, mode)}`;
14070
14737
  const tab = tabManager.getActiveTab();
14071
14738
  if (!tab) return asTextResponse("Error: No active tab");
14072
14739
  return withAction(
14073
- runtime,
14740
+ runtime2,
14074
14741
  tabManager,
14075
14742
  "click",
14076
14743
  { index, selector },
@@ -14099,7 +14766,7 @@ ${buildScopedContext(pageContent, mode)}`;
14099
14766
  const tab = tabManager.getActiveTab();
14100
14767
  if (!tab) return asTextResponse("Error: No active tab");
14101
14768
  return withAction(
14102
- runtime,
14769
+ runtime2,
14103
14770
  tabManager,
14104
14771
  "hover",
14105
14772
  { index, selector },
@@ -14128,7 +14795,7 @@ ${buildScopedContext(pageContent, mode)}`;
14128
14795
  const tab = tabManager.getActiveTab();
14129
14796
  if (!tab) return asTextResponse("Error: No active tab");
14130
14797
  return withAction(
14131
- runtime,
14798
+ runtime2,
14132
14799
  tabManager,
14133
14800
  "focus",
14134
14801
  { index, selector },
@@ -14242,7 +14909,7 @@ ${buildScopedContext(pageContent, mode)}`;
14242
14909
  const tab = tabManager.getActiveTab();
14243
14910
  if (!tab) return asTextResponse("Error: No active tab");
14244
14911
  return withAction(
14245
- runtime,
14912
+ runtime2,
14246
14913
  tabManager,
14247
14914
  "type",
14248
14915
  { index, selector, text, mode },
@@ -14281,7 +14948,7 @@ ${buildScopedContext(pageContent, mode)}`;
14281
14948
  const tab = tabManager.getActiveTab();
14282
14949
  if (!tab) return asTextResponse("Error: No active tab");
14283
14950
  return withAction(
14284
- runtime,
14951
+ runtime2,
14285
14952
  tabManager,
14286
14953
  "type_text",
14287
14954
  { index, selector, text, mode },
@@ -14318,7 +14985,7 @@ ${buildScopedContext(pageContent, mode)}`;
14318
14985
  const tab = tabManager.getActiveTab();
14319
14986
  if (!tab) return asTextResponse("Error: No active tab");
14320
14987
  return withAction(
14321
- runtime,
14988
+ runtime2,
14322
14989
  tabManager,
14323
14990
  "select_option",
14324
14991
  { index, selector, label, value },
@@ -14340,7 +15007,7 @@ ${buildScopedContext(pageContent, mode)}`;
14340
15007
  const tab = tabManager.getActiveTab();
14341
15008
  if (!tab) return asTextResponse("Error: No active tab");
14342
15009
  return withAction(
14343
- runtime,
15010
+ runtime2,
14344
15011
  tabManager,
14345
15012
  "submit_form",
14346
15013
  { index, selector },
@@ -14373,7 +15040,7 @@ ${buildScopedContext(pageContent, mode)}`;
14373
15040
  const tab = tabManager.getActiveTab();
14374
15041
  if (!tab) return asTextResponse("Error: No active tab");
14375
15042
  return withAction(
14376
- runtime,
15043
+ runtime2,
14377
15044
  tabManager,
14378
15045
  "press_key",
14379
15046
  { key, index, selector },
@@ -14409,7 +15076,7 @@ ${buildScopedContext(pageContent, mode)}`;
14409
15076
  const tab = tabManager.getActiveTab();
14410
15077
  if (!tab) return asTextResponse("Error: No active tab");
14411
15078
  return withAction(
14412
- runtime,
15079
+ runtime2,
14413
15080
  tabManager,
14414
15081
  "scroll",
14415
15082
  { direction, amount },
@@ -14432,7 +15099,7 @@ ${buildScopedContext(pageContent, mode)}`;
14432
15099
  const tab = tabManager.getActiveTab();
14433
15100
  if (!tab) return asTextResponse("Error: No active tab");
14434
15101
  return withAction(
14435
- runtime,
15102
+ runtime2,
14436
15103
  tabManager,
14437
15104
  "dismiss_popup",
14438
15105
  {},
@@ -14455,7 +15122,7 @@ ${buildScopedContext(pageContent, mode)}`;
14455
15122
  const tab = tabManager.getActiveTab();
14456
15123
  if (!tab) return asTextResponse("Error: No active tab");
14457
15124
  return withAction(
14458
- runtime,
15125
+ runtime2,
14459
15126
  tabManager,
14460
15127
  "clear_overlays",
14461
15128
  { strategy: strategy || "auto" },
@@ -14481,7 +15148,7 @@ ${buildScopedContext(pageContent, mode)}`;
14481
15148
  const tab = tabManager.getActiveTab();
14482
15149
  if (!tab) return asTextResponse("Error: No active tab");
14483
15150
  return withAction(
14484
- runtime,
15151
+ runtime2,
14485
15152
  tabManager,
14486
15153
  "wait_for",
14487
15154
  { text, selector, timeoutMs },
@@ -14498,7 +15165,7 @@ ${buildScopedContext(pageContent, mode)}`;
14498
15165
  url: zod.z.string().optional().describe("URL to open (defaults to about:blank)")
14499
15166
  }
14500
15167
  },
14501
- async ({ url }) => withAction(runtime, tabManager, "create_tab", { url }, async () => {
15168
+ async ({ url }) => withAction(runtime2, tabManager, "create_tab", { url }, async () => {
14502
15169
  const id = tabManager.createTab(url || "about:blank");
14503
15170
  const tab = tabManager.getActiveTab();
14504
15171
  if (tab) {
@@ -14518,7 +15185,7 @@ ${buildScopedContext(pageContent, mode)}`;
14518
15185
  }
14519
15186
  },
14520
15187
  async ({ tabId, match }) => withAction(
14521
- runtime,
15188
+ runtime2,
14522
15189
  tabManager,
14523
15190
  "switch_tab",
14524
15191
  { tabId, match },
@@ -14541,7 +15208,7 @@ ${buildScopedContext(pageContent, mode)}`;
14541
15208
  tabId: zod.z.string().describe("The tab ID to close")
14542
15209
  }
14543
15210
  },
14544
- async ({ tabId }) => withAction(runtime, tabManager, "close_tab", { tabId }, async () => {
15211
+ async ({ tabId }) => withAction(runtime2, tabManager, "close_tab", { tabId }, async () => {
14545
15212
  tabManager.closeTab(tabId);
14546
15213
  return `Closed tab ${tabId}`;
14547
15214
  })
@@ -14557,12 +15224,12 @@ ${buildScopedContext(pageContent, mode)}`;
14557
15224
  }
14558
15225
  },
14559
15226
  async ({ name, note }) => withAction(
14560
- runtime,
15227
+ runtime2,
14561
15228
  tabManager,
14562
15229
  "create_checkpoint",
14563
15230
  { name, note },
14564
15231
  async () => {
14565
- const checkpoint = runtime.createCheckpoint(name, note);
15232
+ const checkpoint = runtime2.createCheckpoint(name, note);
14566
15233
  return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
14567
15234
  }
14568
15235
  )
@@ -14578,12 +15245,12 @@ ${buildScopedContext(pageContent, mode)}`;
14578
15245
  }
14579
15246
  },
14580
15247
  async ({ name, note }) => withAction(
14581
- runtime,
15248
+ runtime2,
14582
15249
  tabManager,
14583
15250
  "create_checkpoint",
14584
15251
  { name, note },
14585
15252
  async () => {
14586
- const checkpoint = runtime.createCheckpoint(name, note);
15253
+ const checkpoint = runtime2.createCheckpoint(name, note);
14587
15254
  return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
14588
15255
  }
14589
15256
  )
@@ -14599,17 +15266,17 @@ ${buildScopedContext(pageContent, mode)}`;
14599
15266
  }
14600
15267
  },
14601
15268
  async ({ checkpointId, name }) => withAction(
14602
- runtime,
15269
+ runtime2,
14603
15270
  tabManager,
14604
15271
  "restore_checkpoint",
14605
15272
  { checkpointId, name },
14606
15273
  async () => {
14607
- const state2 = runtime.getState();
15274
+ const state2 = runtime2.getState();
14608
15275
  const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
14609
15276
  if (!checkpoint) {
14610
15277
  return "Error: No matching checkpoint found";
14611
15278
  }
14612
- runtime.restoreCheckpoint(checkpoint.id);
15279
+ runtime2.restoreCheckpoint(checkpoint.id);
14613
15280
  return `Restored checkpoint ${checkpoint.name}`;
14614
15281
  }
14615
15282
  )
@@ -14625,17 +15292,17 @@ ${buildScopedContext(pageContent, mode)}`;
14625
15292
  }
14626
15293
  },
14627
15294
  async ({ checkpointId, name }) => withAction(
14628
- runtime,
15295
+ runtime2,
14629
15296
  tabManager,
14630
15297
  "restore_checkpoint",
14631
15298
  { checkpointId, name },
14632
15299
  async () => {
14633
- const state2 = runtime.getState();
15300
+ const state2 = runtime2.getState();
14634
15301
  const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
14635
15302
  if (!checkpoint) {
14636
15303
  return "Error: No matching checkpoint found";
14637
15304
  }
14638
- runtime.restoreCheckpoint(checkpoint.id);
15305
+ runtime2.restoreCheckpoint(checkpoint.id);
14639
15306
  return `Restored checkpoint ${checkpoint.name}`;
14640
15307
  }
14641
15308
  )
@@ -14649,7 +15316,7 @@ ${buildScopedContext(pageContent, mode)}`;
14649
15316
  name: zod.z.string().describe("Session name such as github-logged-in")
14650
15317
  }
14651
15318
  },
14652
- async ({ name }) => withAction(runtime, tabManager, "save_session", { name }, async () => {
15319
+ async ({ name }) => withAction(runtime2, tabManager, "save_session", { name }, async () => {
14653
15320
  const saved = await saveNamedSession(
14654
15321
  tabManager,
14655
15322
  name
@@ -14666,7 +15333,7 @@ ${buildScopedContext(pageContent, mode)}`;
14666
15333
  name: zod.z.string().describe("Previously saved session name")
14667
15334
  }
14668
15335
  },
14669
- async ({ name }) => withAction(runtime, tabManager, "load_session", { name }, async () => {
15336
+ async ({ name }) => withAction(runtime2, tabManager, "load_session", { name }, async () => {
14670
15337
  const loaded = await loadNamedSession(
14671
15338
  tabManager,
14672
15339
  name
@@ -14680,7 +15347,7 @@ ${buildScopedContext(pageContent, mode)}`;
14680
15347
  title: "List Sessions",
14681
15348
  description: "List previously saved named browser sessions with cookie and storage counts."
14682
15349
  },
14683
- async () => withAction(runtime, tabManager, "list_sessions", {}, async () => {
15350
+ async () => withAction(runtime2, tabManager, "list_sessions", {}, async () => {
14684
15351
  const sessions2 = listNamedSessions();
14685
15352
  if (sessions2.length === 0) return "No saved sessions";
14686
15353
  return sessions2.map(
@@ -14698,7 +15365,7 @@ ${buildScopedContext(pageContent, mode)}`;
14698
15365
  }
14699
15366
  },
14700
15367
  async ({ name }) => withAction(
14701
- runtime,
15368
+ runtime2,
14702
15369
  tabManager,
14703
15370
  "delete_session",
14704
15371
  { name },
@@ -14785,7 +15452,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14785
15452
  if (!tab) return asTextResponse("Error: No active tab");
14786
15453
  const normalizedText = normalizeLooseString(text);
14787
15454
  return withAction(
14788
- runtime,
15455
+ runtime2,
14789
15456
  tabManager,
14790
15457
  "highlight",
14791
15458
  {
@@ -14834,7 +15501,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14834
15501
  const tab = tabManager.getActiveTab();
14835
15502
  if (!tab) return asTextResponse("Error: No active tab");
14836
15503
  return withAction(
14837
- runtime,
15504
+ runtime2,
14838
15505
  tabManager,
14839
15506
  "clear_highlights",
14840
15507
  {},
@@ -14859,7 +15526,7 @@ To analyze visually, call vision_analyze with image_url="${screenshotPath}"`
14859
15526
  }
14860
15527
  },
14861
15528
  async ({ url }) => {
14862
- const state2 = getState$1();
15529
+ const state2 = getState$2();
14863
15530
  const activeTab = tabManager.getActiveTab();
14864
15531
  const activeUrl = activeTab ? normalizeUrl(activeTab.view.webContents.getURL()) : null;
14865
15532
  const activeSavedHighlights = activeUrl ? state2.highlights.filter((highlight) => highlight.url === activeUrl) : [];
@@ -14990,7 +15657,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
14990
15657
  },
14991
15658
  async ({ name, summary }) => {
14992
15659
  return withAction(
14993
- runtime,
15660
+ runtime2,
14994
15661
  tabManager,
14995
15662
  "create_bookmark_folder",
14996
15663
  { name, summary },
@@ -15052,7 +15719,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15052
15719
  on_duplicate
15053
15720
  }) => {
15054
15721
  return withAction(
15055
- runtime,
15722
+ runtime2,
15056
15723
  tabManager,
15057
15724
  "save_bookmark",
15058
15725
  {
@@ -15131,7 +15798,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15131
15798
  },
15132
15799
  async ({ folder_id, folder_name }) => {
15133
15800
  return withAction(
15134
- runtime,
15801
+ runtime2,
15135
15802
  tabManager,
15136
15803
  "list_bookmarks",
15137
15804
  { folder_id, folder_name },
@@ -15196,7 +15863,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15196
15863
  },
15197
15864
  async (args) => {
15198
15865
  return withAction(
15199
- runtime,
15866
+ runtime2,
15200
15867
  tabManager,
15201
15868
  "organize_bookmark",
15202
15869
  args,
@@ -15263,7 +15930,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15263
15930
  },
15264
15931
  async ({ query }) => {
15265
15932
  return withAction(
15266
- runtime,
15933
+ runtime2,
15267
15934
  tabManager,
15268
15935
  "search_bookmarks",
15269
15936
  { query },
@@ -15294,7 +15961,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15294
15961
  },
15295
15962
  async ({ bookmark_id }) => {
15296
15963
  return withAction(
15297
- runtime,
15964
+ runtime2,
15298
15965
  tabManager,
15299
15966
  "remove_bookmark",
15300
15967
  { bookmark_id },
@@ -15327,7 +15994,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15327
15994
  },
15328
15995
  async ({ bookmark_id, url, title, index, selector, note }) => {
15329
15996
  return withAction(
15330
- runtime,
15997
+ runtime2,
15331
15998
  tabManager,
15332
15999
  "archive_bookmark",
15333
16000
  { bookmark_id, url, title, index, selector, note },
@@ -15397,7 +16064,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15397
16064
  },
15398
16065
  async ({ bookmark_id, new_tab }) => {
15399
16066
  return withAction(
15400
- runtime,
16067
+ runtime2,
15401
16068
  tabManager,
15402
16069
  "open_bookmark",
15403
16070
  { bookmark_id, new_tab },
@@ -15440,7 +16107,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15440
16107
  },
15441
16108
  async ({ folder_id }) => {
15442
16109
  return withAction(
15443
- runtime,
16110
+ runtime2,
15444
16111
  tabManager,
15445
16112
  "remove_bookmark_folder",
15446
16113
  { folder_id },
@@ -15466,7 +16133,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15466
16133
  },
15467
16134
  async ({ folder_id, new_name, summary }) => {
15468
16135
  return withAction(
15469
- runtime,
16136
+ runtime2,
15470
16137
  tabManager,
15471
16138
  "rename_bookmark_folder",
15472
16139
  { folder_id, new_name, summary },
@@ -15503,7 +16170,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15503
16170
  },
15504
16171
  async ({ title, body, folder, tags }) => {
15505
16172
  return withAction(
15506
- runtime,
16173
+ runtime2,
15507
16174
  tabManager,
15508
16175
  "memory_note_create",
15509
16176
  { title, folder, tags },
@@ -15527,7 +16194,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15527
16194
  },
15528
16195
  async ({ note_path, content, heading }) => {
15529
16196
  return withAction(
15530
- runtime,
16197
+ runtime2,
15531
16198
  tabManager,
15532
16199
  "memory_note_append",
15533
16200
  { note_path, heading },
@@ -15554,7 +16221,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15554
16221
  },
15555
16222
  async ({ folder, limit }) => {
15556
16223
  return withAction(
15557
- runtime,
16224
+ runtime2,
15558
16225
  tabManager,
15559
16226
  "memory_note_list",
15560
16227
  { folder, limit },
@@ -15584,7 +16251,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15584
16251
  },
15585
16252
  async ({ query, folder, tags, limit }) => {
15586
16253
  return withAction(
15587
- runtime,
16254
+ runtime2,
15588
16255
  tabManager,
15589
16256
  "memory_note_search",
15590
16257
  { query, folder, tags, limit },
@@ -15617,7 +16284,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15617
16284
  const tab = tabManager.getActiveTab();
15618
16285
  if (!tab) return asTextResponse("Error: No active tab");
15619
16286
  return withAction(
15620
- runtime,
16287
+ runtime2,
15621
16288
  tabManager,
15622
16289
  "memory_page_capture",
15623
16290
  { title, folder, tags },
@@ -15654,7 +16321,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15654
16321
  },
15655
16322
  async ({ bookmark_id, note_path, title, folder, note, tags }) => {
15656
16323
  return withAction(
15657
- runtime,
16324
+ runtime2,
15658
16325
  tabManager,
15659
16326
  "memory_link_bookmark",
15660
16327
  { bookmark_id, note_path, title, folder, tags },
@@ -15693,7 +16360,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
15693
16360
  async ({ goal, steps }) => {
15694
16361
  const normalizedSteps = coerceStringArray(steps) ?? [];
15695
16362
  const tab = tabManager.getActiveTab();
15696
- const flow = runtime.startFlow(
16363
+ const flow = runtime2.startFlow(
15697
16364
  goal,
15698
16365
  normalizedSteps,
15699
16366
  tab?.view.webContents.getURL()
@@ -15714,9 +16381,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15714
16381
  }
15715
16382
  },
15716
16383
  async ({ detail }) => {
15717
- const flow = runtime.advanceFlow(detail);
16384
+ const flow = runtime2.advanceFlow(detail);
15718
16385
  if (!flow) return asTextResponse("No active flow to advance");
15719
- const ctx = runtime.getFlowContext();
16386
+ const ctx = runtime2.getFlowContext();
15720
16387
  return asTextResponse(`Step completed.${ctx}`);
15721
16388
  }
15722
16389
  );
@@ -15727,9 +16394,9 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15727
16394
  description: "Check the current workflow progress."
15728
16395
  },
15729
16396
  async () => {
15730
- const flow = runtime.getFlowState();
16397
+ const flow = runtime2.getFlowState();
15731
16398
  if (!flow) return asTextResponse("No active workflow.");
15732
- return asTextResponse(runtime.getFlowContext());
16399
+ return asTextResponse(runtime2.getFlowContext());
15733
16400
  }
15734
16401
  );
15735
16402
  server.registerTool(
@@ -15739,7 +16406,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15739
16406
  description: "Clear the active workflow tracker."
15740
16407
  },
15741
16408
  async () => {
15742
- runtime.clearFlow();
16409
+ runtime2.clearFlow();
15743
16410
  return asTextResponse("Workflow ended.");
15744
16411
  }
15745
16412
  );
@@ -15768,7 +16435,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15768
16435
  suggestions.push(`Page: ${page.title || "(untitled)"}`);
15769
16436
  suggestions.push(`URL: ${page.url}`);
15770
16437
  suggestions.push("");
15771
- const flowCtx = runtime.getFlowContext();
16438
+ const flowCtx = runtime2.getFlowContext();
15772
16439
  if (flowCtx) {
15773
16440
  suggestions.push(flowCtx);
15774
16441
  suggestions.push("");
@@ -15868,7 +16535,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
15868
16535
  const tab = tabManager.getActiveTab();
15869
16536
  if (!tab) return asTextResponse("Error: No active tab");
15870
16537
  return withAction(
15871
- runtime,
16538
+ runtime2,
15872
16539
  tabManager,
15873
16540
  "fill_form",
15874
16541
  { fieldCount: fields.length, submit },
@@ -15925,7 +16592,7 @@ ${results.join("\n")}`;
15925
16592
  const tab = tabManager.getActiveTab();
15926
16593
  if (!tab) return asTextResponse("Error: No active tab");
15927
16594
  return withAction(
15928
- runtime,
16595
+ runtime2,
15929
16596
  tabManager,
15930
16597
  "login",
15931
16598
  { url, username: username.slice(0, 3) + "***" },
@@ -16030,7 +16697,7 @@ ${steps.join("\n")}`;
16030
16697
  `Error: "${query}" looks like a button label, not a search query. Use the click tool to interact with this element instead.`
16031
16698
  );
16032
16699
  }
16033
- return withAction(runtime, tabManager, "search", { query }, async () => {
16700
+ return withAction(runtime2, tabManager, "search", { query }, async () => {
16034
16701
  const wc = tab.view.webContents;
16035
16702
  const searchSel = selector || await wc.executeJavaScript(`
16036
16703
  (function() {
@@ -16084,7 +16751,7 @@ ${steps.join("\n")}`;
16084
16751
  const tab = tabManager.getActiveTab();
16085
16752
  if (!tab) return asTextResponse("Error: No active tab");
16086
16753
  return withAction(
16087
- runtime,
16754
+ runtime2,
16088
16755
  tabManager,
16089
16756
  "paginate",
16090
16757
  { direction },
@@ -16135,7 +16802,7 @@ ${steps.join("\n")}`;
16135
16802
  const tab = tabManager.getActiveTab();
16136
16803
  if (!tab) return asTextResponse("Error: No active tab");
16137
16804
  return withAction(
16138
- runtime,
16805
+ runtime2,
16139
16806
  tabManager,
16140
16807
  "vessel_accept_cookies",
16141
16808
  {},
@@ -16193,7 +16860,7 @@ ${steps.join("\n")}`;
16193
16860
  const tab = tabManager.getActiveTab();
16194
16861
  if (!tab) return asTextResponse("Error: No active tab");
16195
16862
  return withAction(
16196
- runtime,
16863
+ runtime2,
16197
16864
  tabManager,
16198
16865
  "vessel_extract_table",
16199
16866
  { index, selector: rawSelector },
@@ -16248,7 +16915,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
16248
16915
  const tab = tabManager.getActiveTab();
16249
16916
  if (!tab) return asTextResponse("Error: No active tab");
16250
16917
  return withAction(
16251
- runtime,
16918
+ runtime2,
16252
16919
  tabManager,
16253
16920
  "vessel_scroll_to_element",
16254
16921
  { index, selector: rawSelector, position },
@@ -16306,7 +16973,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
16306
16973
  const tab = tabManager.getActiveTab();
16307
16974
  if (!tab) return asTextResponse("Error: No active tab");
16308
16975
  return withAction(
16309
- runtime,
16976
+ runtime2,
16310
16977
  tabManager,
16311
16978
  "vessel_wait_for_navigation",
16312
16979
  { timeoutMs },
@@ -16357,7 +17024,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
16357
17024
  inputSchema: zod.z.object({})
16358
17025
  },
16359
17026
  async () => {
16360
- const m = runtime.getMetrics();
17027
+ const m = runtime2.getMetrics();
16361
17028
  const lines = [
16362
17029
  `Session Metrics:`,
16363
17030
  ` Total actions: ${m.totalActions}`,
@@ -16517,16 +17184,16 @@ async function resolveSelector(wc, index, selector) {
16517
17184
  `
16518
17185
  );
16519
17186
  }
16520
- function createMcpServer(tabManager, runtime) {
17187
+ function createMcpServer(tabManager, runtime2) {
16521
17188
  const server = new mcp_js.McpServer({
16522
17189
  name: "vessel-browser",
16523
17190
  version: "0.1.0"
16524
17191
  });
16525
- registerTools(server, tabManager, runtime);
16526
- registerDevTools(server, tabManager, runtime);
17192
+ registerTools(server, tabManager, runtime2);
17193
+ registerDevTools(server, tabManager, runtime2);
16527
17194
  return server;
16528
17195
  }
16529
- function startMcpServer(tabManager, runtime, port) {
17196
+ function startMcpServer(tabManager, runtime2, port) {
16530
17197
  setMcpHealth({
16531
17198
  configuredPort: port,
16532
17199
  activePort: null,
@@ -16534,6 +17201,7 @@ function startMcpServer(tabManager, runtime, port) {
16534
17201
  status: "starting",
16535
17202
  message: `Starting MCP server on port ${port}.`
16536
17203
  });
17204
+ mcpAuthToken = crypto$1.randomBytes(32).toString("hex");
16537
17205
  return new Promise((resolve) => {
16538
17206
  const server = http.createServer(async (req, res) => {
16539
17207
  const url = new URL(req.url || "/", `http://localhost:${port}`);
@@ -16542,22 +17210,28 @@ function startMcpServer(tabManager, runtime, port) {
16542
17210
  res.end("Not found");
16543
17211
  return;
16544
17212
  }
16545
- res.setHeader("Access-Control-Allow-Origin", "*");
17213
+ res.setHeader("Access-Control-Allow-Origin", "null");
16546
17214
  res.setHeader(
16547
17215
  "Access-Control-Allow-Methods",
16548
17216
  "POST, GET, DELETE, OPTIONS"
16549
17217
  );
16550
17218
  res.setHeader(
16551
17219
  "Access-Control-Allow-Headers",
16552
- "Content-Type, mcp-session-id"
17220
+ "Content-Type, mcp-session-id, Authorization"
16553
17221
  );
16554
17222
  if (req.method === "OPTIONS") {
16555
17223
  res.writeHead(204);
16556
17224
  res.end();
16557
17225
  return;
16558
17226
  }
17227
+ const authHeader = req.headers.authorization;
17228
+ if (!authHeader || authHeader !== `Bearer ${mcpAuthToken}`) {
17229
+ res.writeHead(401, { "Content-Type": "application/json" });
17230
+ res.end(JSON.stringify({ error: "Unauthorized — missing or invalid bearer token" }));
17231
+ return;
17232
+ }
16559
17233
  try {
16560
- const mcpServer = createMcpServer(tabManager, runtime);
17234
+ const mcpServer = createMcpServer(tabManager, runtime2);
16561
17235
  const transport = new streamableHttp_js.StreamableHTTPServerTransport({
16562
17236
  sessionIdGenerator: void 0
16563
17237
  });
@@ -16599,6 +17273,7 @@ function startMcpServer(tabManager, runtime, port) {
16599
17273
  configuredPort: port,
16600
17274
  activePort: null,
16601
17275
  endpoint: null,
17276
+ authToken: null,
16602
17277
  error: message
16603
17278
  });
16604
17279
  });
@@ -16615,11 +17290,13 @@ function startMcpServer(tabManager, runtime, port) {
16615
17290
  message: `MCP server listening on ${endpoint}.`
16616
17291
  });
16617
17292
  console.log(`[Vessel MCP] Server listening on ${endpoint}`);
17293
+ console.log(`[Vessel MCP] Auth token: ${mcpAuthToken}`);
16618
17294
  finish({
16619
17295
  ok: true,
16620
17296
  configuredPort: port,
16621
17297
  activePort: actualPort,
16622
- endpoint
17298
+ endpoint,
17299
+ authToken: mcpAuthToken
16623
17300
  });
16624
17301
  });
16625
17302
  });
@@ -16638,6 +17315,7 @@ function stopMcpServer() {
16638
17315
  }
16639
17316
  const server = httpServer;
16640
17317
  httpServer = null;
17318
+ mcpAuthToken = null;
16641
17319
  server.close(() => {
16642
17320
  setMcpHealth({
16643
17321
  activePort: null,
@@ -16651,14 +17329,14 @@ function stopMcpServer() {
16651
17329
  });
16652
17330
  }
16653
17331
  let activeChatProvider = null;
16654
- function registerIpcHandlers(windowState, runtime) {
17332
+ function registerIpcHandlers(windowState, runtime2) {
16655
17333
  const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
16656
17334
  const sendToRendererViews = (channel, ...args) => {
16657
17335
  chromeView.webContents.send(channel, ...args);
16658
17336
  sidebarView.webContents.send(channel, ...args);
16659
17337
  devtoolsPanelView.webContents.send(channel, ...args);
16660
17338
  };
16661
- runtime.setUpdateListener((state2) => {
17339
+ runtime2.setUpdateListener((state2) => {
16662
17340
  sendToRendererViews(Channels.AGENT_RUNTIME_UPDATE, state2);
16663
17341
  });
16664
17342
  electron.ipcMain.handle(Channels.TAB_CREATE, (_, url) => {
@@ -16708,7 +17386,7 @@ function registerIpcHandlers(windowState, runtime) {
16708
17386
  (chunk) => sendToRendererViews(Channels.AI_STREAM_CHUNK, chunk),
16709
17387
  () => sendToRendererViews(Channels.AI_STREAM_END),
16710
17388
  tabManager,
16711
- runtime,
17389
+ runtime2,
16712
17390
  history
16713
17391
  );
16714
17392
  } catch (err) {
@@ -16791,46 +17469,50 @@ function registerIpcHandlers(windowState, runtime) {
16791
17469
  });
16792
17470
  electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
16793
17471
  electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key, value) => {
16794
- const updatedSettings = setSetting(key, value);
17472
+ if (!SETTABLE_KEYS.has(key)) {
17473
+ throw new Error(`Unknown setting key: ${key}`);
17474
+ }
17475
+ const settingsKey = key;
17476
+ const updatedSettings = setSetting(settingsKey, value);
16795
17477
  if (key === "approvalMode") {
16796
- runtime.setApprovalMode(value);
17478
+ runtime2.setApprovalMode(value);
16797
17479
  }
16798
17480
  if (key === "mcpPort") {
16799
17481
  await stopMcpServer();
16800
- await startMcpServer(tabManager, runtime, updatedSettings.mcpPort);
17482
+ await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
16801
17483
  }
16802
17484
  sendToRendererViews(Channels.SETTINGS_UPDATE, updatedSettings);
16803
17485
  return updatedSettings;
16804
17486
  });
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());
17487
+ electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime2.getState());
17488
+ electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime2.pause());
17489
+ electron.ipcMain.handle(Channels.AGENT_RESUME, () => runtime2.resume());
16808
17490
  electron.ipcMain.handle(
16809
17491
  Channels.AGENT_SET_APPROVAL_MODE,
16810
17492
  (_, mode) => {
16811
17493
  setSetting("approvalMode", mode);
16812
- return runtime.setApprovalMode(mode);
17494
+ return runtime2.setApprovalMode(mode);
16813
17495
  }
16814
17496
  );
16815
17497
  electron.ipcMain.handle(
16816
17498
  Channels.AGENT_APPROVAL_RESOLVE,
16817
- (_, approvalId, approved) => runtime.resolveApproval(approvalId, approved)
17499
+ (_, approvalId, approved) => runtime2.resolveApproval(approvalId, approved)
16818
17500
  );
16819
17501
  electron.ipcMain.handle(
16820
17502
  Channels.AGENT_CHECKPOINT_CREATE,
16821
- (_, name, note) => runtime.createCheckpoint(name, note)
17503
+ (_, name, note) => runtime2.createCheckpoint(name, note)
16822
17504
  );
16823
17505
  electron.ipcMain.handle(
16824
17506
  Channels.AGENT_CHECKPOINT_RESTORE,
16825
- (_, checkpointId) => runtime.restoreCheckpoint(checkpointId)
17507
+ (_, checkpointId) => runtime2.restoreCheckpoint(checkpointId)
16826
17508
  );
16827
17509
  electron.ipcMain.handle(
16828
17510
  Channels.AGENT_SESSION_CAPTURE,
16829
- (_, note) => runtime.captureSession(note)
17511
+ (_, note) => runtime2.captureSession(note)
16830
17512
  );
16831
17513
  electron.ipcMain.handle(
16832
17514
  Channels.AGENT_SESSION_RESTORE,
16833
- (_, snapshot) => runtime.restoreSession(snapshot)
17515
+ (_, snapshot) => runtime2.restoreSession(snapshot)
16834
17516
  );
16835
17517
  electron.ipcMain.handle(Channels.BOOKMARKS_GET, () => {
16836
17518
  return getState();
@@ -16864,36 +17546,12 @@ function registerIpcHandlers(windowState, runtime) {
16864
17546
  return { success: false, message: "No active tab" };
16865
17547
  }
16866
17548
  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" };
17549
+ const result = await captureSelectionHighlight(wc);
17550
+ if (result.success && result.text) {
17551
+ await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(() => {
17552
+ });
16882
17553
  }
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 };
17554
+ return result;
16897
17555
  } catch {
16898
17556
  return { success: false, message: "Could not capture selection" };
16899
17557
  }
@@ -16909,24 +17567,11 @@ function registerIpcHandlers(windowState, runtime) {
16909
17567
  if (wc.isDestroyed()) return;
16910
17568
  const tab = tabManager.findTabByWebContentsId(wc.id);
16911
17569
  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
- }
17570
+ void persistAndMarkHighlight(wc, text).then((result) => {
17571
+ if (result.success && !chromeView.webContents.isDestroyed()) {
17572
+ chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, result);
17573
+ }
17574
+ });
16930
17575
  } catch {
16931
17576
  }
16932
17577
  });
@@ -16936,9 +17581,7 @@ function registerIpcHandlers(windowState, runtime) {
16936
17581
  const wc = tab.view.webContents;
16937
17582
  if (wc.isDestroyed()) return 0;
16938
17583
  try {
16939
- return wc.executeJavaScript(
16940
- `document.querySelectorAll('.__vessel-highlight, .__vessel-highlight-text').length`
16941
- );
17584
+ return getHighlightCount(wc);
16942
17585
  } catch {
16943
17586
  return 0;
16944
17587
  }
@@ -16949,20 +17592,7 @@ function registerIpcHandlers(windowState, runtime) {
16949
17592
  const wc = tab.view.webContents;
16950
17593
  if (wc.isDestroyed()) return false;
16951
17594
  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
- `);
17595
+ return scrollToHighlight(wc, index);
16966
17596
  } catch {
16967
17597
  return false;
16968
17598
  }
@@ -16973,32 +17603,7 @@ function registerIpcHandlers(windowState, runtime) {
16973
17603
  const wc = tab.view.webContents;
16974
17604
  if (wc.isDestroyed()) return false;
16975
17605
  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
- `);
17606
+ return removeHighlightAtIndex(wc, index);
17002
17607
  } catch {
17003
17608
  return false;
17004
17609
  }
@@ -17009,39 +17614,65 @@ function registerIpcHandlers(windowState, runtime) {
17009
17614
  const wc = tab.view.webContents;
17010
17615
  if (wc.isDestroyed()) return false;
17011
17616
  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
- `);
17617
+ return clearAllHighlightElements(wc);
17035
17618
  } catch {
17036
17619
  return false;
17037
17620
  }
17038
17621
  });
17622
+ let findWiredWcId = null;
17623
+ function wireFindEvents(wc) {
17624
+ if (findWiredWcId === wc.id) return;
17625
+ if (findWiredWcId !== null) {
17626
+ const prev = tabManager.findTabByWebContentsId(findWiredWcId);
17627
+ if (prev) prev.view.webContents.removeAllListeners("found-in-page");
17628
+ }
17629
+ findWiredWcId = wc.id;
17630
+ wc.on("found-in-page", (_event, result) => {
17631
+ if (!chromeView.webContents.isDestroyed()) {
17632
+ chromeView.webContents.send(Channels.FIND_IN_PAGE_RESULT, result);
17633
+ }
17634
+ });
17635
+ }
17636
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_START, (_, text, options) => {
17637
+ const tab = tabManager.getActiveTab();
17638
+ if (!tab) return null;
17639
+ const wc = tab.view.webContents;
17640
+ if (wc.isDestroyed()) return null;
17641
+ wireFindEvents(wc);
17642
+ return wc.findInPage(text, {
17643
+ forward: options?.forward ?? true,
17644
+ findNext: options?.findNext ?? false
17645
+ });
17646
+ });
17647
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_NEXT, (_, forward) => {
17648
+ const tab = tabManager.getActiveTab();
17649
+ if (!tab) return null;
17650
+ const wc = tab.view.webContents;
17651
+ if (wc.isDestroyed()) return null;
17652
+ return wc.findInPage("", { forward: forward ?? true, findNext: true });
17653
+ });
17654
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_STOP, (_, action) => {
17655
+ const tab = tabManager.getActiveTab();
17656
+ if (!tab) return;
17657
+ const wc = tab.view.webContents;
17658
+ if (wc.isDestroyed()) return;
17659
+ wc.stopFindInPage(action ?? "clearSelection");
17660
+ });
17661
+ electron.ipcMain.handle(Channels.HISTORY_GET, () => {
17662
+ return getState$1();
17663
+ });
17664
+ electron.ipcMain.handle(Channels.HISTORY_SEARCH, (_, query) => {
17665
+ return search(query);
17666
+ });
17667
+ electron.ipcMain.handle(Channels.HISTORY_CLEAR, () => {
17668
+ clearAll$1();
17669
+ });
17039
17670
  electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => {
17040
17671
  windowState.uiState.devtoolsPanelOpen = !windowState.uiState.devtoolsPanelOpen;
17041
17672
  layoutViews(windowState);
17042
17673
  return { open: windowState.uiState.devtoolsPanelOpen };
17043
17674
  });
17044
- electron.ipcMain.handle("devtools-panel:resize", (_, height) => {
17675
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE, (_, height) => {
17045
17676
  const clamped = Math.max(MIN_DEVTOOLS_PANEL, Math.min(MAX_DEVTOOLS_PANEL, Math.round(height)));
17046
17677
  windowState.uiState.devtoolsPanelHeight = clamped;
17047
17678
  layoutViews(windowState);
@@ -17062,6 +17693,7 @@ function registerIpcHandlers(windowState, runtime) {
17062
17693
  });
17063
17694
  }
17064
17695
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
17696
+ const PERSIST_DEBOUNCE_MS = 500;
17065
17697
  function clone(value) {
17066
17698
  return JSON.parse(JSON.stringify(value));
17067
17699
  }
@@ -17139,7 +17771,7 @@ class AgentRuntime {
17139
17771
  createCheckpoint(name, note) {
17140
17772
  const snapshot = this.captureSession(note);
17141
17773
  const checkpoint = {
17142
- id: node_crypto.randomUUID(),
17774
+ id: crypto$1.randomUUID(),
17143
17775
  name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
17144
17776
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
17145
17777
  note: note?.trim() || void 0,
@@ -17193,7 +17825,7 @@ class AgentRuntime {
17193
17825
  }
17194
17826
  }
17195
17827
  const entry = {
17196
- id: node_crypto.randomUUID(),
17828
+ id: crypto$1.randomUUID(),
17197
17829
  source: input.source,
17198
17830
  kind,
17199
17831
  title: input.title?.trim() || void 0,
@@ -17217,7 +17849,7 @@ class AgentRuntime {
17217
17849
  // --- Speedee Flow State ---
17218
17850
  startFlow(goal, steps, startUrl) {
17219
17851
  const flow = {
17220
- id: node_crypto.randomUUID(),
17852
+ id: crypto$1.randomUUID(),
17221
17853
  goal,
17222
17854
  steps: steps.map((label) => ({ label, status: "pending" })),
17223
17855
  currentStepIndex: 0,
@@ -17403,7 +18035,14 @@ ${progress}
17403
18035
  return sanitizePersistence(null);
17404
18036
  }
17405
18037
  }
17406
- persist() {
18038
+ persistTimer = null;
18039
+ persistDirty = false;
18040
+ persistNow() {
18041
+ this.persistDirty = false;
18042
+ if (this.persistTimer) {
18043
+ clearTimeout(this.persistTimer);
18044
+ this.persistTimer = null;
18045
+ }
17407
18046
  const persisted = {
17408
18047
  session: this.state.session,
17409
18048
  supervisor: {
@@ -17414,20 +18053,36 @@ ${progress}
17414
18053
  actions: this.state.actions.slice(-120),
17415
18054
  checkpoints: this.state.checkpoints.slice(-20)
17416
18055
  };
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
- );
18056
+ try {
18057
+ fs$1.mkdirSync(path$1.dirname(getRuntimeStatePath()), { recursive: true });
18058
+ fs$1.writeFileSync(
18059
+ getRuntimeStatePath(),
18060
+ JSON.stringify(persisted, null, 2),
18061
+ "utf-8"
18062
+ );
18063
+ } catch (err) {
18064
+ console.error("[Vessel] Failed to persist runtime state:", err);
18065
+ }
18066
+ }
18067
+ schedulePersist() {
18068
+ this.persistDirty = true;
18069
+ if (this.persistTimer) return;
18070
+ this.persistTimer = setTimeout(() => {
18071
+ this.persistTimer = null;
18072
+ if (this.persistDirty) this.persistNow();
18073
+ }, PERSIST_DEBOUNCE_MS);
18074
+ }
18075
+ /** Flush any pending debounced persist to disk immediately. Call on shutdown. */
18076
+ flushPersist() {
18077
+ if (this.persistDirty) this.persistNow();
17423
18078
  }
17424
18079
  emit() {
17425
- this.persist();
18080
+ this.schedulePersist();
17426
18081
  this.updateListener?.(this.getState());
17427
18082
  }
17428
18083
  startAction(input) {
17429
18084
  const action = {
17430
- id: node_crypto.randomUUID(),
18085
+ id: crypto$1.randomUUID(),
17431
18086
  source: input.source,
17432
18087
  name: input.name,
17433
18088
  args: clone(input.args),
@@ -17461,7 +18116,7 @@ ${progress}
17461
18116
  /** Aggregate metrics for all completed actions in this session. */
17462
18117
  getMetrics() {
17463
18118
  const completed = this.state.actions.filter((a) => a.status === "completed");
17464
- const failed = this.state.actions.filter((a) => a.status === "error");
18119
+ const failed = this.state.actions.filter((a) => a.status === "failed");
17465
18120
  const durations = completed.filter((a) => a.durationMs != null).map((a) => a.durationMs);
17466
18121
  const avgDuration = durations.length > 0 ? durations.reduce((s, d) => s + d, 0) / durations.length : 0;
17467
18122
  const toolBreakdown = {};
@@ -17499,7 +18154,7 @@ ${progress}
17499
18154
  }
17500
18155
  awaitApproval(action, reason) {
17501
18156
  const approval = {
17502
- id: node_crypto.randomUUID(),
18157
+ id: crypto$1.randomUUID(),
17503
18158
  actionId: action.id,
17504
18159
  source: action.source,
17505
18160
  name: action.name,
@@ -17625,6 +18280,41 @@ function installAdBlocking(tabManager) {
17625
18280
  callback({ cancel: shouldBlockRequest(details) });
17626
18281
  });
17627
18282
  }
18283
+ function installDownloadHandler(chromeView) {
18284
+ electron.session.defaultSession.on("will-download", (_event, item) => {
18285
+ const settings2 = loadSettings();
18286
+ const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
18287
+ const filename = item.getFilename();
18288
+ const savePath = path.join(downloadDir, filename);
18289
+ item.setSavePath(savePath);
18290
+ const info = {
18291
+ filename,
18292
+ savePath,
18293
+ totalBytes: item.getTotalBytes(),
18294
+ receivedBytes: 0,
18295
+ state: "progressing"
18296
+ };
18297
+ if (!chromeView.webContents.isDestroyed()) {
18298
+ chromeView.webContents.send(Channels.DOWNLOAD_STARTED, info);
18299
+ }
18300
+ item.on("updated", (_event2, state2) => {
18301
+ info.receivedBytes = item.getReceivedBytes();
18302
+ info.totalBytes = item.getTotalBytes();
18303
+ info.state = state2 === "progressing" ? "progressing" : "interrupted";
18304
+ if (!chromeView.webContents.isDestroyed()) {
18305
+ chromeView.webContents.send(Channels.DOWNLOAD_PROGRESS, info);
18306
+ }
18307
+ });
18308
+ item.once("done", (_event2, state2) => {
18309
+ info.receivedBytes = item.getReceivedBytes();
18310
+ info.state = state2 === "completed" ? "completed" : "cancelled";
18311
+ if (!chromeView.webContents.isDestroyed()) {
18312
+ chromeView.webContents.send(Channels.DOWNLOAD_DONE, info);
18313
+ }
18314
+ });
18315
+ });
18316
+ }
18317
+ let runtime = null;
17628
18318
  function rendererUrlFor(view) {
17629
18319
  if (!process.env.ELECTRON_RENDERER_URL) return null;
17630
18320
  const url = new URL(process.env.ELECTRON_RENDERER_URL);
@@ -17705,7 +18395,6 @@ async function bootstrap() {
17705
18395
  if (settings2.clearBookmarksOnLaunch) {
17706
18396
  clearAll();
17707
18397
  }
17708
- let runtime = null;
17709
18398
  const windowState = createMainWindow((tabs, activeId) => {
17710
18399
  windowState.chromeView.webContents.send(
17711
18400
  Channels.TAB_STATE_UPDATE,
@@ -17727,15 +18416,10 @@ async function bootstrap() {
17727
18416
  const registerHighlightShortcut = () => {
17728
18417
  electron.globalShortcut.unregister("CommandOrControl+H");
17729
18418
  const success = electron.globalShortcut.register("CommandOrControl+H", () => {
17730
- console.log("[Vessel] Ctrl+H shortcut triggered");
17731
18419
  const activeTab = tabManager.getActiveTab();
17732
- if (!activeTab) {
17733
- console.log("[Vessel] No active tab");
17734
- return;
17735
- }
18420
+ if (!activeTab) return;
17736
18421
  tabManager.captureHighlightFromActiveTab();
17737
18422
  });
17738
- console.log("[Vessel] Ctrl+H shortcut registered:", success);
17739
18423
  if (!success) {
17740
18424
  console.warn("[Vessel] Failed to register Ctrl+H shortcut");
17741
18425
  }
@@ -17761,6 +18445,11 @@ async function bootstrap() {
17761
18445
  chromeView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
17762
18446
  sidebarView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
17763
18447
  });
18448
+ subscribe$1((state2) => {
18449
+ chromeView.webContents.send(Channels.HISTORY_UPDATE, state2);
18450
+ sidebarView.webContents.send(Channels.HISTORY_UPDATE, state2);
18451
+ });
18452
+ installDownloadHandler(chromeView);
17764
18453
  const chromeUrl = rendererUrlFor("chrome");
17765
18454
  const sidebarUrl = rendererUrlFor("sidebar");
17766
18455
  const devtoolsUrl = rendererUrlFor("devtools");
@@ -17800,6 +18489,7 @@ electron.app.whenReady().then(bootstrap).catch((error) => {
17800
18489
  });
17801
18490
  electron.app.on("window-all-closed", () => {
17802
18491
  electron.globalShortcut.unregisterAll();
18492
+ runtime?.flushPersist();
17803
18493
  void stopMcpServer().finally(() => {
17804
18494
  electron.app.quit();
17805
18495
  });