@quanta-intellect/vessel-browser 0.1.20 → 0.1.24

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
@@ -3,11 +3,12 @@ const electron = require("electron");
3
3
  const fs$1 = require("node:fs");
4
4
  const path = require("path");
5
5
  const fs = require("fs");
6
- const crypto = require("crypto");
7
- const crypto$1 = require("node:crypto");
6
+ const crypto$1 = require("crypto");
7
+ const crypto$2 = require("node:crypto");
8
8
  const path$1 = require("node:path");
9
9
  const Anthropic = require("@anthropic-ai/sdk");
10
10
  const OpenAI = require("openai");
11
+ const node_module = require("node:module");
11
12
  const zod = require("zod");
12
13
  const http = require("node:http");
13
14
  const os = require("node:os");
@@ -16,7 +17,7 @@ const streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHt
16
17
  const defaults = {
17
18
  defaultUrl: "https://start.duckduckgo.com",
18
19
  theme: "dark",
19
- sidebarWidth: 340,
20
+ sidebarWidth: 400,
20
21
  mcpPort: 3100,
21
22
  autoRestoreSession: true,
22
23
  clearBookmarksOnLaunch: false,
@@ -36,11 +37,20 @@ const defaults = {
36
37
  expiresAt: ""
37
38
  }
38
39
  };
40
+ const SAVE_DEBOUNCE_MS$3 = 150;
39
41
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
40
42
  let settings = null;
41
43
  let settingsIssues = [];
44
+ let saveTimer$3 = null;
45
+ let saveDirty$3 = false;
46
+ function getUserDataPath() {
47
+ if (typeof electron.app?.getPath === "function") {
48
+ return electron.app.getPath("userData");
49
+ }
50
+ return path.join(process.cwd(), ".vessel-test-data");
51
+ }
42
52
  function getSettingsPath() {
43
- return path.join(electron.app.getPath("userData"), "vessel-settings.json");
53
+ return path.join(getUserDataPath(), "vessel-settings.json");
44
54
  }
45
55
  function getSettingsLoadIssues() {
46
56
  return settingsIssues.map((issue) => ({ ...issue }));
@@ -87,11 +97,26 @@ function loadSettings() {
87
97
  }
88
98
  return settings;
89
99
  }
90
- function saveSettings() {
91
- fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
100
+ function persistNow$3() {
101
+ saveDirty$3 = false;
102
+ if (saveTimer$3) {
103
+ clearTimeout(saveTimer$3);
104
+ saveTimer$3 = null;
105
+ }
106
+ return fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
92
107
  () => fs.promises.writeFile(getSettingsPath(), JSON.stringify(settings, null, 2))
93
108
  ).catch((err) => console.error("[Vessel] Failed to save settings:", err));
94
109
  }
110
+ function saveSettings() {
111
+ saveDirty$3 = true;
112
+ if (saveTimer$3) return;
113
+ saveTimer$3 = setTimeout(() => {
114
+ saveTimer$3 = null;
115
+ if (saveDirty$3) {
116
+ void persistNow$3();
117
+ }
118
+ }, SAVE_DEBOUNCE_MS$3);
119
+ }
95
120
  function setSetting(key, value) {
96
121
  loadSettings();
97
122
  if (key === "mcpPort") {
@@ -102,6 +127,9 @@ function setSetting(key, value) {
102
127
  saveSettings();
103
128
  return { ...settings };
104
129
  }
130
+ function flushPersist$3() {
131
+ return saveDirty$3 ? persistNow$3() : Promise.resolve();
132
+ }
105
133
  function checkDomainPolicy(url) {
106
134
  if (!url || url.startsWith("about:")) return null;
107
135
  const settings2 = loadSettings();
@@ -215,7 +243,7 @@ class Tab {
215
243
  this._state.canGoForward = this.urlForwardStack.length > 0;
216
244
  this.onChange();
217
245
  };
218
- wc.on("did-navigate", (_event, url) => {
246
+ const recordNavigation = (url) => {
219
247
  if (this.navigatingViaHistory) {
220
248
  this.navigatingViaHistory = false;
221
249
  this.lastCommittedUrl = url;
@@ -231,6 +259,9 @@ class Tab {
231
259
  }
232
260
  this.lastCommittedUrl = url;
233
261
  syncNavigationState();
262
+ };
263
+ wc.on("did-navigate", (_event, url) => {
264
+ recordNavigation(url);
234
265
  });
235
266
  wc.on("page-title-updated", (_, title) => {
236
267
  this._state.title = title;
@@ -244,8 +275,9 @@ class Tab {
244
275
  this._state.isLoading = false;
245
276
  syncNavigationState();
246
277
  });
247
- wc.on("did-navigate-in-page", () => {
248
- syncNavigationState();
278
+ wc.on("did-navigate-in-page", (_event, url, isMainFrame) => {
279
+ if (!isMainFrame) return;
280
+ recordNavigation(url);
249
281
  this.onPageLoad?.(wc.getURL(), wc);
250
282
  });
251
283
  wc.on("did-finish-load", () => {
@@ -533,6 +565,9 @@ class Tab {
533
565
  }
534
566
  let state$3 = null;
535
567
  const listeners$2 = /* @__PURE__ */ new Set();
568
+ const SAVE_DEBOUNCE_MS$2 = 250;
569
+ let saveTimer$2 = null;
570
+ let saveDirty$2 = false;
536
571
  function getHighlightsPath() {
537
572
  return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
538
573
  }
@@ -549,8 +584,18 @@ function load$2() {
549
584
  }
550
585
  return state$3;
551
586
  }
587
+ function persistNow$2() {
588
+ if (!state$3) return Promise.resolve();
589
+ saveDirty$2 = false;
590
+ return fs.promises.writeFile(getHighlightsPath(), JSON.stringify(state$3, null, 2), "utf-8").catch((err) => console.error("[Vessel] Failed to save highlights:", err));
591
+ }
552
592
  function save$2() {
553
- fs.promises.writeFile(getHighlightsPath(), JSON.stringify(state$3, null, 2), "utf-8").catch((err) => console.error("[Vessel] Failed to save highlights:", err));
593
+ saveDirty$2 = true;
594
+ if (saveTimer$2) clearTimeout(saveTimer$2);
595
+ saveTimer$2 = setTimeout(() => {
596
+ saveTimer$2 = null;
597
+ void persistNow$2();
598
+ }, SAVE_DEBOUNCE_MS$2);
554
599
  }
555
600
  function emit$2() {
556
601
  if (!state$3) return;
@@ -580,7 +625,7 @@ function getHighlightsForUrl(url) {
580
625
  function addHighlight(url, selector, text, label, color, source) {
581
626
  load$2();
582
627
  const highlight = {
583
- id: crypto.randomUUID(),
628
+ id: crypto$1.randomUUID(),
584
629
  url: normalizeUrl(url),
585
630
  selector: selector || void 0,
586
631
  text: text || void 0,
@@ -631,6 +676,14 @@ function clearHighlightsForUrl(url) {
631
676
  }
632
677
  return removed;
633
678
  }
679
+ function flushPersist$2() {
680
+ if (saveTimer$2) {
681
+ clearTimeout(saveTimer$2);
682
+ saveTimer$2 = null;
683
+ }
684
+ if (!saveDirty$2) return Promise.resolve();
685
+ return persistNow$2();
686
+ }
634
687
  const HIGHLIGHT_COLORS = {
635
688
  yellow: {
636
689
  solid: "#f0c636",
@@ -1201,8 +1254,11 @@ function persistHighlight(url, text) {
1201
1254
  return { success: true, text: capped, id: highlight.id };
1202
1255
  }
1203
1256
  const MAX_HISTORY_ENTRIES = 5e3;
1257
+ const SAVE_DEBOUNCE_MS$1 = 250;
1204
1258
  let state$2 = null;
1205
1259
  const listeners$1 = /* @__PURE__ */ new Set();
1260
+ let saveTimer$1 = null;
1261
+ let saveDirty$1 = false;
1206
1262
  function getHistoryPath() {
1207
1263
  return path.join(electron.app.getPath("userData"), "vessel-history.json");
1208
1264
  }
@@ -1219,8 +1275,13 @@ function load$1() {
1219
1275
  }
1220
1276
  return state$2;
1221
1277
  }
1222
- function save$1() {
1223
- fs.promises.mkdir(path.dirname(getHistoryPath()), { recursive: true }).then(
1278
+ function persistNow$1() {
1279
+ saveDirty$1 = false;
1280
+ if (saveTimer$1) {
1281
+ clearTimeout(saveTimer$1);
1282
+ saveTimer$1 = null;
1283
+ }
1284
+ return fs.promises.mkdir(path.dirname(getHistoryPath()), { recursive: true }).then(
1224
1285
  () => fs.promises.writeFile(
1225
1286
  getHistoryPath(),
1226
1287
  JSON.stringify(state$2, null, 2),
@@ -1228,6 +1289,16 @@ function save$1() {
1228
1289
  )
1229
1290
  ).catch((err) => console.error("[Vessel] Failed to save history:", err));
1230
1291
  }
1292
+ function save$1() {
1293
+ saveDirty$1 = true;
1294
+ if (saveTimer$1) return;
1295
+ saveTimer$1 = setTimeout(() => {
1296
+ saveTimer$1 = null;
1297
+ if (saveDirty$1) {
1298
+ void persistNow$1();
1299
+ }
1300
+ }, SAVE_DEBOUNCE_MS$1);
1301
+ }
1231
1302
  function emit$1() {
1232
1303
  if (!state$2) return;
1233
1304
  const snapshot = { entries: [...state$2.entries] };
@@ -1282,6 +1353,9 @@ function clearAll$1() {
1282
1353
  save$1();
1283
1354
  emit$1();
1284
1355
  }
1356
+ function flushPersist$1() {
1357
+ return saveDirty$1 ? persistNow$1() : Promise.resolve();
1358
+ }
1285
1359
  const MAX_CONSOLE_ENTRIES = 500;
1286
1360
  const MAX_NETWORK_ENTRIES = 200;
1287
1361
  const MAX_ERROR_ENTRIES = 200;
@@ -1999,7 +2073,7 @@ class TabManager {
1999
2073
  }
2000
2074
  createTab(url = "about:blank", options) {
2001
2075
  const background = options?.background ?? false;
2002
- const id = crypto.randomUUID();
2076
+ const id = crypto$1.randomUUID();
2003
2077
  const tab = new Tab(id, url, () => this.broadcastState(), {
2004
2078
  adBlockingEnabled: options?.adBlockingEnabled,
2005
2079
  parentWindow: this.window,
@@ -2039,6 +2113,10 @@ class TabManager {
2039
2113
  closeTab(id) {
2040
2114
  const tab = this.tabs.get(id);
2041
2115
  if (!tab) return;
2116
+ const wcId = tab.webContentsId;
2117
+ if (wcId !== void 0) {
2118
+ this.lastReapply.delete(wcId);
2119
+ }
2042
2120
  destroySession(id);
2043
2121
  this.window.contentView.removeChildView(tab.view);
2044
2122
  tab.destroy();
@@ -2296,6 +2374,10 @@ const Channels = {
2296
2374
  AI_STREAM_START: "ai:stream-start",
2297
2375
  AI_STREAM_CHUNK: "ai:stream-chunk",
2298
2376
  AI_STREAM_END: "ai:stream-end",
2377
+ AI_STREAM_IDLE: "ai:stream-idle",
2378
+ AUTOMATION_ACTIVITY_START: "automation:activity-start",
2379
+ AUTOMATION_ACTIVITY_CHUNK: "automation:activity-chunk",
2380
+ AUTOMATION_ACTIVITY_END: "automation:activity-end",
2299
2381
  AI_CANCEL: "ai:cancel",
2300
2382
  AI_FETCH_MODELS: "ai:fetch-models",
2301
2383
  AGENT_RUNTIME_GET: "agent-runtime:get",
@@ -2324,6 +2406,7 @@ const Channels = {
2324
2406
  SETTINGS_SET: "settings:set",
2325
2407
  SETTINGS_UPDATE: "settings:update",
2326
2408
  SETTINGS_HEALTH_GET: "settings:health:get",
2409
+ SETTINGS_HEALTH_UPDATE: "settings:health:update",
2327
2410
  // Bookmarks
2328
2411
  BOOKMARKS_GET: "bookmarks:get",
2329
2412
  BOOKMARKS_UPDATE: "bookmarks:update",
@@ -2338,6 +2421,7 @@ const Channels = {
2338
2421
  HIGHLIGHT_CAPTURE_RESULT: "highlights:capture-result",
2339
2422
  HIGHLIGHT_SELECTION: "vessel:highlight-selection",
2340
2423
  HIGHLIGHT_NAV_COUNT: "highlights:nav-count",
2424
+ HIGHLIGHT_COUNT_UPDATE: "highlights:count-update",
2341
2425
  HIGHLIGHT_NAV_SCROLL: "highlights:nav-scroll",
2342
2426
  HIGHLIGHT_NAV_REMOVE: "highlights:nav-remove",
2343
2427
  HIGHLIGHT_NAV_CLEAR: "highlights:nav-clear",
@@ -2373,6 +2457,16 @@ const Channels = {
2373
2457
  VAULT_UPDATE: "vault:update",
2374
2458
  VAULT_REMOVE: "vault:remove",
2375
2459
  VAULT_AUDIT_LOG: "vault:audit-log",
2460
+ // Automation kits
2461
+ AUTOMATION_GET_INSTALLED: "automation:get-installed",
2462
+ AUTOMATION_INSTALL_FROM_FILE: "automation:install-from-file",
2463
+ AUTOMATION_UNINSTALL: "automation:uninstall",
2464
+ // Scheduled jobs
2465
+ SCHEDULE_GET_ALL: "schedule:get-all",
2466
+ SCHEDULE_CREATE: "schedule:create",
2467
+ SCHEDULE_UPDATE: "schedule:update",
2468
+ SCHEDULE_DELETE: "schedule:delete",
2469
+ SCHEDULE_JOBS_UPDATE: "schedule:jobs-update",
2376
2470
  // Window controls
2377
2471
  WINDOW_MINIMIZE: "window:minimize",
2378
2472
  WINDOW_MAXIMIZE: "window:maximize",
@@ -2533,6 +2627,7 @@ function createMainWindow(onTabStateChange) {
2533
2627
  minWidth: 800,
2534
2628
  minHeight: 600,
2535
2629
  frame: false,
2630
+ show: false,
2536
2631
  backgroundColor: "#1a1a1e",
2537
2632
  icon: getWindowIconPath()
2538
2633
  });
@@ -2575,7 +2670,7 @@ function createMainWindow(onTabStateChange) {
2575
2670
  enableClipboardShortcuts(devtoolsPanelView);
2576
2671
  const settings2 = loadSettings();
2577
2672
  const uiState = {
2578
- sidebarOpen: false,
2673
+ sidebarOpen: true,
2579
2674
  sidebarWidth: settings2.sidebarWidth,
2580
2675
  focusMode: false,
2581
2676
  settingsOpen: false,
@@ -3392,7 +3487,7 @@ function getDeviceId() {
3392
3487
  if (deviceId) return deviceId;
3393
3488
  } catch {
3394
3489
  }
3395
- deviceId = crypto.randomUUID();
3490
+ deviceId = crypto$1.randomUUID();
3396
3491
  try {
3397
3492
  fs.mkdirSync(path.dirname(idPath), { recursive: true });
3398
3493
  fs.writeFileSync(idPath, deviceId, "utf-8");
@@ -4705,15 +4800,28 @@ function escapeHtml(str) {
4705
4800
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4706
4801
  }
4707
4802
  const mcpStatusChangeListeners = /* @__PURE__ */ new Set();
4803
+ const runtimeHealthChangeListeners = /* @__PURE__ */ new Set();
4708
4804
  function onMcpStatusChange(listener) {
4709
4805
  mcpStatusChangeListeners.add(listener);
4710
4806
  return () => {
4711
4807
  mcpStatusChangeListeners.delete(listener);
4712
4808
  };
4713
4809
  }
4810
+ function onRuntimeHealthChange(listener) {
4811
+ runtimeHealthChangeListeners.add(listener);
4812
+ return () => {
4813
+ runtimeHealthChangeListeners.delete(listener);
4814
+ };
4815
+ }
4714
4816
  function getMcpStatus() {
4715
4817
  return state$1.mcp.status;
4716
4818
  }
4819
+ function emitRuntimeHealthChange() {
4820
+ const snapshot = getRuntimeHealth();
4821
+ for (const listener of runtimeHealthChangeListeners) {
4822
+ listener(snapshot);
4823
+ }
4824
+ }
4717
4825
  const state$1 = {
4718
4826
  userDataPath: "",
4719
4827
  settingsPath: "",
@@ -4734,9 +4842,11 @@ function initializeRuntimeHealth(paths) {
4734
4842
  state$1.mcp.endpoint = null;
4735
4843
  state$1.mcp.status = "stopped";
4736
4844
  state$1.mcp.message = "MCP server has not started yet.";
4845
+ emitRuntimeHealthChange();
4737
4846
  }
4738
4847
  function setStartupIssues(issues) {
4739
4848
  state$1.startupIssues = issues.map((issue) => ({ ...issue }));
4849
+ emitRuntimeHealthChange();
4740
4850
  }
4741
4851
  function getRuntimeHealth() {
4742
4852
  return {
@@ -4764,6 +4874,7 @@ function setMcpHealth(update) {
4764
4874
  listener(state$1.mcp.status);
4765
4875
  }
4766
4876
  }
4877
+ emitRuntimeHealthChange();
4767
4878
  }
4768
4879
  const VAULT_FILENAME = "vessel-vault.enc";
4769
4880
  const KEY_FILENAME = "vessel-vault.key";
@@ -4789,7 +4900,7 @@ function getOrCreateEncryptionKey() {
4789
4900
  }
4790
4901
  return encryptedKey;
4791
4902
  }
4792
- const key = crypto$1.randomBytes(32);
4903
+ const key = crypto$2.randomBytes(32);
4793
4904
  fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
4794
4905
  if (electron.safeStorage.isEncryptionAvailable()) {
4795
4906
  const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
@@ -4802,8 +4913,8 @@ function getOrCreateEncryptionKey() {
4802
4913
  }
4803
4914
  function encrypt(plaintext) {
4804
4915
  const key = getOrCreateEncryptionKey();
4805
- const iv = crypto$1.randomBytes(IV_LENGTH);
4806
- const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv, {
4916
+ const iv = crypto$2.randomBytes(IV_LENGTH);
4917
+ const cipher = crypto$2.createCipheriv(ALGORITHM, key, iv, {
4807
4918
  authTagLength: AUTH_TAG_LENGTH
4808
4919
  });
4809
4920
  const encrypted = Buffer.concat([
@@ -4818,7 +4929,7 @@ function decrypt(data) {
4818
4929
  const iv = data.subarray(0, IV_LENGTH);
4819
4930
  const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
4820
4931
  const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
4821
- const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv, {
4932
+ const decipher = crypto$2.createDecipheriv(ALGORITHM, key, iv, {
4822
4933
  authTagLength: AUTH_TAG_LENGTH
4823
4934
  });
4824
4935
  decipher.setAuthTag(authTag);
@@ -4877,7 +4988,7 @@ function addEntry(entry) {
4877
4988
  const entries = loadVault();
4878
4989
  const newEntry = {
4879
4990
  ...entry,
4880
- id: crypto$1.randomUUID(),
4991
+ id: crypto$2.randomUUID(),
4881
4992
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4882
4993
  useCount: 0
4883
4994
  };
@@ -4936,7 +5047,7 @@ function generateTotpCode(secret) {
4936
5047
  const counterBuf = Buffer.alloc(8);
4937
5048
  counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
4938
5049
  counterBuf.writeUInt32BE(counter & 4294967295, 4);
4939
- const hmac = crypto$1.createHmac("sha1", keyBytes).update(counterBuf).digest();
5050
+ const hmac = crypto$2.createHmac("sha1", keyBytes).update(counterBuf).digest();
4940
5051
  const offset = hmac[hmac.length - 1] & 15;
4941
5052
  const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
4942
5053
  return (code % 1e6).toString().padStart(6, "0");
@@ -5542,6 +5653,48 @@ function createProvider(config) {
5542
5653
  }
5543
5654
  return new OpenAICompatProvider(normalized);
5544
5655
  }
5656
+ const require$1 = node_module.createRequire(require("url").pathToFileURL(__filename).href);
5657
+ let cachedFactory;
5658
+ function createNoopTraceSession() {
5659
+ return {
5660
+ logToolCall() {
5661
+ },
5662
+ end() {
5663
+ }
5664
+ };
5665
+ }
5666
+ function getCandidatePaths() {
5667
+ const roots = /* @__PURE__ */ new Set([
5668
+ process.cwd(),
5669
+ electron.app.getAppPath(),
5670
+ path$1.join(electron.app.getAppPath(), "..")
5671
+ ]);
5672
+ return Array.from(roots).map(
5673
+ (root) => path$1.join(root, "src", "main", "telemetry", "trace-logger.local.cjs")
5674
+ );
5675
+ }
5676
+ function loadLocalFactory() {
5677
+ if (cachedFactory !== void 0) return cachedFactory;
5678
+ cachedFactory = null;
5679
+ if (electron.app.isPackaged) return cachedFactory;
5680
+ for (const candidate of getCandidatePaths()) {
5681
+ if (!fs$1.existsSync(candidate)) continue;
5682
+ try {
5683
+ const loaded = require$1(candidate);
5684
+ if (typeof loaded.createTraceSession === "function") {
5685
+ cachedFactory = loaded.createTraceSession;
5686
+ return cachedFactory;
5687
+ }
5688
+ } catch (err) {
5689
+ console.warn("[dev-trace] Failed to load local trace logger:", err);
5690
+ }
5691
+ }
5692
+ return cachedFactory;
5693
+ }
5694
+ function createTraceSession(query, url, title) {
5695
+ const factory = loadLocalFactory();
5696
+ return factory ? factory(query, url, title) : createNoopTraceSession();
5697
+ }
5545
5698
  const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
5546
5699
  const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
5547
5700
  function elementLabel(el) {
@@ -7186,7 +7339,7 @@ const TOOL_DEFINITIONS = [
7186
7339
  {
7187
7340
  name: "click",
7188
7341
  title: "Click Element",
7189
- description: "Click an element on the page by its index number or CSS selector.",
7342
+ description: "Click an element on the page by its index number or CSS selector. Use this to check or uncheck checkboxes and to select radio buttons — do NOT use select_option for those.",
7190
7343
  inputSchema: {
7191
7344
  index: zod.z.number().optional().describe("Element index from the page content listing"),
7192
7345
  selector: zod.z.string().optional().describe("CSS selector as fallback")
@@ -7211,7 +7364,7 @@ const TOOL_DEFINITIONS = [
7211
7364
  {
7212
7365
  name: "select_option",
7213
7366
  title: "Select Option",
7214
- description: "Select an option in a dropdown by visible label or option value.",
7367
+ description: "Select an option in a <select> dropdown by visible label or option value. Only works on <select> elements — for checkboxes or radio buttons use click instead.",
7215
7368
  inputSchema: {
7216
7369
  index: zod.z.number().optional().describe("The select element index number"),
7217
7370
  selector: zod.z.string().optional().describe("CSS selector as fallback"),
@@ -8021,8 +8174,11 @@ function getBookmarkSearchMatch(args) {
8021
8174
  }
8022
8175
  const UNSORTED_ID = "unsorted";
8023
8176
  const ARCHIVE_FOLDER_NAME = "Archive";
8177
+ const SAVE_DEBOUNCE_MS = 250;
8024
8178
  let state = null;
8025
8179
  const listeners = /* @__PURE__ */ new Set();
8180
+ let saveTimer = null;
8181
+ let saveDirty = false;
8026
8182
  function cloneState(current) {
8027
8183
  return {
8028
8184
  folders: current.folders.map((folder) => ({ ...folder })),
@@ -8046,8 +8202,13 @@ function load() {
8046
8202
  }
8047
8203
  return state;
8048
8204
  }
8049
- function save() {
8050
- fs.promises.mkdir(path.dirname(getBookmarksPath()), { recursive: true }).then(
8205
+ function persistNow() {
8206
+ saveDirty = false;
8207
+ if (saveTimer) {
8208
+ clearTimeout(saveTimer);
8209
+ saveTimer = null;
8210
+ }
8211
+ return fs.promises.mkdir(path.dirname(getBookmarksPath()), { recursive: true }).then(
8051
8212
  () => fs.promises.writeFile(
8052
8213
  getBookmarksPath(),
8053
8214
  JSON.stringify(state, null, 2),
@@ -8055,6 +8216,16 @@ function save() {
8055
8216
  )
8056
8217
  ).catch((err) => console.error("[Vessel] Failed to save bookmarks:", err));
8057
8218
  }
8219
+ function save() {
8220
+ saveDirty = true;
8221
+ if (saveTimer) return;
8222
+ saveTimer = setTimeout(() => {
8223
+ saveTimer = null;
8224
+ if (saveDirty) {
8225
+ void persistNow();
8226
+ }
8227
+ }, SAVE_DEBOUNCE_MS);
8228
+ }
8058
8229
  function emit() {
8059
8230
  if (!state) return;
8060
8231
  const snapshot = cloneState(state);
@@ -8168,7 +8339,7 @@ function createFolderWithSummary(name, summary) {
8168
8339
  const trimmed = name.trim();
8169
8340
  if (!trimmed) throw new Error("Folder name cannot be empty");
8170
8341
  const folder = {
8171
- id: crypto.randomUUID(),
8342
+ id: crypto$1.randomUUID(),
8172
8343
  name: trimmed,
8173
8344
  summary: summary?.trim() || void 0,
8174
8345
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -8236,7 +8407,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
8236
8407
  }
8237
8408
  }
8238
8409
  const bookmark = {
8239
- id: crypto.randomUUID(),
8410
+ id: crypto$1.randomUUID(),
8240
8411
  url: normalizedUrl,
8241
8412
  title: normalizedTitle,
8242
8413
  note: note?.trim() || void 0,
@@ -8305,6 +8476,9 @@ function renameFolder(id, newName, summary) {
8305
8476
  emit();
8306
8477
  return { ...folder };
8307
8478
  }
8479
+ function flushPersist() {
8480
+ return saveDirty ? persistNow() : Promise.resolve();
8481
+ }
8308
8482
  function normalizeText(text) {
8309
8483
  return text?.trim() ?? "";
8310
8484
  }
@@ -8531,7 +8705,7 @@ function normalizeSessionName(name) {
8531
8705
  function sessionFileName(name) {
8532
8706
  const normalized = normalizeSessionName(name).toLowerCase();
8533
8707
  const slug = normalized.replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "session";
8534
- const hash = crypto$1.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
8708
+ const hash = crypto$2.createHash("sha256").update(normalized).digest("hex").slice(0, 8);
8535
8709
  return `${slug}-${hash}.json`;
8536
8710
  }
8537
8711
  function getSessionPath(name) {
@@ -12660,13 +12834,38 @@ Instructions:
12660
12834
  pageType,
12661
12835
  query
12662
12836
  );
12837
+ const trace = createTraceSession(query, activeTabUrl, activeTabTitle);
12838
+ let accumulatedResponse = "";
12839
+ const tracedOnChunk = (text) => {
12840
+ accumulatedResponse += text;
12841
+ onChunk(text);
12842
+ };
12843
+ const tracedOnEnd = () => {
12844
+ trace.end(accumulatedResponse);
12845
+ onEnd();
12846
+ };
12847
+ const tracedExecuteAction = async (name, args) => {
12848
+ const t0 = Date.now();
12849
+ let output = "";
12850
+ let isError = false;
12851
+ try {
12852
+ output = await executeAction(name, args, actionCtx);
12853
+ } catch (err) {
12854
+ isError = true;
12855
+ output = err instanceof Error ? err.message : String(err);
12856
+ throw err;
12857
+ } finally {
12858
+ trace.logToolCall(name, args, output, Date.now() - t0, isError);
12859
+ }
12860
+ return output;
12861
+ };
12663
12862
  await provider.streamAgentQuery(
12664
12863
  systemPrompt,
12665
12864
  query,
12666
12865
  contextualTools,
12667
- onChunk,
12668
- (name, args) => executeAction(name, args, actionCtx),
12669
- onEnd,
12866
+ tracedOnChunk,
12867
+ tracedExecuteAction,
12868
+ tracedOnEnd,
12670
12869
  history
12671
12870
  );
12672
12871
  return;
@@ -12696,6 +12895,27 @@ Instructions:
12696
12895
  history
12697
12896
  );
12698
12897
  }
12898
+ let activeSource = null;
12899
+ const idleListeners = /* @__PURE__ */ new Set();
12900
+ function tryBeginAIStream(source) {
12901
+ if (activeSource !== null) return false;
12902
+ activeSource = source;
12903
+ return true;
12904
+ }
12905
+ function endAIStream(source) {
12906
+ if (activeSource !== source) return;
12907
+ activeSource = null;
12908
+ for (const listener of idleListeners) {
12909
+ listener();
12910
+ }
12911
+ }
12912
+ function isAIStreamActive() {
12913
+ return activeSource !== null;
12914
+ }
12915
+ function onAIStreamIdle(listener) {
12916
+ idleListeners.add(listener);
12917
+ return () => idleListeners.delete(listener);
12918
+ }
12699
12919
  const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
12700
12920
  const DEFAULT_NOTE_FOLDER = "Vessel/Research";
12701
12921
  const DEFAULT_BOOKMARK_FOLDER = "Vessel/Bookmarks";
@@ -15214,6 +15434,13 @@ ${buildScopedContext(pageContent, mode)}`;
15214
15434
  `Navigation blocked: ${url} returned ${preCheck.detail || "dead link"}. Try a different URL or go back and choose another link.`
15215
15435
  );
15216
15436
  }
15437
+ try {
15438
+ assertSafeURL(url);
15439
+ } catch (err) {
15440
+ return asTextResponse(
15441
+ `Navigation blocked: ${err instanceof Error ? err.message : "Unsafe URL scheme"}`
15442
+ );
15443
+ }
15217
15444
  return withAction(runtime2, tabManager, "navigate", { url }, async () => {
15218
15445
  const id = tabManager.getActiveTabId();
15219
15446
  const navError = tabManager.navigateTab(id, url, postBody);
@@ -15894,27 +16121,6 @@ ${buildScopedContext(pageContent, mode)}`;
15894
16121
  }
15895
16122
  )
15896
16123
  );
15897
- server.registerTool(
15898
- "vessel_create_checkpoint",
15899
- {
15900
- title: "Create Checkpoint",
15901
- description: "Alias for vessel_checkpoint_create. Capture the current session as a checkpoint.",
15902
- inputSchema: {
15903
- name: zod.z.string().optional().describe("Optional checkpoint name"),
15904
- note: zod.z.string().optional().describe("Optional note")
15905
- }
15906
- },
15907
- async ({ name, note }) => withAction(
15908
- runtime2,
15909
- tabManager,
15910
- "create_checkpoint",
15911
- { name, note },
15912
- async () => {
15913
- const checkpoint = runtime2.createCheckpoint(name, note);
15914
- return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
15915
- }
15916
- )
15917
- );
15918
16124
  server.registerTool(
15919
16125
  "vessel_checkpoint_restore",
15920
16126
  {
@@ -15941,32 +16147,6 @@ ${buildScopedContext(pageContent, mode)}`;
15941
16147
  }
15942
16148
  )
15943
16149
  );
15944
- server.registerTool(
15945
- "vessel_restore_checkpoint",
15946
- {
15947
- title: "Restore Checkpoint",
15948
- description: "Alias for vessel_checkpoint_restore. Restore a saved checkpoint by ID or exact name.",
15949
- inputSchema: {
15950
- checkpointId: zod.z.string().optional().describe("Checkpoint ID"),
15951
- name: zod.z.string().optional().describe("Exact checkpoint name")
15952
- }
15953
- },
15954
- async ({ checkpointId, name }) => withAction(
15955
- runtime2,
15956
- tabManager,
15957
- "restore_checkpoint",
15958
- { checkpointId, name },
15959
- async () => {
15960
- const state2 = runtime2.getState();
15961
- const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
15962
- if (!checkpoint) {
15963
- return "Error: No matching checkpoint found";
15964
- }
15965
- runtime2.restoreCheckpoint(checkpoint.id);
15966
- return `Restored checkpoint ${checkpoint.name}`;
15967
- }
15968
- )
15969
- );
15970
16150
  server.registerTool(
15971
16151
  "vessel_save_session",
15972
16152
  {
@@ -18099,7 +18279,7 @@ function startMcpServer(tabManager, runtime2, port) {
18099
18279
  status: "starting",
18100
18280
  message: `Starting MCP server on port ${port}.`
18101
18281
  });
18102
- mcpAuthToken = crypto$1.randomBytes(32).toString("hex");
18282
+ mcpAuthToken = crypto$2.randomBytes(32).toString("hex");
18103
18283
  return new Promise((resolve) => {
18104
18284
  const server = http.createServer(async (req, res) => {
18105
18285
  const url = new URL(req.url || "/", `http://localhost:${port}`);
@@ -18190,8 +18370,7 @@ function startMcpServer(tabManager, runtime2, port) {
18190
18370
  status: "ready",
18191
18371
  message: `MCP server listening on ${endpoint}.`
18192
18372
  });
18193
- console.log(`[Vessel MCP] Server listening on ${endpoint}`);
18194
- console.log(`[Vessel MCP] Auth token: ${mcpAuthToken}`);
18373
+ console.log(`[Vessel MCP] Server listening on ${endpoint} (auth enabled)`);
18195
18374
  finish({
18196
18375
  ok: true,
18197
18376
  configuredPort: port,
@@ -18229,6 +18408,372 @@ function stopMcpServer() {
18229
18408
  });
18230
18409
  });
18231
18410
  }
18411
+ const BUNDLED_KIT_IDS = /* @__PURE__ */ new Set([
18412
+ "research-collect",
18413
+ "price-scout",
18414
+ "form-filler"
18415
+ ]);
18416
+ function getUserKitsDir() {
18417
+ return path$1.join(electron.app.getPath("userData"), "kits");
18418
+ }
18419
+ function ensureKitsDir() {
18420
+ const dir = getUserKitsDir();
18421
+ if (!fs$1.existsSync(dir)) {
18422
+ fs$1.mkdirSync(dir, { recursive: true });
18423
+ }
18424
+ }
18425
+ function isValidKit(value) {
18426
+ if (!value || typeof value !== "object") return false;
18427
+ const k = value;
18428
+ return typeof k.id === "string" && k.id.length > 0 && typeof k.name === "string" && k.name.length > 0 && typeof k.description === "string" && typeof k.icon === "string" && typeof k.promptTemplate === "string" && k.promptTemplate.length > 0 && Array.isArray(k.inputs);
18429
+ }
18430
+ function getInstalledKits() {
18431
+ ensureKitsDir();
18432
+ const dir = getUserKitsDir();
18433
+ let files;
18434
+ try {
18435
+ files = fs$1.readdirSync(dir).filter((f) => f.endsWith(".kit.json"));
18436
+ } catch {
18437
+ return [];
18438
+ }
18439
+ const kits = [];
18440
+ for (const file of files) {
18441
+ try {
18442
+ const raw = fs$1.readFileSync(path$1.join(dir, file), "utf-8");
18443
+ const parsed = JSON.parse(raw);
18444
+ if (isValidKit(parsed)) {
18445
+ kits.push(parsed);
18446
+ } else {
18447
+ console.warn(`[kit-registry] Skipping invalid kit file: ${file}`);
18448
+ }
18449
+ } catch {
18450
+ console.warn(`[kit-registry] Failed to read kit file: ${file}`);
18451
+ }
18452
+ }
18453
+ return kits;
18454
+ }
18455
+ async function installKitFromFile() {
18456
+ const { canceled, filePaths } = await electron.dialog.showOpenDialog({
18457
+ title: "Install Automation Kit",
18458
+ filters: [{ name: "Automation Kit", extensions: ["kit.json", "json"] }],
18459
+ properties: ["openFile"]
18460
+ });
18461
+ if (canceled || filePaths.length === 0) {
18462
+ return { ok: false, error: "canceled" };
18463
+ }
18464
+ let raw;
18465
+ try {
18466
+ raw = fs$1.readFileSync(filePaths[0], "utf-8");
18467
+ } catch {
18468
+ return { ok: false, error: "Could not read the selected file." };
18469
+ }
18470
+ let parsed;
18471
+ try {
18472
+ parsed = JSON.parse(raw);
18473
+ } catch {
18474
+ return { ok: false, error: "File is not valid JSON." };
18475
+ }
18476
+ if (!isValidKit(parsed)) {
18477
+ return {
18478
+ ok: false,
18479
+ error: "File is not a valid automation kit. Required fields: id, name, description, icon, inputs, promptTemplate."
18480
+ };
18481
+ }
18482
+ if (BUNDLED_KIT_IDS.has(parsed.id)) {
18483
+ return {
18484
+ ok: false,
18485
+ error: `Kit id "${parsed.id}" conflicts with a built-in kit and cannot be overwritten.`
18486
+ };
18487
+ }
18488
+ ensureKitsDir();
18489
+ const dest = path$1.join(getUserKitsDir(), `${parsed.id}.kit.json`);
18490
+ try {
18491
+ fs$1.writeFileSync(dest, JSON.stringify(parsed, null, 2), "utf-8");
18492
+ } catch {
18493
+ return { ok: false, error: "Failed to save the kit file." };
18494
+ }
18495
+ return { ok: true, kit: parsed };
18496
+ }
18497
+ function uninstallKit(id) {
18498
+ if (BUNDLED_KIT_IDS.has(id)) {
18499
+ return { ok: false, error: "Built-in kits cannot be removed." };
18500
+ }
18501
+ ensureKitsDir();
18502
+ const target = path$1.join(getUserKitsDir(), `${id}.kit.json`);
18503
+ if (!fs$1.existsSync(target)) {
18504
+ return { ok: false, error: "Kit not found." };
18505
+ }
18506
+ try {
18507
+ fs$1.unlinkSync(target);
18508
+ return { ok: true };
18509
+ } catch {
18510
+ return { ok: false, error: "Failed to remove the kit file." };
18511
+ }
18512
+ }
18513
+ let jobs = [];
18514
+ let removeIdleListener = null;
18515
+ let broadcastFn = null;
18516
+ function getJobsPath() {
18517
+ return path$1.join(electron.app.getPath("userData"), "scheduled-jobs.json");
18518
+ }
18519
+ function loadJobs() {
18520
+ try {
18521
+ const raw = fs$1.readFileSync(getJobsPath(), "utf-8");
18522
+ const parsed = JSON.parse(raw);
18523
+ if (Array.isArray(parsed)) {
18524
+ jobs = parsed;
18525
+ }
18526
+ } catch {
18527
+ jobs = [];
18528
+ }
18529
+ }
18530
+ function saveJobs() {
18531
+ try {
18532
+ fs$1.writeFileSync(getJobsPath(), JSON.stringify(jobs, null, 2), "utf-8");
18533
+ } catch (err) {
18534
+ console.warn("[scheduler] Failed to save jobs:", err);
18535
+ }
18536
+ }
18537
+ function normalizeJob(job, now = /* @__PURE__ */ new Date()) {
18538
+ if (!job.enabled) return false;
18539
+ if (job.schedule.type === "once") {
18540
+ const runAt = job.schedule.runAt ? new Date(job.schedule.runAt) : null;
18541
+ if (!runAt || Number.isNaN(runAt.getTime()) || runAt <= now) {
18542
+ job.enabled = false;
18543
+ return true;
18544
+ }
18545
+ const nextRunAt2 = runAt.toISOString();
18546
+ if (job.nextRunAt !== nextRunAt2) {
18547
+ job.nextRunAt = nextRunAt2;
18548
+ return true;
18549
+ }
18550
+ return false;
18551
+ }
18552
+ const nextRunAt = computeNextRun(job.schedule, now).toISOString();
18553
+ if (job.nextRunAt !== nextRunAt) {
18554
+ job.nextRunAt = nextRunAt;
18555
+ return true;
18556
+ }
18557
+ return false;
18558
+ }
18559
+ function normalizeJobs(now = /* @__PURE__ */ new Date()) {
18560
+ let changed = false;
18561
+ for (const job of jobs) {
18562
+ changed = normalizeJob(job, now) || changed;
18563
+ }
18564
+ return changed;
18565
+ }
18566
+ function isIntegerInRange(value, min, max) {
18567
+ return Number.isInteger(value) && Number(value) >= min && Number(value) <= max;
18568
+ }
18569
+ function isStringRecord(value) {
18570
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
18571
+ return Object.values(value).every((entry) => typeof entry === "string");
18572
+ }
18573
+ function computeNextRun(schedule, from = /* @__PURE__ */ new Date()) {
18574
+ switch (schedule.type) {
18575
+ case "once":
18576
+ return new Date(schedule.runAt);
18577
+ case "hourly": {
18578
+ const next = new Date(from);
18579
+ next.setMinutes(0, 0, 0);
18580
+ next.setHours(next.getHours() + 1);
18581
+ return next;
18582
+ }
18583
+ case "daily": {
18584
+ const next = new Date(from);
18585
+ next.setHours(schedule.hour, schedule.minute, 0, 0);
18586
+ if (next <= from) next.setDate(next.getDate() + 1);
18587
+ return next;
18588
+ }
18589
+ case "weekly": {
18590
+ const next = new Date(from);
18591
+ next.setHours(schedule.hour, schedule.minute, 0, 0);
18592
+ const daysUntil = (schedule.dayOfWeek - next.getDay() + 7) % 7;
18593
+ if (daysUntil === 0 && next <= from) {
18594
+ next.setDate(next.getDate() + 7);
18595
+ } else {
18596
+ next.setDate(next.getDate() + (daysUntil || 7));
18597
+ }
18598
+ return next;
18599
+ }
18600
+ }
18601
+ }
18602
+ function isValidScheduleConfig(s) {
18603
+ if (!s || typeof s !== "object") return false;
18604
+ const sc = s;
18605
+ if (!["once", "hourly", "daily", "weekly"].includes(sc.type)) return false;
18606
+ if (sc.type === "once") {
18607
+ if (typeof sc.runAt !== "string") return false;
18608
+ if (Number.isNaN(new Date(sc.runAt).getTime())) return false;
18609
+ }
18610
+ if ((sc.type === "daily" || sc.type === "weekly") && (!isIntegerInRange(sc.hour, 0, 23) || !isIntegerInRange(sc.minute, 0, 59)))
18611
+ return false;
18612
+ if (sc.type === "weekly" && !isIntegerInRange(sc.dayOfWeek, 0, 6)) return false;
18613
+ return true;
18614
+ }
18615
+ function isValidJobData(v) {
18616
+ if (!v || typeof v !== "object") return false;
18617
+ const j = v;
18618
+ return typeof j.kitId === "string" && j.kitId.length > 0 && typeof j.kitName === "string" && j.kitName.length > 0 && typeof j.kitIcon === "string" && typeof j.renderedPrompt === "string" && j.renderedPrompt.length > 0 && (j.fieldValues === void 0 || isStringRecord(j.fieldValues)) && isValidScheduleConfig(j.schedule) && typeof j.enabled === "boolean";
18619
+ }
18620
+ async function fireJob(job, windowState, runtime2) {
18621
+ const { chromeView, sidebarView, devtoolsPanelView, tabManager } = windowState;
18622
+ const send = (channel, ...args) => {
18623
+ if (!chromeView.webContents.isDestroyed())
18624
+ chromeView.webContents.send(channel, ...args);
18625
+ if (!sidebarView.webContents.isDestroyed())
18626
+ sidebarView.webContents.send(channel, ...args);
18627
+ if (!devtoolsPanelView.webContents.isDestroyed())
18628
+ devtoolsPanelView.webContents.send(channel, ...args);
18629
+ };
18630
+ const settings2 = loadSettings();
18631
+ const activityId = `scheduled:${job.id}:${Date.now()}`;
18632
+ const startActivity = () => {
18633
+ const entry = {
18634
+ id: activityId,
18635
+ source: "scheduled",
18636
+ title: job.kitName,
18637
+ icon: job.kitIcon,
18638
+ status: "running",
18639
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
18640
+ output: ""
18641
+ };
18642
+ send(Channels.AUTOMATION_ACTIVITY_START, entry);
18643
+ };
18644
+ const appendActivity = (chunk) => {
18645
+ send(Channels.AUTOMATION_ACTIVITY_CHUNK, { id: activityId, chunk });
18646
+ };
18647
+ const finishActivity = (status) => {
18648
+ send(Channels.AUTOMATION_ACTIVITY_END, {
18649
+ id: activityId,
18650
+ status,
18651
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
18652
+ });
18653
+ };
18654
+ startActivity();
18655
+ if (!settings2.chatProvider) {
18656
+ console.warn(`[scheduler] Job "${job.kitName}" skipped — no chat provider configured`);
18657
+ appendActivity(
18658
+ "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
18659
+ );
18660
+ finishActivity("failed");
18661
+ return;
18662
+ }
18663
+ console.log(`[scheduler] Firing scheduled job: ${job.kitName} (${job.id})`);
18664
+ try {
18665
+ const provider = createProvider(settings2.chatProvider);
18666
+ const activeTab = tabManager.getActiveTab();
18667
+ await handleAIQuery(
18668
+ job.renderedPrompt,
18669
+ provider,
18670
+ activeTab?.view.webContents,
18671
+ (chunk) => appendActivity(chunk),
18672
+ () => finishActivity("completed"),
18673
+ tabManager,
18674
+ runtime2
18675
+ );
18676
+ } catch (err) {
18677
+ const msg = err instanceof Error ? err.message : "Unknown error";
18678
+ appendActivity(`
18679
+ [Scheduled Kit Error: ${msg}]`);
18680
+ finishActivity("failed");
18681
+ }
18682
+ }
18683
+ function tick(windowState, runtime2) {
18684
+ if (isAIStreamActive()) return;
18685
+ const now = /* @__PURE__ */ new Date();
18686
+ let changed = false;
18687
+ for (const job of jobs) {
18688
+ if (!job.enabled) continue;
18689
+ if (now < new Date(job.nextRunAt)) continue;
18690
+ if (!tryBeginAIStream("scheduled")) break;
18691
+ void fireJob(job, windowState, runtime2).finally(() => {
18692
+ endAIStream("scheduled");
18693
+ queueMicrotask(() => tick(windowState, runtime2));
18694
+ });
18695
+ job.lastRunAt = now.toISOString();
18696
+ if (job.schedule.type === "once") {
18697
+ job.enabled = false;
18698
+ } else {
18699
+ job.nextRunAt = computeNextRun(job.schedule, now).toISOString();
18700
+ }
18701
+ changed = true;
18702
+ break;
18703
+ }
18704
+ if (changed) {
18705
+ saveJobs();
18706
+ broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
18707
+ }
18708
+ }
18709
+ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
18710
+ broadcastFn = sendToAll;
18711
+ loadJobs();
18712
+ if (normalizeJobs()) {
18713
+ saveJobs();
18714
+ }
18715
+ removeIdleListener?.();
18716
+ removeIdleListener = onAIStreamIdle(() => tick(windowState, runtime2));
18717
+ const now = /* @__PURE__ */ new Date();
18718
+ const msToNextMinute = (60 - now.getSeconds()) * 1e3 - now.getMilliseconds();
18719
+ setTimeout(() => {
18720
+ tick(windowState, runtime2);
18721
+ setInterval(() => tick(windowState, runtime2), 6e4);
18722
+ }, msToNextMinute);
18723
+ electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, () => jobs);
18724
+ electron.ipcMain.handle(Channels.SCHEDULE_CREATE, (_, rawJob) => {
18725
+ if (!isValidJobData(rawJob)) {
18726
+ throw new Error(
18727
+ "Invalid job data. Required: kitId, kitName, kitIcon, renderedPrompt, schedule, enabled."
18728
+ );
18729
+ }
18730
+ const newJob = {
18731
+ ...rawJob,
18732
+ id: crypto.randomUUID(),
18733
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
18734
+ nextRunAt: computeNextRun(rawJob.schedule).toISOString()
18735
+ };
18736
+ jobs.push(newJob);
18737
+ saveJobs();
18738
+ sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
18739
+ return newJob;
18740
+ });
18741
+ electron.ipcMain.handle(Channels.SCHEDULE_UPDATE, (_, id, updates) => {
18742
+ if (typeof id !== "string") throw new Error("id must be a string");
18743
+ const job = jobs.find((j) => j.id === id);
18744
+ if (!job) return null;
18745
+ if (updates && typeof updates === "object") {
18746
+ const u = updates;
18747
+ const wasEnabled = job.enabled;
18748
+ if (u.enabled !== void 0) job.enabled = u.enabled;
18749
+ if (u.schedule !== void 0 && isValidScheduleConfig(u.schedule)) {
18750
+ job.schedule = u.schedule;
18751
+ job.nextRunAt = computeNextRun(u.schedule).toISOString();
18752
+ }
18753
+ if (typeof u.renderedPrompt === "string" && u.renderedPrompt.length > 0) {
18754
+ job.renderedPrompt = u.renderedPrompt;
18755
+ }
18756
+ if (u.fieldValues !== void 0 && isStringRecord(u.fieldValues)) {
18757
+ job.fieldValues = u.fieldValues;
18758
+ }
18759
+ if ((u.schedule !== void 0 || u.enabled === true && !wasEnabled) && job.enabled) {
18760
+ normalizeJob(job);
18761
+ }
18762
+ }
18763
+ saveJobs();
18764
+ sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
18765
+ return job;
18766
+ });
18767
+ electron.ipcMain.handle(Channels.SCHEDULE_DELETE, (_, id) => {
18768
+ if (typeof id !== "string") throw new Error("id must be a string");
18769
+ const before = jobs.length;
18770
+ jobs = jobs.filter((j) => j.id !== id);
18771
+ if (jobs.length === before) return false;
18772
+ saveJobs();
18773
+ sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
18774
+ return true;
18775
+ });
18776
+ }
18232
18777
  let activeChatProvider = null;
18233
18778
  function assertString(value, name) {
18234
18779
  if (typeof value !== "string") throw new Error(`${name} must be a string`);
@@ -18247,9 +18792,30 @@ function registerIpcHandlers(windowState, runtime2) {
18247
18792
  sidebarView.webContents.send(channel, ...args);
18248
18793
  devtoolsPanelView.webContents.send(channel, ...args);
18249
18794
  };
18795
+ const getActiveHighlightCountSafe = async () => {
18796
+ const tab = tabManager.getActiveTab();
18797
+ if (!tab) return 0;
18798
+ const wc = tab.view.webContents;
18799
+ if (wc.isDestroyed()) return 0;
18800
+ try {
18801
+ return await getHighlightCount(wc) ?? 0;
18802
+ } catch {
18803
+ return 0;
18804
+ }
18805
+ };
18806
+ const emitHighlightCount = async () => {
18807
+ const count = await getActiveHighlightCountSafe();
18808
+ sendToRendererViews(Channels.HIGHLIGHT_COUNT_UPDATE, count);
18809
+ };
18250
18810
  runtime2.setUpdateListener((state2) => {
18251
18811
  sendToRendererViews(Channels.AGENT_RUNTIME_UPDATE, state2);
18252
18812
  });
18813
+ onRuntimeHealthChange((health) => {
18814
+ sendToRendererViews(Channels.SETTINGS_HEALTH_UPDATE, health);
18815
+ });
18816
+ onAIStreamIdle(() => {
18817
+ sendToRendererViews(Channels.AI_STREAM_IDLE);
18818
+ });
18253
18819
  electron.ipcMain.handle(Channels.TAB_CREATE, (_, url) => {
18254
18820
  const id = tabManager.createTab(url || loadSettings().defaultUrl);
18255
18821
  layoutViews(windowState);
@@ -18283,15 +18849,19 @@ function registerIpcHandlers(windowState, runtime2) {
18283
18849
  electron.ipcMain.handle(Channels.AI_QUERY, async (_, query, history) => {
18284
18850
  const settings2 = loadSettings();
18285
18851
  const chatConfig = settings2.chatProvider;
18286
- sendToRendererViews(Channels.AI_STREAM_START, query);
18287
18852
  if (!chatConfig) {
18853
+ sendToRendererViews(Channels.AI_STREAM_START, query);
18288
18854
  sendToRendererViews(
18289
18855
  Channels.AI_STREAM_CHUNK,
18290
18856
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
18291
18857
  );
18292
18858
  sendToRendererViews(Channels.AI_STREAM_END);
18293
- return;
18859
+ return { accepted: true };
18294
18860
  }
18861
+ if (!tryBeginAIStream("manual")) {
18862
+ return { accepted: false, reason: "busy" };
18863
+ }
18864
+ sendToRendererViews(Channels.AI_STREAM_START, query);
18295
18865
  try {
18296
18866
  activeChatProvider = createProvider(chatConfig);
18297
18867
  trackProviderConfigured(chatConfig.id);
@@ -18313,7 +18883,9 @@ function registerIpcHandlers(windowState, runtime2) {
18313
18883
  sendToRendererViews(Channels.AI_STREAM_END);
18314
18884
  } finally {
18315
18885
  activeChatProvider = null;
18886
+ endAIStream("manual");
18316
18887
  }
18888
+ return { accepted: true };
18317
18889
  });
18318
18890
  electron.ipcMain.handle(Channels.AI_CANCEL, () => {
18319
18891
  activeChatProvider?.cancel();
@@ -18484,6 +19056,7 @@ function registerIpcHandlers(windowState, runtime2) {
18484
19056
  if (result.success && result.text) {
18485
19057
  await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(() => {
18486
19058
  });
19059
+ await emitHighlightCount();
18487
19060
  }
18488
19061
  return result;
18489
19062
  } catch {
@@ -18491,6 +19064,9 @@ function registerIpcHandlers(windowState, runtime2) {
18491
19064
  }
18492
19065
  });
18493
19066
  tabManager.onHighlightCapture((result) => {
19067
+ if (result.success) {
19068
+ void emitHighlightCount();
19069
+ }
18494
19070
  if (!chromeView.webContents.isDestroyed()) {
18495
19071
  chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, result);
18496
19072
  }
@@ -18503,6 +19079,7 @@ function registerIpcHandlers(windowState, runtime2) {
18503
19079
  if (!tab || !tab.highlightModeActive) return;
18504
19080
  void persistAndMarkHighlight(wc, text).then((result) => {
18505
19081
  if (result.success && !chromeView.webContents.isDestroyed()) {
19082
+ void emitHighlightCount();
18506
19083
  chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, result);
18507
19084
  }
18508
19085
  });
@@ -18510,15 +19087,7 @@ function registerIpcHandlers(windowState, runtime2) {
18510
19087
  }
18511
19088
  });
18512
19089
  electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_COUNT, () => {
18513
- const tab = tabManager.getActiveTab();
18514
- if (!tab) return 0;
18515
- const wc = tab.view.webContents;
18516
- if (wc.isDestroyed()) return 0;
18517
- try {
18518
- return getHighlightCount(wc);
18519
- } catch {
18520
- return 0;
18521
- }
19090
+ return getActiveHighlightCountSafe();
18522
19091
  });
18523
19092
  electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_SCROLL, (_, index) => {
18524
19093
  const tab = tabManager.getActiveTab();
@@ -18531,24 +19100,32 @@ function registerIpcHandlers(windowState, runtime2) {
18531
19100
  return false;
18532
19101
  }
18533
19102
  });
18534
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, (_, index) => {
19103
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, async (_, index) => {
18535
19104
  const tab = tabManager.getActiveTab();
18536
19105
  if (!tab) return false;
18537
19106
  const wc = tab.view.webContents;
18538
19107
  if (wc.isDestroyed()) return false;
18539
19108
  try {
18540
- return removeHighlightAtIndex(wc, index);
19109
+ const removed = await removeHighlightAtIndex(wc, index);
19110
+ if (removed) {
19111
+ await emitHighlightCount();
19112
+ }
19113
+ return removed;
18541
19114
  } catch {
18542
19115
  return false;
18543
19116
  }
18544
19117
  });
18545
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, () => {
19118
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, async () => {
18546
19119
  const tab = tabManager.getActiveTab();
18547
19120
  if (!tab) return false;
18548
19121
  const wc = tab.view.webContents;
18549
19122
  if (wc.isDestroyed()) return false;
18550
19123
  try {
18551
- return clearAllHighlightElements(wc);
19124
+ const cleared = await clearAllHighlightElements(wc);
19125
+ if (cleared) {
19126
+ await emitHighlightCount();
19127
+ }
19128
+ return cleared;
18552
19129
  } catch {
18553
19130
  return false;
18554
19131
  }
@@ -18702,6 +19279,17 @@ function registerIpcHandlers(windowState, runtime2) {
18702
19279
  electron.ipcMain.handle(Channels.WINDOW_CLOSE, () => {
18703
19280
  mainWindow.close();
18704
19281
  });
19282
+ electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, () => {
19283
+ return getInstalledKits();
19284
+ });
19285
+ electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async () => {
19286
+ return await installKitFromFile();
19287
+ });
19288
+ electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (_event, id) => {
19289
+ assertString(id, "id");
19290
+ return uninstallKit(id);
19291
+ });
19292
+ registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
18705
19293
  }
18706
19294
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
18707
19295
  const PERSIST_DEBOUNCE_MS = 500;
@@ -18782,7 +19370,7 @@ class AgentRuntime {
18782
19370
  createCheckpoint(name, note) {
18783
19371
  const snapshot = this.captureSession(note);
18784
19372
  const checkpoint = {
18785
- id: crypto$1.randomUUID(),
19373
+ id: crypto$2.randomUUID(),
18786
19374
  name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
18787
19375
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
18788
19376
  note: note?.trim() || void 0,
@@ -18836,7 +19424,7 @@ class AgentRuntime {
18836
19424
  }
18837
19425
  }
18838
19426
  const entry = {
18839
- id: crypto$1.randomUUID(),
19427
+ id: crypto$2.randomUUID(),
18840
19428
  source: input.source,
18841
19429
  kind,
18842
19430
  title: input.title?.trim() || void 0,
@@ -18860,7 +19448,7 @@ class AgentRuntime {
18860
19448
  // --- Speedee Flow State ---
18861
19449
  startFlow(goal, steps, startUrl) {
18862
19450
  const flow = {
18863
- id: crypto$1.randomUUID(),
19451
+ id: crypto$2.randomUUID(),
18864
19452
  goal,
18865
19453
  steps: steps.map((label) => ({ label, status: "pending" })),
18866
19454
  currentStepIndex: 0,
@@ -19089,7 +19677,7 @@ ${progress}
19089
19677
  }
19090
19678
  startAction(input) {
19091
19679
  const action = {
19092
- id: crypto$1.randomUUID(),
19680
+ id: crypto$2.randomUUID(),
19093
19681
  source: input.source,
19094
19682
  name: input.name,
19095
19683
  args: clone(input.args),
@@ -19161,7 +19749,7 @@ ${progress}
19161
19749
  }
19162
19750
  awaitApproval(action, reason) {
19163
19751
  const approval = {
19164
- id: crypto$1.randomUUID(),
19752
+ id: crypto$2.randomUUID(),
19165
19753
  actionId: action.id,
19166
19754
  source: action.source,
19167
19755
  name: action.name,
@@ -19287,12 +19875,25 @@ function installAdBlocking(tabManager) {
19287
19875
  callback({ cancel: shouldBlockRequest(details) });
19288
19876
  });
19289
19877
  }
19878
+ function resolveDownloadPath(downloadDir, filename) {
19879
+ fs$1.mkdirSync(downloadDir, { recursive: true });
19880
+ const parsed = path.parse(filename);
19881
+ let attempt = 0;
19882
+ while (true) {
19883
+ const candidateName = attempt === 0 ? filename : `${parsed.name} (${attempt})${parsed.ext}`;
19884
+ const candidatePath = path.join(downloadDir, candidateName);
19885
+ if (!fs$1.existsSync(candidatePath)) {
19886
+ return candidatePath;
19887
+ }
19888
+ attempt += 1;
19889
+ }
19890
+ }
19290
19891
  function installDownloadHandler(chromeView) {
19291
19892
  electron.session.defaultSession.on("will-download", (_event, item) => {
19292
19893
  const settings2 = loadSettings();
19293
19894
  const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
19294
19895
  const filename = item.getFilename();
19295
- const savePath = path.join(downloadDir, filename);
19896
+ const savePath = resolveDownloadPath(downloadDir, filename);
19296
19897
  item.setSavePath(savePath);
19297
19898
  const info = {
19298
19899
  filename,
@@ -19363,6 +19964,21 @@ function rendererUrlFor(view) {
19363
19964
  url.searchParams.set("view", view);
19364
19965
  return url.toString();
19365
19966
  }
19967
+ function resolveRendererFile() {
19968
+ const candidates = [
19969
+ path$1.join(__dirname, "../renderer/index.html"),
19970
+ path$1.join(__dirname, "../../out/renderer/index.html"),
19971
+ path$1.join(electron.app.getAppPath(), "out/renderer/index.html"),
19972
+ path$1.join(electron.app.getAppPath(), "renderer/index.html")
19973
+ ];
19974
+ const match = candidates.find((candidate) => fs$1.existsSync(candidate));
19975
+ if (!match) {
19976
+ throw new Error(
19977
+ `Could not locate renderer/index.html. Tried: ${candidates.join(", ")}`
19978
+ );
19979
+ }
19980
+ return match;
19981
+ }
19366
19982
  function loadRenderers(chromeView, sidebarView, devtoolsPanelView) {
19367
19983
  const chromeUrl = rendererUrlFor("chrome");
19368
19984
  const sidebarUrl = rendererUrlFor("sidebar");
@@ -19372,7 +19988,7 @@ function loadRenderers(chromeView, sidebarView, devtoolsPanelView) {
19372
19988
  sidebarView.webContents.loadURL(sidebarUrl);
19373
19989
  devtoolsPanelView.webContents.loadURL(devtoolsUrl);
19374
19990
  } else {
19375
- const rendererFile = path$1.join(__dirname, "../../renderer/index.html");
19991
+ const rendererFile = resolveRendererFile();
19376
19992
  chromeView.webContents.loadFile(rendererFile, {
19377
19993
  query: { view: "chrome" }
19378
19994
  });
@@ -19384,6 +20000,181 @@ function loadRenderers(chromeView, sidebarView, devtoolsPanelView) {
19384
20000
  });
19385
20001
  }
19386
20002
  }
20003
+ function findIconBase64() {
20004
+ const candidates = [
20005
+ path$1.join(electron.app.getAppPath(), "resources", "vessel-icon-400x400.png"),
20006
+ path$1.join(process.resourcesPath, "vessel-icon-400x400.png"),
20007
+ path$1.join(__dirname, "../../resources/vessel-icon-400x400.png"),
20008
+ path$1.join(electron.app.getAppPath(), "resources", "vessel-icon.png"),
20009
+ path$1.join(process.resourcesPath, "vessel-icon.png"),
20010
+ path$1.join(__dirname, "../../resources/vessel-icon.png")
20011
+ ];
20012
+ for (const p of candidates) {
20013
+ try {
20014
+ const data = fs$1.readFileSync(p);
20015
+ return `data:image/png;base64,${data.toString("base64")}`;
20016
+ } catch {
20017
+ }
20018
+ }
20019
+ return "";
20020
+ }
20021
+ function buildSplashHTML(iconSrc) {
20022
+ const imgTag = iconSrc ? `<img class="logo" src="${iconSrc}" alt="" />` : `<div class="logo-fallback">V</div>`;
20023
+ return `<!DOCTYPE html>
20024
+ <html>
20025
+ <head>
20026
+ <meta charset="UTF-8">
20027
+ <style>
20028
+ * { margin: 0; padding: 0; box-sizing: border-box; }
20029
+ html, body {
20030
+ width: 100%; height: 100%;
20031
+ background: #1a1a1e;
20032
+ display: flex;
20033
+ flex-direction: column;
20034
+ align-items: center;
20035
+ justify-content: center;
20036
+ gap: 20px;
20037
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
20038
+ overflow: hidden;
20039
+ -webkit-app-region: drag;
20040
+ user-select: none;
20041
+ }
20042
+ .logo-wrap {
20043
+ position: relative;
20044
+ display: flex;
20045
+ align-items: center;
20046
+ justify-content: center;
20047
+ animation: pop-in 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
20048
+ }
20049
+ .glow {
20050
+ position: absolute;
20051
+ inset: -16px;
20052
+ border-radius: 36px;
20053
+ background: radial-gradient(ellipse at center,
20054
+ rgba(196, 160, 90, 0.22) 0%,
20055
+ transparent 68%
20056
+ );
20057
+ animation: glow-pulse 2.8s ease-in-out infinite;
20058
+ }
20059
+ .logo {
20060
+ width: 84px;
20061
+ height: 84px;
20062
+ border-radius: 20px;
20063
+ display: block;
20064
+ position: relative;
20065
+ }
20066
+ .logo-fallback {
20067
+ width: 84px;
20068
+ height: 84px;
20069
+ border-radius: 20px;
20070
+ background: linear-gradient(135deg, #2a2a30, #1e1e24);
20071
+ border: 1px solid rgba(196, 160, 90, 0.25);
20072
+ display: flex;
20073
+ align-items: center;
20074
+ justify-content: center;
20075
+ font-size: 36px;
20076
+ font-weight: 700;
20077
+ color: #c4a05a;
20078
+ position: relative;
20079
+ }
20080
+ .name {
20081
+ font-size: 13px;
20082
+ font-weight: 600;
20083
+ letter-spacing: 0.22em;
20084
+ color: #7a7a8a;
20085
+ text-transform: uppercase;
20086
+ animation: fade-up 0.5s 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
20087
+ }
20088
+ .dots {
20089
+ display: flex;
20090
+ gap: 6px;
20091
+ animation: fade-up 0.4s 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
20092
+ }
20093
+ .dot {
20094
+ width: 5px;
20095
+ height: 5px;
20096
+ border-radius: 50%;
20097
+ background: #3e3e50;
20098
+ animation: dot-bounce 1.5s ease-in-out infinite;
20099
+ }
20100
+ .dot:nth-child(2) { animation-delay: 0.2s; }
20101
+ .dot:nth-child(3) { animation-delay: 0.4s; }
20102
+
20103
+ @keyframes pop-in {
20104
+ from { opacity: 0; transform: scale(0.78); }
20105
+ to { opacity: 1; transform: scale(1); }
20106
+ }
20107
+ @keyframes fade-up {
20108
+ from { opacity: 0; transform: translateY(7px); }
20109
+ to { opacity: 1; transform: translateY(0); }
20110
+ }
20111
+ @keyframes glow-pulse {
20112
+ 0%, 100% { opacity: 0.55; transform: scale(1); }
20113
+ 50% { opacity: 1; transform: scale(1.1); }
20114
+ }
20115
+ @keyframes dot-bounce {
20116
+ 0%, 75%, 100% { transform: translateY(0); opacity: 0.3; }
20117
+ 40% { transform: translateY(-6px); opacity: 1; }
20118
+ }
20119
+ </style>
20120
+ </head>
20121
+ <body>
20122
+ <div class="logo-wrap">
20123
+ <div class="glow"></div>
20124
+ ${imgTag}
20125
+ </div>
20126
+ <div class="name">Vessel</div>
20127
+ <div class="dots">
20128
+ <div class="dot"></div>
20129
+ <div class="dot"></div>
20130
+ <div class="dot"></div>
20131
+ </div>
20132
+ </body>
20133
+ </html>`;
20134
+ }
20135
+ function createSplashWindow() {
20136
+ const splash = new electron.BrowserWindow({
20137
+ width: 1280,
20138
+ height: 800,
20139
+ center: true,
20140
+ frame: false,
20141
+ show: false,
20142
+ // only show once content has painted — prevents black-window flash
20143
+ resizable: false,
20144
+ movable: true,
20145
+ alwaysOnTop: true,
20146
+ skipTaskbar: true,
20147
+ backgroundColor: "#1a1a1e",
20148
+ webPreferences: {
20149
+ nodeIntegration: false,
20150
+ contextIsolation: true
20151
+ }
20152
+ });
20153
+ splash.once("ready-to-show", () => splash.show());
20154
+ const iconSrc = findIconBase64();
20155
+ const html = buildSplashHTML(iconSrc);
20156
+ try {
20157
+ const tmpDir = fs$1.mkdtempSync(path$1.join(os.tmpdir(), "vessel-splash-"));
20158
+ const tmpPath = path$1.join(tmpDir, "index.html");
20159
+ splash.once("closed", () => {
20160
+ try {
20161
+ fs$1.rmSync(tmpDir, { recursive: true, force: true });
20162
+ } catch {
20163
+ }
20164
+ });
20165
+ fs$1.writeFileSync(tmpPath, html, "utf-8");
20166
+ void splash.loadFile(tmpPath);
20167
+ } catch (err) {
20168
+ console.warn("[splash] Failed to write temp HTML, using fallback:", err);
20169
+ void splash.loadFile(path$1.join(__dirname, "../../resources/vessel-icon.png"));
20170
+ }
20171
+ return splash;
20172
+ }
20173
+ function closeSplash(splash, delayMs = 0) {
20174
+ setTimeout(() => {
20175
+ if (!splash.isDestroyed()) splash.close();
20176
+ }, delayMs);
20177
+ }
19387
20178
  let runtime = null;
19388
20179
  function checkWritableUserData(userDataPath) {
19389
20180
  const issues = [];
@@ -19448,6 +20239,7 @@ Action: Open Settings (Ctrl+,) to choose a different port, then save to restart
19448
20239
  });
19449
20240
  }
19450
20241
  async function bootstrap() {
20242
+ const splash = createSplashWindow();
19451
20243
  const settings2 = loadSettings();
19452
20244
  const userDataPath = electron.app.getPath("userData");
19453
20245
  initializeRuntimeHealth({
@@ -19459,15 +20251,51 @@ async function bootstrap() {
19459
20251
  if (settings2.clearBookmarksOnLaunch) {
19460
20252
  clearAll();
19461
20253
  }
20254
+ const syncActiveHighlightCount = async (state2) => {
20255
+ const activeTab = state2.tabManager.getActiveTab();
20256
+ const wc = activeTab?.view.webContents;
20257
+ let count = 0;
20258
+ if (wc && !wc.isDestroyed()) {
20259
+ try {
20260
+ count = await getHighlightCount(wc) ?? 0;
20261
+ } catch {
20262
+ count = 0;
20263
+ }
20264
+ }
20265
+ if (!state2.chromeView.webContents.isDestroyed()) {
20266
+ state2.chromeView.webContents.send(Channels.HIGHLIGHT_COUNT_UPDATE, count);
20267
+ }
20268
+ if (!state2.sidebarView.webContents.isDestroyed()) {
20269
+ state2.sidebarView.webContents.send(Channels.HIGHLIGHT_COUNT_UPDATE, count);
20270
+ }
20271
+ if (!state2.devtoolsPanelView.webContents.isDestroyed()) {
20272
+ state2.devtoolsPanelView.webContents.send(
20273
+ Channels.HIGHLIGHT_COUNT_UPDATE,
20274
+ count
20275
+ );
20276
+ }
20277
+ };
19462
20278
  const windowState = createMainWindow((tabs, activeId) => {
19463
20279
  windowState.chromeView.webContents.send(
19464
20280
  Channels.TAB_STATE_UPDATE,
19465
20281
  tabs,
19466
20282
  activeId
19467
20283
  );
20284
+ void syncActiveHighlightCount(windowState);
19468
20285
  layoutViews(windowState);
19469
20286
  runtime?.onTabStateChanged();
19470
20287
  });
20288
+ let didRevealMainWindow = false;
20289
+ const revealMainWindow = () => {
20290
+ if (didRevealMainWindow) return;
20291
+ didRevealMainWindow = true;
20292
+ windowState.mainWindow.show();
20293
+ closeSplash(splash, 0);
20294
+ };
20295
+ const splashTimeout = setTimeout(() => {
20296
+ console.warn("[bootstrap] Renderer did not finish loading before splash timeout");
20297
+ revealMainWindow();
20298
+ }, 8e3);
19471
20299
  const { chromeView, sidebarView, devtoolsPanelView, tabManager } = windowState;
19472
20300
  runtime = new AgentRuntime(tabManager);
19473
20301
  installAdBlocking(tabManager);
@@ -19491,7 +20319,6 @@ async function bootstrap() {
19491
20319
  startBackgroundRevalidation();
19492
20320
  startTelemetry();
19493
20321
  loadRenderers(chromeView, sidebarView, devtoolsPanelView);
19494
- await startMcpServer(tabManager, runtime, settings2.mcpPort);
19495
20322
  chromeView.webContents.once("did-finish-load", () => {
19496
20323
  const savedSession = runtime.getState().session;
19497
20324
  if (settings2.autoRestoreSession && savedSession?.tabs.length) {
@@ -19502,8 +20329,27 @@ async function bootstrap() {
19502
20329
  }
19503
20330
  layoutViews(windowState);
19504
20331
  setImmediate(() => layoutViews(windowState));
20332
+ clearTimeout(splashTimeout);
20333
+ revealMainWindow();
19505
20334
  void maybeShowStartupHealthDialog(windowState);
19506
20335
  });
20336
+ chromeView.webContents.once(
20337
+ "did-fail-load",
20338
+ (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
20339
+ if (!isMainFrame) return;
20340
+ console.error(
20341
+ "[bootstrap] Chrome renderer failed to load:",
20342
+ errorCode,
20343
+ errorDescription,
20344
+ validatedURL
20345
+ );
20346
+ clearTimeout(splashTimeout);
20347
+ revealMainWindow();
20348
+ }
20349
+ );
20350
+ startMcpServer(tabManager, runtime, settings2.mcpPort).catch((err) => {
20351
+ console.error("[bootstrap] MCP server failed to start:", err);
20352
+ });
19507
20353
  }
19508
20354
  process.on("uncaughtException", (error) => {
19509
20355
  console.error("[Vessel] Uncaught exception:", error.message, error.stack);
@@ -19523,8 +20369,15 @@ electron.app.on("window-all-closed", () => {
19523
20369
  electron.globalShortcut.unregisterAll();
19524
20370
  stopTelemetry();
19525
20371
  stopBackgroundRevalidation();
19526
- runtime?.flushPersist();
19527
- void stopMcpServer().finally(() => {
19528
- electron.app.quit();
20372
+ void Promise.all([
20373
+ runtime?.flushPersist() ?? Promise.resolve(),
20374
+ flushPersist(),
20375
+ flushPersist$1(),
20376
+ flushPersist$2(),
20377
+ flushPersist$3()
20378
+ ]).finally(() => {
20379
+ void stopMcpServer().finally(() => {
20380
+ electron.app.quit();
20381
+ });
19529
20382
  });
19530
20383
  });