@quanta-intellect/vessel-browser 0.1.19 → 0.1.21

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", () => {
@@ -362,7 +394,7 @@ class Tab {
362
394
  get state() {
363
395
  return { ...this._state };
364
396
  }
365
- navigate(url) {
397
+ navigate(url, postBody) {
366
398
  if (!/^https?:\/\//i.test(url) && !url.startsWith("about:")) {
367
399
  if (url.includes(".") && !url.includes(" ")) {
368
400
  url = "https://" + url;
@@ -375,7 +407,24 @@ class Tab {
375
407
  }
376
408
  const policyError = checkDomainPolicy(url);
377
409
  if (policyError) return policyError;
378
- this.view.webContents.loadURL(url);
410
+ if (postBody) {
411
+ const params = new URLSearchParams();
412
+ for (const [key, value] of Object.entries(postBody)) {
413
+ params.set(key, value);
414
+ }
415
+ this.view.webContents.loadURL(url, {
416
+ method: "POST",
417
+ extraHeaders: "Content-Type: application/x-www-form-urlencoded\r\n",
418
+ postData: [
419
+ {
420
+ type: "rawData",
421
+ bytes: Buffer.from(params.toString())
422
+ }
423
+ ]
424
+ });
425
+ } else {
426
+ this.view.webContents.loadURL(url);
427
+ }
379
428
  return null;
380
429
  }
381
430
  goBack() {
@@ -516,6 +565,9 @@ class Tab {
516
565
  }
517
566
  let state$3 = null;
518
567
  const listeners$2 = /* @__PURE__ */ new Set();
568
+ const SAVE_DEBOUNCE_MS$2 = 250;
569
+ let saveTimer$2 = null;
570
+ let saveDirty$2 = false;
519
571
  function getHighlightsPath() {
520
572
  return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
521
573
  }
@@ -532,8 +584,18 @@ function load$2() {
532
584
  }
533
585
  return state$3;
534
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
+ }
535
592
  function save$2() {
536
- 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);
537
599
  }
538
600
  function emit$2() {
539
601
  if (!state$3) return;
@@ -563,7 +625,7 @@ function getHighlightsForUrl(url) {
563
625
  function addHighlight(url, selector, text, label, color, source) {
564
626
  load$2();
565
627
  const highlight = {
566
- id: crypto.randomUUID(),
628
+ id: crypto$1.randomUUID(),
567
629
  url: normalizeUrl(url),
568
630
  selector: selector || void 0,
569
631
  text: text || void 0,
@@ -614,6 +676,14 @@ function clearHighlightsForUrl(url) {
614
676
  }
615
677
  return removed;
616
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
+ }
617
687
  const HIGHLIGHT_COLORS = {
618
688
  yellow: {
619
689
  solid: "#f0c636",
@@ -1184,8 +1254,11 @@ function persistHighlight(url, text) {
1184
1254
  return { success: true, text: capped, id: highlight.id };
1185
1255
  }
1186
1256
  const MAX_HISTORY_ENTRIES = 5e3;
1257
+ const SAVE_DEBOUNCE_MS$1 = 250;
1187
1258
  let state$2 = null;
1188
1259
  const listeners$1 = /* @__PURE__ */ new Set();
1260
+ let saveTimer$1 = null;
1261
+ let saveDirty$1 = false;
1189
1262
  function getHistoryPath() {
1190
1263
  return path.join(electron.app.getPath("userData"), "vessel-history.json");
1191
1264
  }
@@ -1202,8 +1275,13 @@ function load$1() {
1202
1275
  }
1203
1276
  return state$2;
1204
1277
  }
1205
- function save$1() {
1206
- 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(
1207
1285
  () => fs.promises.writeFile(
1208
1286
  getHistoryPath(),
1209
1287
  JSON.stringify(state$2, null, 2),
@@ -1211,6 +1289,16 @@ function save$1() {
1211
1289
  )
1212
1290
  ).catch((err) => console.error("[Vessel] Failed to save history:", err));
1213
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
+ }
1214
1302
  function emit$1() {
1215
1303
  if (!state$2) return;
1216
1304
  const snapshot = { entries: [...state$2.entries] };
@@ -1265,6 +1353,9 @@ function clearAll$1() {
1265
1353
  save$1();
1266
1354
  emit$1();
1267
1355
  }
1356
+ function flushPersist$1() {
1357
+ return saveDirty$1 ? persistNow$1() : Promise.resolve();
1358
+ }
1268
1359
  const MAX_CONSOLE_ENTRIES = 500;
1269
1360
  const MAX_NETWORK_ENTRIES = 200;
1270
1361
  const MAX_ERROR_ENTRIES = 200;
@@ -1982,7 +2073,7 @@ class TabManager {
1982
2073
  }
1983
2074
  createTab(url = "about:blank", options) {
1984
2075
  const background = options?.background ?? false;
1985
- const id = crypto.randomUUID();
2076
+ const id = crypto$1.randomUUID();
1986
2077
  const tab = new Tab(id, url, () => this.broadcastState(), {
1987
2078
  adBlockingEnabled: options?.adBlockingEnabled,
1988
2079
  parentWindow: this.window,
@@ -2022,6 +2113,10 @@ class TabManager {
2022
2113
  closeTab(id) {
2023
2114
  const tab = this.tabs.get(id);
2024
2115
  if (!tab) return;
2116
+ const wcId = tab.webContentsId;
2117
+ if (wcId !== void 0) {
2118
+ this.lastReapply.delete(wcId);
2119
+ }
2025
2120
  destroySession(id);
2026
2121
  this.window.contentView.removeChildView(tab.view);
2027
2122
  tab.destroy();
@@ -2037,9 +2132,10 @@ class TabManager {
2037
2132
  this.broadcastState();
2038
2133
  }
2039
2134
  }
2040
- navigateTab(id, url) {
2135
+ navigateTab(id, url, postBody) {
2041
2136
  const tab = this.tabs.get(id);
2042
- if (tab) tab.navigate(url);
2137
+ if (!tab) return `No tab with id ${id}`;
2138
+ return tab.navigate(url, postBody);
2043
2139
  }
2044
2140
  goBack(id) {
2045
2141
  return this.tabs.get(id)?.goBack() ?? false;
@@ -2278,6 +2374,10 @@ const Channels = {
2278
2374
  AI_STREAM_START: "ai:stream-start",
2279
2375
  AI_STREAM_CHUNK: "ai:stream-chunk",
2280
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",
2281
2381
  AI_CANCEL: "ai:cancel",
2282
2382
  AI_FETCH_MODELS: "ai:fetch-models",
2283
2383
  AGENT_RUNTIME_GET: "agent-runtime:get",
@@ -2306,6 +2406,7 @@ const Channels = {
2306
2406
  SETTINGS_SET: "settings:set",
2307
2407
  SETTINGS_UPDATE: "settings:update",
2308
2408
  SETTINGS_HEALTH_GET: "settings:health:get",
2409
+ SETTINGS_HEALTH_UPDATE: "settings:health:update",
2309
2410
  // Bookmarks
2310
2411
  BOOKMARKS_GET: "bookmarks:get",
2311
2412
  BOOKMARKS_UPDATE: "bookmarks:update",
@@ -2320,6 +2421,7 @@ const Channels = {
2320
2421
  HIGHLIGHT_CAPTURE_RESULT: "highlights:capture-result",
2321
2422
  HIGHLIGHT_SELECTION: "vessel:highlight-selection",
2322
2423
  HIGHLIGHT_NAV_COUNT: "highlights:nav-count",
2424
+ HIGHLIGHT_COUNT_UPDATE: "highlights:count-update",
2323
2425
  HIGHLIGHT_NAV_SCROLL: "highlights:nav-scroll",
2324
2426
  HIGHLIGHT_NAV_REMOVE: "highlights:nav-remove",
2325
2427
  HIGHLIGHT_NAV_CLEAR: "highlights:nav-clear",
@@ -2355,6 +2457,16 @@ const Channels = {
2355
2457
  VAULT_UPDATE: "vault:update",
2356
2458
  VAULT_REMOVE: "vault:remove",
2357
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",
2358
2470
  // Window controls
2359
2471
  WINDOW_MINIMIZE: "window:minimize",
2360
2472
  WINDOW_MAXIMIZE: "window:maximize",
@@ -2515,6 +2627,7 @@ function createMainWindow(onTabStateChange) {
2515
2627
  minWidth: 800,
2516
2628
  minHeight: 600,
2517
2629
  frame: false,
2630
+ show: false,
2518
2631
  backgroundColor: "#1a1a1e",
2519
2632
  icon: getWindowIconPath()
2520
2633
  });
@@ -2557,7 +2670,7 @@ function createMainWindow(onTabStateChange) {
2557
2670
  enableClipboardShortcuts(devtoolsPanelView);
2558
2671
  const settings2 = loadSettings();
2559
2672
  const uiState = {
2560
- sidebarOpen: false,
2673
+ sidebarOpen: true,
2561
2674
  sidebarWidth: settings2.sidebarWidth,
2562
2675
  focusMode: false,
2563
2676
  settingsOpen: false,
@@ -3374,7 +3487,7 @@ function getDeviceId() {
3374
3487
  if (deviceId) return deviceId;
3375
3488
  } catch {
3376
3489
  }
3377
- deviceId = crypto.randomUUID();
3490
+ deviceId = crypto$1.randomUUID();
3378
3491
  try {
3379
3492
  fs.mkdirSync(path.dirname(idPath), { recursive: true });
3380
3493
  fs.writeFileSync(idPath, deviceId, "utf-8");
@@ -4687,15 +4800,28 @@ function escapeHtml(str) {
4687
4800
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4688
4801
  }
4689
4802
  const mcpStatusChangeListeners = /* @__PURE__ */ new Set();
4803
+ const runtimeHealthChangeListeners = /* @__PURE__ */ new Set();
4690
4804
  function onMcpStatusChange(listener) {
4691
4805
  mcpStatusChangeListeners.add(listener);
4692
4806
  return () => {
4693
4807
  mcpStatusChangeListeners.delete(listener);
4694
4808
  };
4695
4809
  }
4810
+ function onRuntimeHealthChange(listener) {
4811
+ runtimeHealthChangeListeners.add(listener);
4812
+ return () => {
4813
+ runtimeHealthChangeListeners.delete(listener);
4814
+ };
4815
+ }
4696
4816
  function getMcpStatus() {
4697
4817
  return state$1.mcp.status;
4698
4818
  }
4819
+ function emitRuntimeHealthChange() {
4820
+ const snapshot = getRuntimeHealth();
4821
+ for (const listener of runtimeHealthChangeListeners) {
4822
+ listener(snapshot);
4823
+ }
4824
+ }
4699
4825
  const state$1 = {
4700
4826
  userDataPath: "",
4701
4827
  settingsPath: "",
@@ -4716,9 +4842,11 @@ function initializeRuntimeHealth(paths) {
4716
4842
  state$1.mcp.endpoint = null;
4717
4843
  state$1.mcp.status = "stopped";
4718
4844
  state$1.mcp.message = "MCP server has not started yet.";
4845
+ emitRuntimeHealthChange();
4719
4846
  }
4720
4847
  function setStartupIssues(issues) {
4721
4848
  state$1.startupIssues = issues.map((issue) => ({ ...issue }));
4849
+ emitRuntimeHealthChange();
4722
4850
  }
4723
4851
  function getRuntimeHealth() {
4724
4852
  return {
@@ -4746,6 +4874,7 @@ function setMcpHealth(update) {
4746
4874
  listener(state$1.mcp.status);
4747
4875
  }
4748
4876
  }
4877
+ emitRuntimeHealthChange();
4749
4878
  }
4750
4879
  const VAULT_FILENAME = "vessel-vault.enc";
4751
4880
  const KEY_FILENAME = "vessel-vault.key";
@@ -4771,7 +4900,7 @@ function getOrCreateEncryptionKey() {
4771
4900
  }
4772
4901
  return encryptedKey;
4773
4902
  }
4774
- const key = crypto$1.randomBytes(32);
4903
+ const key = crypto$2.randomBytes(32);
4775
4904
  fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
4776
4905
  if (electron.safeStorage.isEncryptionAvailable()) {
4777
4906
  const encrypted = electron.safeStorage.encryptString(key.toString("utf-8"));
@@ -4784,8 +4913,8 @@ function getOrCreateEncryptionKey() {
4784
4913
  }
4785
4914
  function encrypt(plaintext) {
4786
4915
  const key = getOrCreateEncryptionKey();
4787
- const iv = crypto$1.randomBytes(IV_LENGTH);
4788
- 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, {
4789
4918
  authTagLength: AUTH_TAG_LENGTH
4790
4919
  });
4791
4920
  const encrypted = Buffer.concat([
@@ -4800,7 +4929,7 @@ function decrypt(data) {
4800
4929
  const iv = data.subarray(0, IV_LENGTH);
4801
4930
  const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
4802
4931
  const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);
4803
- const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv, {
4932
+ const decipher = crypto$2.createDecipheriv(ALGORITHM, key, iv, {
4804
4933
  authTagLength: AUTH_TAG_LENGTH
4805
4934
  });
4806
4935
  decipher.setAuthTag(authTag);
@@ -4859,7 +4988,7 @@ function addEntry(entry) {
4859
4988
  const entries = loadVault();
4860
4989
  const newEntry = {
4861
4990
  ...entry,
4862
- id: crypto$1.randomUUID(),
4991
+ id: crypto$2.randomUUID(),
4863
4992
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4864
4993
  useCount: 0
4865
4994
  };
@@ -4918,7 +5047,7 @@ function generateTotpCode(secret) {
4918
5047
  const counterBuf = Buffer.alloc(8);
4919
5048
  counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
4920
5049
  counterBuf.writeUInt32BE(counter & 4294967295, 4);
4921
- const hmac = crypto$1.createHmac("sha1", keyBytes).update(counterBuf).digest();
5050
+ const hmac = crypto$2.createHmac("sha1", keyBytes).update(counterBuf).digest();
4922
5051
  const offset = hmac[hmac.length - 1] & 15;
4923
5052
  const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
4924
5053
  return (code % 1e6).toString().padStart(6, "0");
@@ -5524,6 +5653,48 @@ function createProvider(config) {
5524
5653
  }
5525
5654
  return new OpenAICompatProvider(normalized);
5526
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
+ }
5527
5698
  const CORRECT_HINT_RE = /\b(correct|right choice|this is correct|correct answer|pick this|select this|choose this|right answer)\b/i;
5528
5699
  const WRONG_HINT_RE = /\b(wrong|incorrect|not this|don't pick|do not pick|bad option|decoy)\b/i;
5529
5700
  function elementLabel(el) {
@@ -7137,9 +7308,12 @@ const TOOL_DEFINITIONS = [
7137
7308
  {
7138
7309
  name: "navigate",
7139
7310
  title: "Navigate",
7140
- description: "Navigate the browser to a URL.",
7311
+ description: "Navigate the browser to a URL. Use postBody to submit data via POST request (e.g. form submissions).",
7141
7312
  inputSchema: {
7142
- url: zod.z.string().describe("The URL to navigate to")
7313
+ url: zod.z.string().describe("The URL to navigate to"),
7314
+ postBody: zod.z.record(zod.z.string(), zod.z.string()).optional().describe(
7315
+ "Optional form fields to submit via POST (application/x-www-form-urlencoded). Only supported on http/https URLs."
7316
+ )
7143
7317
  },
7144
7318
  tier: 0
7145
7319
  },
@@ -7165,7 +7339,7 @@ const TOOL_DEFINITIONS = [
7165
7339
  {
7166
7340
  name: "click",
7167
7341
  title: "Click Element",
7168
- 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.",
7169
7343
  inputSchema: {
7170
7344
  index: zod.z.number().optional().describe("Element index from the page content listing"),
7171
7345
  selector: zod.z.string().optional().describe("CSS selector as fallback")
@@ -7190,7 +7364,7 @@ const TOOL_DEFINITIONS = [
7190
7364
  {
7191
7365
  name: "select_option",
7192
7366
  title: "Select Option",
7193
- 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.",
7194
7368
  inputSchema: {
7195
7369
  index: zod.z.number().optional().describe("The select element index number"),
7196
7370
  selector: zod.z.string().optional().describe("CSS selector as fallback"),
@@ -8000,8 +8174,11 @@ function getBookmarkSearchMatch(args) {
8000
8174
  }
8001
8175
  const UNSORTED_ID = "unsorted";
8002
8176
  const ARCHIVE_FOLDER_NAME = "Archive";
8177
+ const SAVE_DEBOUNCE_MS = 250;
8003
8178
  let state = null;
8004
8179
  const listeners = /* @__PURE__ */ new Set();
8180
+ let saveTimer = null;
8181
+ let saveDirty = false;
8005
8182
  function cloneState(current) {
8006
8183
  return {
8007
8184
  folders: current.folders.map((folder) => ({ ...folder })),
@@ -8025,8 +8202,13 @@ function load() {
8025
8202
  }
8026
8203
  return state;
8027
8204
  }
8028
- function save() {
8029
- 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(
8030
8212
  () => fs.promises.writeFile(
8031
8213
  getBookmarksPath(),
8032
8214
  JSON.stringify(state, null, 2),
@@ -8034,6 +8216,16 @@ function save() {
8034
8216
  )
8035
8217
  ).catch((err) => console.error("[Vessel] Failed to save bookmarks:", err));
8036
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
+ }
8037
8229
  function emit() {
8038
8230
  if (!state) return;
8039
8231
  const snapshot = cloneState(state);
@@ -8147,7 +8339,7 @@ function createFolderWithSummary(name, summary) {
8147
8339
  const trimmed = name.trim();
8148
8340
  if (!trimmed) throw new Error("Folder name cannot be empty");
8149
8341
  const folder = {
8150
- id: crypto.randomUUID(),
8342
+ id: crypto$1.randomUUID(),
8151
8343
  name: trimmed,
8152
8344
  summary: summary?.trim() || void 0,
8153
8345
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -8215,7 +8407,7 @@ function saveBookmarkWithPolicy(url, title, folderId, note, options) {
8215
8407
  }
8216
8408
  }
8217
8409
  const bookmark = {
8218
- id: crypto.randomUUID(),
8410
+ id: crypto$1.randomUUID(),
8219
8411
  url: normalizedUrl,
8220
8412
  title: normalizedTitle,
8221
8413
  note: note?.trim() || void 0,
@@ -8284,6 +8476,9 @@ function renameFolder(id, newName, summary) {
8284
8476
  emit();
8285
8477
  return { ...folder };
8286
8478
  }
8479
+ function flushPersist() {
8480
+ return saveDirty ? persistNow() : Promise.resolve();
8481
+ }
8287
8482
  function normalizeText(text) {
8288
8483
  return text?.trim() ?? "";
8289
8484
  }
@@ -8510,7 +8705,7 @@ function normalizeSessionName(name) {
8510
8705
  function sessionFileName(name) {
8511
8706
  const normalized = normalizeSessionName(name).toLowerCase();
8512
8707
  const slug = normalized.replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "session";
8513
- 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);
8514
8709
  return `${slug}-${hash}.json`;
8515
8710
  }
8516
8711
  function getSessionPath(name) {
@@ -11418,7 +11613,7 @@ async function executeAction(name, args, ctx) {
11418
11613
  const createdId = ctx.tabManager.createTab(
11419
11614
  typeof args.url === "string" && args.url.trim() ? args.url.trim() : "about:blank"
11420
11615
  );
11421
- const created = ctx.tabManager.getActiveTab();
11616
+ const created = ctx.tabManager.getTab(createdId);
11422
11617
  if (created) {
11423
11618
  await waitForLoad$1(created.view.webContents);
11424
11619
  return `Created tab ${createdId}${await getPostNavSummary(created.view.webContents)}`;
@@ -11431,7 +11626,8 @@ async function executeAction(name, args, ctx) {
11431
11626
  if (navValidation.status === "dead") {
11432
11627
  return `Navigation blocked: ${args.url} returned ${navValidation.detail || "dead link"}. Try a different URL or go back and choose another link.`;
11433
11628
  }
11434
- ctx.tabManager.navigateTab(tabId, args.url);
11629
+ const navError = ctx.tabManager.navigateTab(tabId, args.url, args.postBody);
11630
+ if (navError) return navError;
11435
11631
  await waitForLoad$1(wc);
11436
11632
  return `Navigated to ${wc.getURL()}${await getPostNavSummary(wc)}`;
11437
11633
  }
@@ -12638,13 +12834,38 @@ Instructions:
12638
12834
  pageType,
12639
12835
  query
12640
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
+ };
12641
12862
  await provider.streamAgentQuery(
12642
12863
  systemPrompt,
12643
12864
  query,
12644
12865
  contextualTools,
12645
- onChunk,
12646
- (name, args) => executeAction(name, args, actionCtx),
12647
- onEnd,
12866
+ tracedOnChunk,
12867
+ tracedExecuteAction,
12868
+ tracedOnEnd,
12648
12869
  history
12649
12870
  );
12650
12871
  return;
@@ -12674,6 +12895,27 @@ Instructions:
12674
12895
  history
12675
12896
  );
12676
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
+ }
12677
12919
  const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
12678
12920
  const DEFAULT_NOTE_FOLDER = "Vessel/Research";
12679
12921
  const DEFAULT_BOOKMARK_FOLDER = "Vessel/Bookmarks";
@@ -15175,10 +15417,15 @@ ${buildScopedContext(pageContent, mode)}`;
15175
15417
  "vessel_navigate",
15176
15418
  {
15177
15419
  title: "Navigate",
15178
- description: "Navigate the active browser tab to a URL.",
15179
- inputSchema: { url: zod.z.string().describe("The URL to navigate to") }
15420
+ description: "Navigate the active browser tab to a URL. Use postBody to submit data via POST request (e.g. form submissions).",
15421
+ inputSchema: {
15422
+ url: zod.z.string().describe("The URL to navigate to"),
15423
+ postBody: zod.z.record(zod.z.string(), zod.z.string()).optional().describe(
15424
+ "Optional form fields to submit via POST (application/x-www-form-urlencoded). Only supported on http/https URLs."
15425
+ )
15426
+ }
15180
15427
  },
15181
- async ({ url }) => {
15428
+ async ({ url, postBody }) => {
15182
15429
  const tab = tabManager.getActiveTab();
15183
15430
  if (!tab) return asTextResponse("Error: No active tab");
15184
15431
  const preCheck = await validateLinkDestination(url);
@@ -15187,9 +15434,17 @@ ${buildScopedContext(pageContent, mode)}`;
15187
15434
  `Navigation blocked: ${url} returned ${preCheck.detail || "dead link"}. Try a different URL or go back and choose another link.`
15188
15435
  );
15189
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
+ }
15190
15444
  return withAction(runtime2, tabManager, "navigate", { url }, async () => {
15191
15445
  const id = tabManager.getActiveTabId();
15192
- tabManager.navigateTab(id, url);
15446
+ const navError = tabManager.navigateTab(id, url, postBody);
15447
+ if (navError) return navError;
15193
15448
  const { httpStatus } = await waitForLoadWithStatus(
15194
15449
  tab.view.webContents
15195
15450
  );
@@ -15866,27 +16121,6 @@ ${buildScopedContext(pageContent, mode)}`;
15866
16121
  }
15867
16122
  )
15868
16123
  );
15869
- server.registerTool(
15870
- "vessel_create_checkpoint",
15871
- {
15872
- title: "Create Checkpoint",
15873
- description: "Alias for vessel_checkpoint_create. Capture the current session as a checkpoint.",
15874
- inputSchema: {
15875
- name: zod.z.string().optional().describe("Optional checkpoint name"),
15876
- note: zod.z.string().optional().describe("Optional note")
15877
- }
15878
- },
15879
- async ({ name, note }) => withAction(
15880
- runtime2,
15881
- tabManager,
15882
- "create_checkpoint",
15883
- { name, note },
15884
- async () => {
15885
- const checkpoint = runtime2.createCheckpoint(name, note);
15886
- return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
15887
- }
15888
- )
15889
- );
15890
16124
  server.registerTool(
15891
16125
  "vessel_checkpoint_restore",
15892
16126
  {
@@ -15913,32 +16147,6 @@ ${buildScopedContext(pageContent, mode)}`;
15913
16147
  }
15914
16148
  )
15915
16149
  );
15916
- server.registerTool(
15917
- "vessel_restore_checkpoint",
15918
- {
15919
- title: "Restore Checkpoint",
15920
- description: "Alias for vessel_checkpoint_restore. Restore a saved checkpoint by ID or exact name.",
15921
- inputSchema: {
15922
- checkpointId: zod.z.string().optional().describe("Checkpoint ID"),
15923
- name: zod.z.string().optional().describe("Exact checkpoint name")
15924
- }
15925
- },
15926
- async ({ checkpointId, name }) => withAction(
15927
- runtime2,
15928
- tabManager,
15929
- "restore_checkpoint",
15930
- { checkpointId, name },
15931
- async () => {
15932
- const state2 = runtime2.getState();
15933
- const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
15934
- if (!checkpoint) {
15935
- return "Error: No matching checkpoint found";
15936
- }
15937
- runtime2.restoreCheckpoint(checkpoint.id);
15938
- return `Restored checkpoint ${checkpoint.name}`;
15939
- }
15940
- )
15941
- );
15942
16150
  server.registerTool(
15943
16151
  "vessel_save_session",
15944
16152
  {
@@ -18071,7 +18279,7 @@ function startMcpServer(tabManager, runtime2, port) {
18071
18279
  status: "starting",
18072
18280
  message: `Starting MCP server on port ${port}.`
18073
18281
  });
18074
- mcpAuthToken = crypto$1.randomBytes(32).toString("hex");
18282
+ mcpAuthToken = crypto$2.randomBytes(32).toString("hex");
18075
18283
  return new Promise((resolve) => {
18076
18284
  const server = http.createServer(async (req, res) => {
18077
18285
  const url = new URL(req.url || "/", `http://localhost:${port}`);
@@ -18162,8 +18370,7 @@ function startMcpServer(tabManager, runtime2, port) {
18162
18370
  status: "ready",
18163
18371
  message: `MCP server listening on ${endpoint}.`
18164
18372
  });
18165
- console.log(`[Vessel MCP] Server listening on ${endpoint}`);
18166
- console.log(`[Vessel MCP] Auth token: ${mcpAuthToken}`);
18373
+ console.log(`[Vessel MCP] Server listening on ${endpoint} (auth enabled)`);
18167
18374
  finish({
18168
18375
  ok: true,
18169
18376
  configuredPort: port,
@@ -18201,6 +18408,372 @@ function stopMcpServer() {
18201
18408
  });
18202
18409
  });
18203
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
+ }
18204
18777
  let activeChatProvider = null;
18205
18778
  function assertString(value, name) {
18206
18779
  if (typeof value !== "string") throw new Error(`${name} must be a string`);
@@ -18219,9 +18792,30 @@ function registerIpcHandlers(windowState, runtime2) {
18219
18792
  sidebarView.webContents.send(channel, ...args);
18220
18793
  devtoolsPanelView.webContents.send(channel, ...args);
18221
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
+ };
18222
18810
  runtime2.setUpdateListener((state2) => {
18223
18811
  sendToRendererViews(Channels.AGENT_RUNTIME_UPDATE, state2);
18224
18812
  });
18813
+ onRuntimeHealthChange((health) => {
18814
+ sendToRendererViews(Channels.SETTINGS_HEALTH_UPDATE, health);
18815
+ });
18816
+ onAIStreamIdle(() => {
18817
+ sendToRendererViews(Channels.AI_STREAM_IDLE);
18818
+ });
18225
18819
  electron.ipcMain.handle(Channels.TAB_CREATE, (_, url) => {
18226
18820
  const id = tabManager.createTab(url || loadSettings().defaultUrl);
18227
18821
  layoutViews(windowState);
@@ -18235,11 +18829,14 @@ function registerIpcHandlers(windowState, runtime2) {
18235
18829
  tabManager.switchTab(id);
18236
18830
  layoutViews(windowState);
18237
18831
  });
18238
- electron.ipcMain.handle(Channels.TAB_NAVIGATE, (_, id, url) => {
18239
- assertString(id, "tabId");
18240
- assertString(url, "url");
18241
- tabManager.navigateTab(id, url);
18242
- });
18832
+ electron.ipcMain.handle(
18833
+ Channels.TAB_NAVIGATE,
18834
+ (_, id, url, postBody) => {
18835
+ assertString(id, "tabId");
18836
+ assertString(url, "url");
18837
+ return tabManager.navigateTab(id, url, postBody);
18838
+ }
18839
+ );
18243
18840
  electron.ipcMain.handle(Channels.TAB_BACK, (_, id) => {
18244
18841
  tabManager.goBack(id);
18245
18842
  });
@@ -18252,15 +18849,19 @@ function registerIpcHandlers(windowState, runtime2) {
18252
18849
  electron.ipcMain.handle(Channels.AI_QUERY, async (_, query, history) => {
18253
18850
  const settings2 = loadSettings();
18254
18851
  const chatConfig = settings2.chatProvider;
18255
- sendToRendererViews(Channels.AI_STREAM_START, query);
18256
18852
  if (!chatConfig) {
18853
+ sendToRendererViews(Channels.AI_STREAM_START, query);
18257
18854
  sendToRendererViews(
18258
18855
  Channels.AI_STREAM_CHUNK,
18259
18856
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
18260
18857
  );
18261
18858
  sendToRendererViews(Channels.AI_STREAM_END);
18262
- return;
18859
+ return { accepted: true };
18860
+ }
18861
+ if (!tryBeginAIStream("manual")) {
18862
+ return { accepted: false, reason: "busy" };
18263
18863
  }
18864
+ sendToRendererViews(Channels.AI_STREAM_START, query);
18264
18865
  try {
18265
18866
  activeChatProvider = createProvider(chatConfig);
18266
18867
  trackProviderConfigured(chatConfig.id);
@@ -18282,7 +18883,9 @@ function registerIpcHandlers(windowState, runtime2) {
18282
18883
  sendToRendererViews(Channels.AI_STREAM_END);
18283
18884
  } finally {
18284
18885
  activeChatProvider = null;
18886
+ endAIStream("manual");
18285
18887
  }
18888
+ return { accepted: true };
18286
18889
  });
18287
18890
  electron.ipcMain.handle(Channels.AI_CANCEL, () => {
18288
18891
  activeChatProvider?.cancel();
@@ -18453,6 +19056,7 @@ function registerIpcHandlers(windowState, runtime2) {
18453
19056
  if (result.success && result.text) {
18454
19057
  await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(() => {
18455
19058
  });
19059
+ await emitHighlightCount();
18456
19060
  }
18457
19061
  return result;
18458
19062
  } catch {
@@ -18460,6 +19064,9 @@ function registerIpcHandlers(windowState, runtime2) {
18460
19064
  }
18461
19065
  });
18462
19066
  tabManager.onHighlightCapture((result) => {
19067
+ if (result.success) {
19068
+ void emitHighlightCount();
19069
+ }
18463
19070
  if (!chromeView.webContents.isDestroyed()) {
18464
19071
  chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, result);
18465
19072
  }
@@ -18472,6 +19079,7 @@ function registerIpcHandlers(windowState, runtime2) {
18472
19079
  if (!tab || !tab.highlightModeActive) return;
18473
19080
  void persistAndMarkHighlight(wc, text).then((result) => {
18474
19081
  if (result.success && !chromeView.webContents.isDestroyed()) {
19082
+ void emitHighlightCount();
18475
19083
  chromeView.webContents.send(Channels.HIGHLIGHT_CAPTURE_RESULT, result);
18476
19084
  }
18477
19085
  });
@@ -18479,15 +19087,7 @@ function registerIpcHandlers(windowState, runtime2) {
18479
19087
  }
18480
19088
  });
18481
19089
  electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_COUNT, () => {
18482
- const tab = tabManager.getActiveTab();
18483
- if (!tab) return 0;
18484
- const wc = tab.view.webContents;
18485
- if (wc.isDestroyed()) return 0;
18486
- try {
18487
- return getHighlightCount(wc);
18488
- } catch {
18489
- return 0;
18490
- }
19090
+ return getActiveHighlightCountSafe();
18491
19091
  });
18492
19092
  electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_SCROLL, (_, index) => {
18493
19093
  const tab = tabManager.getActiveTab();
@@ -18500,24 +19100,32 @@ function registerIpcHandlers(windowState, runtime2) {
18500
19100
  return false;
18501
19101
  }
18502
19102
  });
18503
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, (_, index) => {
19103
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, async (_, index) => {
18504
19104
  const tab = tabManager.getActiveTab();
18505
19105
  if (!tab) return false;
18506
19106
  const wc = tab.view.webContents;
18507
19107
  if (wc.isDestroyed()) return false;
18508
19108
  try {
18509
- return removeHighlightAtIndex(wc, index);
19109
+ const removed = await removeHighlightAtIndex(wc, index);
19110
+ if (removed) {
19111
+ await emitHighlightCount();
19112
+ }
19113
+ return removed;
18510
19114
  } catch {
18511
19115
  return false;
18512
19116
  }
18513
19117
  });
18514
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, () => {
19118
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, async () => {
18515
19119
  const tab = tabManager.getActiveTab();
18516
19120
  if (!tab) return false;
18517
19121
  const wc = tab.view.webContents;
18518
19122
  if (wc.isDestroyed()) return false;
18519
19123
  try {
18520
- return clearAllHighlightElements(wc);
19124
+ const cleared = await clearAllHighlightElements(wc);
19125
+ if (cleared) {
19126
+ await emitHighlightCount();
19127
+ }
19128
+ return cleared;
18521
19129
  } catch {
18522
19130
  return false;
18523
19131
  }
@@ -18671,6 +19279,17 @@ function registerIpcHandlers(windowState, runtime2) {
18671
19279
  electron.ipcMain.handle(Channels.WINDOW_CLOSE, () => {
18672
19280
  mainWindow.close();
18673
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);
18674
19293
  }
18675
19294
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
18676
19295
  const PERSIST_DEBOUNCE_MS = 500;
@@ -18751,7 +19370,7 @@ class AgentRuntime {
18751
19370
  createCheckpoint(name, note) {
18752
19371
  const snapshot = this.captureSession(note);
18753
19372
  const checkpoint = {
18754
- id: crypto$1.randomUUID(),
19373
+ id: crypto$2.randomUUID(),
18755
19374
  name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
18756
19375
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
18757
19376
  note: note?.trim() || void 0,
@@ -18805,7 +19424,7 @@ class AgentRuntime {
18805
19424
  }
18806
19425
  }
18807
19426
  const entry = {
18808
- id: crypto$1.randomUUID(),
19427
+ id: crypto$2.randomUUID(),
18809
19428
  source: input.source,
18810
19429
  kind,
18811
19430
  title: input.title?.trim() || void 0,
@@ -18829,7 +19448,7 @@ class AgentRuntime {
18829
19448
  // --- Speedee Flow State ---
18830
19449
  startFlow(goal, steps, startUrl) {
18831
19450
  const flow = {
18832
- id: crypto$1.randomUUID(),
19451
+ id: crypto$2.randomUUID(),
18833
19452
  goal,
18834
19453
  steps: steps.map((label) => ({ label, status: "pending" })),
18835
19454
  currentStepIndex: 0,
@@ -19058,7 +19677,7 @@ ${progress}
19058
19677
  }
19059
19678
  startAction(input) {
19060
19679
  const action = {
19061
- id: crypto$1.randomUUID(),
19680
+ id: crypto$2.randomUUID(),
19062
19681
  source: input.source,
19063
19682
  name: input.name,
19064
19683
  args: clone(input.args),
@@ -19130,7 +19749,7 @@ ${progress}
19130
19749
  }
19131
19750
  awaitApproval(action, reason) {
19132
19751
  const approval = {
19133
- id: crypto$1.randomUUID(),
19752
+ id: crypto$2.randomUUID(),
19134
19753
  actionId: action.id,
19135
19754
  source: action.source,
19136
19755
  name: action.name,
@@ -19256,12 +19875,25 @@ function installAdBlocking(tabManager) {
19256
19875
  callback({ cancel: shouldBlockRequest(details) });
19257
19876
  });
19258
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
+ }
19259
19891
  function installDownloadHandler(chromeView) {
19260
19892
  electron.session.defaultSession.on("will-download", (_event, item) => {
19261
19893
  const settings2 = loadSettings();
19262
19894
  const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
19263
19895
  const filename = item.getFilename();
19264
- const savePath = path.join(downloadDir, filename);
19896
+ const savePath = resolveDownloadPath(downloadDir, filename);
19265
19897
  item.setSavePath(savePath);
19266
19898
  const info = {
19267
19899
  filename,
@@ -19290,13 +19922,245 @@ function installDownloadHandler(chromeView) {
19290
19922
  });
19291
19923
  });
19292
19924
  }
19293
- let runtime = null;
19925
+ function registerHighlightShortcut(mainWindow, tabManager) {
19926
+ const register = () => {
19927
+ electron.globalShortcut.unregister("CommandOrControl+H");
19928
+ const success = electron.globalShortcut.register("CommandOrControl+H", () => {
19929
+ const activeTab = tabManager.getActiveTab();
19930
+ if (!activeTab) return;
19931
+ tabManager.captureHighlightFromActiveTab();
19932
+ });
19933
+ if (!success) {
19934
+ console.warn("[Vessel] Failed to register Ctrl+H shortcut");
19935
+ }
19936
+ };
19937
+ register();
19938
+ mainWindow.on("focus", register);
19939
+ return () => {
19940
+ electron.globalShortcut.unregister("CommandOrControl+H");
19941
+ mainWindow.removeListener("focus", register);
19942
+ };
19943
+ }
19944
+ function setupAppMenu() {
19945
+ const appMenu = electron.Menu.buildFromTemplate([
19946
+ {
19947
+ label: "Edit",
19948
+ submenu: [
19949
+ { role: "undo" },
19950
+ { role: "redo" },
19951
+ { type: "separator" },
19952
+ { role: "cut" },
19953
+ { role: "copy" },
19954
+ { role: "paste" },
19955
+ { role: "selectAll" }
19956
+ ]
19957
+ }
19958
+ ]);
19959
+ electron.Menu.setApplicationMenu(appMenu);
19960
+ }
19294
19961
  function rendererUrlFor(view) {
19295
19962
  if (!process.env.ELECTRON_RENDERER_URL) return null;
19296
19963
  const url = new URL(process.env.ELECTRON_RENDERER_URL);
19297
19964
  url.searchParams.set("view", view);
19298
19965
  return url.toString();
19299
19966
  }
19967
+ function loadRenderers(chromeView, sidebarView, devtoolsPanelView) {
19968
+ const chromeUrl = rendererUrlFor("chrome");
19969
+ const sidebarUrl = rendererUrlFor("sidebar");
19970
+ const devtoolsUrl = rendererUrlFor("devtools");
19971
+ if (chromeUrl && sidebarUrl && devtoolsUrl) {
19972
+ chromeView.webContents.loadURL(chromeUrl);
19973
+ sidebarView.webContents.loadURL(sidebarUrl);
19974
+ devtoolsPanelView.webContents.loadURL(devtoolsUrl);
19975
+ } else {
19976
+ const rendererFile = path$1.join(__dirname, "../../renderer/index.html");
19977
+ chromeView.webContents.loadFile(rendererFile, {
19978
+ query: { view: "chrome" }
19979
+ });
19980
+ sidebarView.webContents.loadFile(rendererFile, {
19981
+ query: { view: "sidebar" }
19982
+ });
19983
+ devtoolsPanelView.webContents.loadFile(rendererFile, {
19984
+ query: { view: "devtools" }
19985
+ });
19986
+ }
19987
+ }
19988
+ function findIconBase64() {
19989
+ const candidates = [
19990
+ path$1.join(electron.app.getAppPath(), "resources", "vessel-icon-400x400.png"),
19991
+ path$1.join(process.resourcesPath, "vessel-icon-400x400.png"),
19992
+ path$1.join(__dirname, "../../resources/vessel-icon-400x400.png"),
19993
+ path$1.join(electron.app.getAppPath(), "resources", "vessel-icon.png"),
19994
+ path$1.join(process.resourcesPath, "vessel-icon.png"),
19995
+ path$1.join(__dirname, "../../resources/vessel-icon.png")
19996
+ ];
19997
+ for (const p of candidates) {
19998
+ try {
19999
+ const data = fs$1.readFileSync(p);
20000
+ return `data:image/png;base64,${data.toString("base64")}`;
20001
+ } catch {
20002
+ }
20003
+ }
20004
+ return "";
20005
+ }
20006
+ function buildSplashHTML(iconSrc) {
20007
+ const imgTag = iconSrc ? `<img class="logo" src="${iconSrc}" alt="" />` : `<div class="logo-fallback">V</div>`;
20008
+ return `<!DOCTYPE html>
20009
+ <html>
20010
+ <head>
20011
+ <meta charset="UTF-8">
20012
+ <style>
20013
+ * { margin: 0; padding: 0; box-sizing: border-box; }
20014
+ html, body {
20015
+ width: 100%; height: 100%;
20016
+ background: #1a1a1e;
20017
+ display: flex;
20018
+ flex-direction: column;
20019
+ align-items: center;
20020
+ justify-content: center;
20021
+ gap: 20px;
20022
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
20023
+ overflow: hidden;
20024
+ -webkit-app-region: drag;
20025
+ user-select: none;
20026
+ }
20027
+ .logo-wrap {
20028
+ position: relative;
20029
+ display: flex;
20030
+ align-items: center;
20031
+ justify-content: center;
20032
+ animation: pop-in 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
20033
+ }
20034
+ .glow {
20035
+ position: absolute;
20036
+ inset: -16px;
20037
+ border-radius: 36px;
20038
+ background: radial-gradient(ellipse at center,
20039
+ rgba(196, 160, 90, 0.22) 0%,
20040
+ transparent 68%
20041
+ );
20042
+ animation: glow-pulse 2.8s ease-in-out infinite;
20043
+ }
20044
+ .logo {
20045
+ width: 84px;
20046
+ height: 84px;
20047
+ border-radius: 20px;
20048
+ display: block;
20049
+ position: relative;
20050
+ }
20051
+ .logo-fallback {
20052
+ width: 84px;
20053
+ height: 84px;
20054
+ border-radius: 20px;
20055
+ background: linear-gradient(135deg, #2a2a30, #1e1e24);
20056
+ border: 1px solid rgba(196, 160, 90, 0.25);
20057
+ display: flex;
20058
+ align-items: center;
20059
+ justify-content: center;
20060
+ font-size: 36px;
20061
+ font-weight: 700;
20062
+ color: #c4a05a;
20063
+ position: relative;
20064
+ }
20065
+ .name {
20066
+ font-size: 13px;
20067
+ font-weight: 600;
20068
+ letter-spacing: 0.22em;
20069
+ color: #7a7a8a;
20070
+ text-transform: uppercase;
20071
+ animation: fade-up 0.5s 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
20072
+ }
20073
+ .dots {
20074
+ display: flex;
20075
+ gap: 6px;
20076
+ animation: fade-up 0.4s 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;
20077
+ }
20078
+ .dot {
20079
+ width: 5px;
20080
+ height: 5px;
20081
+ border-radius: 50%;
20082
+ background: #3e3e50;
20083
+ animation: dot-bounce 1.5s ease-in-out infinite;
20084
+ }
20085
+ .dot:nth-child(2) { animation-delay: 0.2s; }
20086
+ .dot:nth-child(3) { animation-delay: 0.4s; }
20087
+
20088
+ @keyframes pop-in {
20089
+ from { opacity: 0; transform: scale(0.78); }
20090
+ to { opacity: 1; transform: scale(1); }
20091
+ }
20092
+ @keyframes fade-up {
20093
+ from { opacity: 0; transform: translateY(7px); }
20094
+ to { opacity: 1; transform: translateY(0); }
20095
+ }
20096
+ @keyframes glow-pulse {
20097
+ 0%, 100% { opacity: 0.55; transform: scale(1); }
20098
+ 50% { opacity: 1; transform: scale(1.1); }
20099
+ }
20100
+ @keyframes dot-bounce {
20101
+ 0%, 75%, 100% { transform: translateY(0); opacity: 0.3; }
20102
+ 40% { transform: translateY(-6px); opacity: 1; }
20103
+ }
20104
+ </style>
20105
+ </head>
20106
+ <body>
20107
+ <div class="logo-wrap">
20108
+ <div class="glow"></div>
20109
+ ${imgTag}
20110
+ </div>
20111
+ <div class="name">Vessel</div>
20112
+ <div class="dots">
20113
+ <div class="dot"></div>
20114
+ <div class="dot"></div>
20115
+ <div class="dot"></div>
20116
+ </div>
20117
+ </body>
20118
+ </html>`;
20119
+ }
20120
+ function createSplashWindow() {
20121
+ const splash = new electron.BrowserWindow({
20122
+ width: 1280,
20123
+ height: 800,
20124
+ center: true,
20125
+ frame: false,
20126
+ show: false,
20127
+ // only show once content has painted — prevents black-window flash
20128
+ resizable: false,
20129
+ movable: true,
20130
+ alwaysOnTop: true,
20131
+ skipTaskbar: true,
20132
+ backgroundColor: "#1a1a1e",
20133
+ webPreferences: {
20134
+ nodeIntegration: false,
20135
+ contextIsolation: true
20136
+ }
20137
+ });
20138
+ splash.once("ready-to-show", () => splash.show());
20139
+ const iconSrc = findIconBase64();
20140
+ const html = buildSplashHTML(iconSrc);
20141
+ try {
20142
+ const tmpDir = fs$1.mkdtempSync(path$1.join(os.tmpdir(), "vessel-splash-"));
20143
+ const tmpPath = path$1.join(tmpDir, "index.html");
20144
+ splash.once("closed", () => {
20145
+ try {
20146
+ fs$1.rmSync(tmpDir, { recursive: true, force: true });
20147
+ } catch {
20148
+ }
20149
+ });
20150
+ fs$1.writeFileSync(tmpPath, html, "utf-8");
20151
+ void splash.loadFile(tmpPath);
20152
+ } catch (err) {
20153
+ console.warn("[splash] Failed to write temp HTML, using fallback:", err);
20154
+ void splash.loadFile(path$1.join(__dirname, "../../resources/vessel-icon.png"));
20155
+ }
20156
+ return splash;
20157
+ }
20158
+ function closeSplash(splash, delayMs = 0) {
20159
+ setTimeout(() => {
20160
+ if (!splash.isDestroyed()) splash.close();
20161
+ }, delayMs);
20162
+ }
20163
+ let runtime = null;
19300
20164
  function checkWritableUserData(userDataPath) {
19301
20165
  const issues = [];
19302
20166
  try {
@@ -19360,6 +20224,7 @@ Action: Open Settings (Ctrl+,) to choose a different port, then save to restart
19360
20224
  });
19361
20225
  }
19362
20226
  async function bootstrap() {
20227
+ const splash = createSplashWindow();
19363
20228
  const settings2 = loadSettings();
19364
20229
  const userDataPath = electron.app.getPath("userData");
19365
20230
  initializeRuntimeHealth({
@@ -19371,15 +20236,51 @@ async function bootstrap() {
19371
20236
  if (settings2.clearBookmarksOnLaunch) {
19372
20237
  clearAll();
19373
20238
  }
20239
+ const syncActiveHighlightCount = async (state2) => {
20240
+ const activeTab = state2.tabManager.getActiveTab();
20241
+ const wc = activeTab?.view.webContents;
20242
+ let count = 0;
20243
+ if (wc && !wc.isDestroyed()) {
20244
+ try {
20245
+ count = await getHighlightCount(wc) ?? 0;
20246
+ } catch {
20247
+ count = 0;
20248
+ }
20249
+ }
20250
+ if (!state2.chromeView.webContents.isDestroyed()) {
20251
+ state2.chromeView.webContents.send(Channels.HIGHLIGHT_COUNT_UPDATE, count);
20252
+ }
20253
+ if (!state2.sidebarView.webContents.isDestroyed()) {
20254
+ state2.sidebarView.webContents.send(Channels.HIGHLIGHT_COUNT_UPDATE, count);
20255
+ }
20256
+ if (!state2.devtoolsPanelView.webContents.isDestroyed()) {
20257
+ state2.devtoolsPanelView.webContents.send(
20258
+ Channels.HIGHLIGHT_COUNT_UPDATE,
20259
+ count
20260
+ );
20261
+ }
20262
+ };
19374
20263
  const windowState = createMainWindow((tabs, activeId) => {
19375
20264
  windowState.chromeView.webContents.send(
19376
20265
  Channels.TAB_STATE_UPDATE,
19377
20266
  tabs,
19378
20267
  activeId
19379
20268
  );
20269
+ void syncActiveHighlightCount(windowState);
19380
20270
  layoutViews(windowState);
19381
20271
  runtime?.onTabStateChanged();
19382
20272
  });
20273
+ let didRevealMainWindow = false;
20274
+ const revealMainWindow = () => {
20275
+ if (didRevealMainWindow) return;
20276
+ didRevealMainWindow = true;
20277
+ windowState.mainWindow.show();
20278
+ closeSplash(splash, 0);
20279
+ };
20280
+ const splashTimeout = setTimeout(() => {
20281
+ console.warn("[bootstrap] Renderer did not finish loading before splash timeout");
20282
+ revealMainWindow();
20283
+ }, 8e3);
19383
20284
  const { chromeView, sidebarView, devtoolsPanelView, tabManager } = windowState;
19384
20285
  runtime = new AgentRuntime(tabManager);
19385
20286
  installAdBlocking(tabManager);
@@ -19389,34 +20290,8 @@ async function bootstrap() {
19389
20290
  }
19390
20291
  });
19391
20292
  registerIpcHandlers(windowState, runtime);
19392
- const registerHighlightShortcut = () => {
19393
- electron.globalShortcut.unregister("CommandOrControl+H");
19394
- const success = electron.globalShortcut.register("CommandOrControl+H", () => {
19395
- const activeTab = tabManager.getActiveTab();
19396
- if (!activeTab) return;
19397
- tabManager.captureHighlightFromActiveTab();
19398
- });
19399
- if (!success) {
19400
- console.warn("[Vessel] Failed to register Ctrl+H shortcut");
19401
- }
19402
- };
19403
- registerHighlightShortcut();
19404
- windowState.mainWindow.on("focus", registerHighlightShortcut);
19405
- const appMenu = electron.Menu.buildFromTemplate([
19406
- {
19407
- label: "Edit",
19408
- submenu: [
19409
- { role: "undo" },
19410
- { role: "redo" },
19411
- { type: "separator" },
19412
- { role: "cut" },
19413
- { role: "copy" },
19414
- { role: "paste" },
19415
- { role: "selectAll" }
19416
- ]
19417
- }
19418
- ]);
19419
- electron.Menu.setApplicationMenu(appMenu);
20293
+ registerHighlightShortcut(windowState.mainWindow, tabManager);
20294
+ setupAppMenu();
19420
20295
  subscribe((state2) => {
19421
20296
  chromeView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
19422
20297
  sidebarView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
@@ -19428,26 +20303,7 @@ async function bootstrap() {
19428
20303
  installDownloadHandler(chromeView);
19429
20304
  startBackgroundRevalidation();
19430
20305
  startTelemetry();
19431
- const chromeUrl = rendererUrlFor("chrome");
19432
- const sidebarUrl = rendererUrlFor("sidebar");
19433
- const devtoolsUrl = rendererUrlFor("devtools");
19434
- if (chromeUrl && sidebarUrl && devtoolsUrl) {
19435
- chromeView.webContents.loadURL(chromeUrl);
19436
- sidebarView.webContents.loadURL(sidebarUrl);
19437
- devtoolsPanelView.webContents.loadURL(devtoolsUrl);
19438
- } else {
19439
- const rendererFile = path.join(__dirname, "../renderer/index.html");
19440
- chromeView.webContents.loadFile(rendererFile, {
19441
- query: { view: "chrome" }
19442
- });
19443
- sidebarView.webContents.loadFile(rendererFile, {
19444
- query: { view: "sidebar" }
19445
- });
19446
- devtoolsPanelView.webContents.loadFile(rendererFile, {
19447
- query: { view: "devtools" }
19448
- });
19449
- }
19450
- await startMcpServer(tabManager, runtime, settings2.mcpPort);
20306
+ loadRenderers(chromeView, sidebarView, devtoolsPanelView);
19451
20307
  chromeView.webContents.once("did-finish-load", () => {
19452
20308
  const savedSession = runtime.getState().session;
19453
20309
  if (settings2.autoRestoreSession && savedSession?.tabs.length) {
@@ -19458,9 +20314,38 @@ async function bootstrap() {
19458
20314
  }
19459
20315
  layoutViews(windowState);
19460
20316
  setImmediate(() => layoutViews(windowState));
20317
+ clearTimeout(splashTimeout);
20318
+ revealMainWindow();
19461
20319
  void maybeShowStartupHealthDialog(windowState);
19462
20320
  });
20321
+ chromeView.webContents.once(
20322
+ "did-fail-load",
20323
+ (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
20324
+ if (!isMainFrame) return;
20325
+ console.error(
20326
+ "[bootstrap] Chrome renderer failed to load:",
20327
+ errorCode,
20328
+ errorDescription,
20329
+ validatedURL
20330
+ );
20331
+ clearTimeout(splashTimeout);
20332
+ revealMainWindow();
20333
+ }
20334
+ );
20335
+ startMcpServer(tabManager, runtime, settings2.mcpPort).catch((err) => {
20336
+ console.error("[bootstrap] MCP server failed to start:", err);
20337
+ });
19463
20338
  }
20339
+ process.on("uncaughtException", (error) => {
20340
+ console.error("[Vessel] Uncaught exception:", error.message, error.stack);
20341
+ electron.app.quit();
20342
+ });
20343
+ process.on("unhandledRejection", (reason) => {
20344
+ console.error(
20345
+ "[Vessel] Unhandled rejection:",
20346
+ reason instanceof Error ? reason.message : reason
20347
+ );
20348
+ });
19464
20349
  electron.app.whenReady().then(bootstrap).catch((error) => {
19465
20350
  console.error("[Vessel] Failed to bootstrap application:", error);
19466
20351
  electron.app.quit();
@@ -19469,8 +20354,15 @@ electron.app.on("window-all-closed", () => {
19469
20354
  electron.globalShortcut.unregisterAll();
19470
20355
  stopTelemetry();
19471
20356
  stopBackgroundRevalidation();
19472
- runtime?.flushPersist();
19473
- void stopMcpServer().finally(() => {
19474
- electron.app.quit();
20357
+ void Promise.all([
20358
+ runtime?.flushPersist() ?? Promise.resolve(),
20359
+ flushPersist(),
20360
+ flushPersist$1(),
20361
+ flushPersist$2(),
20362
+ flushPersist$3()
20363
+ ]).finally(() => {
20364
+ void stopMcpServer().finally(() => {
20365
+ electron.app.quit();
20366
+ });
19475
20367
  });
19476
20368
  });