@quanta-intellect/vessel-browser 0.1.69 → 0.1.73

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
@@ -77,7 +77,7 @@ const defaults = {
77
77
  };
78
78
  const SAVE_DEBOUNCE_MS$6 = 150;
79
79
  const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
80
- const logger$j = createLogger("Settings");
80
+ const logger$k = createLogger("Settings");
81
81
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
82
82
  let settings = null;
83
83
  let settingsIssues = [];
@@ -223,7 +223,7 @@ function persistNow() {
223
223
  getSettingsPath(),
224
224
  JSON.stringify(buildPersistedSettings(settings), null, 2)
225
225
  )
226
- ).catch((err) => logger$j.error("Failed to save settings:", err));
226
+ ).catch((err) => logger$k.error("Failed to save settings:", err));
227
227
  }
228
228
  function saveSettings() {
229
229
  saveDirty = true;
@@ -327,7 +327,7 @@ function assertPermittedNavigationURL(url) {
327
327
  }
328
328
  const MAX_CUSTOM_HISTORY = 50;
329
329
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
330
- const logger$i = createLogger("Tab");
330
+ const logger$j = createLogger("Tab");
331
331
  class Tab {
332
332
  id;
333
333
  view;
@@ -339,6 +339,7 @@ class Tab {
339
339
  onHighlightSelection;
340
340
  onHighlightRemove;
341
341
  onHighlightRecolor;
342
+ onSavePage;
342
343
  _highlightModeActive = false;
343
344
  _readerOriginalUrl = null;
344
345
  // Fully custom URL history — we never rely on Chromium's native back/forward
@@ -368,7 +369,7 @@ class Tab {
368
369
  guardedLoadURL(url, options) {
369
370
  const blockReason = this.getNavigationBlockReason(url);
370
371
  if (blockReason) {
371
- logger$i.warn(blockReason);
372
+ logger$j.warn(blockReason);
372
373
  return blockReason;
373
374
  }
374
375
  void this.view.webContents.loadURL(url, options);
@@ -383,14 +384,18 @@ class Tab {
383
384
  this.onHighlightSelection = options?.onHighlightSelection;
384
385
  this.onHighlightRemove = options?.onHighlightRemove;
385
386
  this.onHighlightRecolor = options?.onHighlightRecolor;
386
- this.view = new electron.WebContentsView({
387
- webPreferences: {
388
- preload: path.join(__dirname, "../preload/content-script.js"),
389
- sandbox: true,
390
- contextIsolation: true,
391
- nodeIntegration: false
392
- }
393
- });
387
+ this.onSavePage = options?.onSavePage;
388
+ const webPreferences = {
389
+ preload: path.join(__dirname, "../preload/content-script.js"),
390
+ sandbox: true,
391
+ contextIsolation: true,
392
+ nodeIntegration: false,
393
+ spellcheck: false
394
+ };
395
+ if (options?.sessionPartition) {
396
+ webPreferences.session = electron.session.fromPartition(options.sessionPartition);
397
+ }
398
+ this.view = new electron.WebContentsView({ webPreferences });
394
399
  const initialUrl = url || "about:blank";
395
400
  this._state = {
396
401
  id,
@@ -402,13 +407,31 @@ class Tab {
402
407
  canGoForward: false,
403
408
  isReaderMode: false,
404
409
  adBlockingEnabled: options?.adBlockingEnabled ?? true,
410
+ isPinned: false,
411
+ isAudible: false,
412
+ isMuted: false,
405
413
  role: options?.role
406
414
  };
407
- this.view.webContents.on("before-input-event", (_event, input) => {
415
+ this.view.webContents.on("before-input-event", (event, input) => {
408
416
  if (!input.control && !input.meta) return;
409
417
  if (input.type !== "keyDown") return;
410
418
  const key = input.key.toLowerCase();
411
419
  const wc = this.view.webContents;
420
+ if (key === "+" || key === "=") {
421
+ this.zoomIn();
422
+ event.preventDefault();
423
+ return;
424
+ }
425
+ if (key === "-") {
426
+ this.zoomOut();
427
+ event.preventDefault();
428
+ return;
429
+ }
430
+ if (key === "0") {
431
+ this.zoomReset();
432
+ event.preventDefault();
433
+ return;
434
+ }
412
435
  if (key === "c") wc.copy();
413
436
  else if (key === "v") wc.paste();
414
437
  else if (key === "x") wc.cut();
@@ -429,7 +452,7 @@ class Tab {
429
452
  wc.setWindowOpenHandler(({ url, disposition }) => {
430
453
  const error = this.getNavigationBlockReason(url);
431
454
  if (error) {
432
- logger$i.warn(error);
455
+ logger$j.warn(error);
433
456
  return { action: "deny" };
434
457
  }
435
458
  this.onOpenUrl?.({
@@ -443,7 +466,7 @@ class Tab {
443
466
  const error = this.getNavigationBlockReason(url);
444
467
  if (!error) return;
445
468
  event.preventDefault();
446
- logger$i.warn(`${context}: ${error}`);
469
+ logger$j.warn(`${context}: ${error}`);
447
470
  };
448
471
  wc.on("will-navigate", (event, url) => {
449
472
  blockNavigation(event, url, "Blocked top-level navigation");
@@ -507,12 +530,25 @@ class Tab {
507
530
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 999px; }
508
531
  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
509
532
  ::-webkit-scrollbar-corner { background: transparent; }
510
- `).catch((err) => logger$i.warn("Failed to inject scrollbar CSS:", err));
533
+ `).catch((err) => logger$j.warn("Failed to inject scrollbar CSS:", err));
511
534
  });
512
535
  wc.on("page-favicon-updated", (_, favicons) => {
513
536
  this._state.favicon = favicons[0] || "";
514
537
  this.onChange();
515
538
  });
539
+ wc.on("media-started-playing", () => {
540
+ this._state.isAudible = true;
541
+ this._state.isMuted = wc.isAudioMuted();
542
+ this.onChange();
543
+ });
544
+ wc.on("media-paused", () => {
545
+ setTimeout(() => {
546
+ if (wc.isDestroyed()) return;
547
+ this._state.isAudible = wc.isCurrentlyAudible();
548
+ this._state.isMuted = wc.isAudioMuted();
549
+ this.onChange();
550
+ }, 250);
551
+ });
516
552
  wc.on("context-menu", (_event, params) => {
517
553
  const x = params.x;
518
554
  const y = params.y;
@@ -530,7 +566,7 @@ class Tab {
530
566
  ).then((highlightedText) => {
531
567
  this.buildContextMenu(wc, params, highlightedText.trim());
532
568
  }).catch((err) => {
533
- logger$i.warn("Failed to inspect highlighted text for context menu:", err);
569
+ logger$j.warn("Failed to inspect highlighted text for context menu:", err);
534
570
  this.buildContextMenu(wc, params, "");
535
571
  });
536
572
  });
@@ -604,6 +640,19 @@ class Tab {
604
640
  })
605
641
  );
606
642
  }
643
+ menu.append(new electron.MenuItem({ type: "separator" }));
644
+ menu.append(
645
+ new electron.MenuItem({
646
+ label: "Save Page As...",
647
+ click: () => this.onSavePage?.()
648
+ })
649
+ );
650
+ menu.append(
651
+ new electron.MenuItem({
652
+ label: "View Page Source",
653
+ click: () => void this.viewSource()
654
+ })
655
+ );
607
656
  menu.popup({ window: this.parentWindow });
608
657
  }
609
658
  get state() {
@@ -676,12 +725,74 @@ class Tab {
676
725
  reload() {
677
726
  this.view.webContents.reload();
678
727
  }
728
+ zoomIn() {
729
+ const wc = this.view.webContents;
730
+ const level = wc.getZoomLevel();
731
+ wc.setZoomLevel(level + 0.5);
732
+ }
733
+ zoomOut() {
734
+ const wc = this.view.webContents;
735
+ const level = wc.getZoomLevel();
736
+ wc.setZoomLevel(level - 0.5);
737
+ }
738
+ zoomReset() {
739
+ this.view.webContents.setZoomLevel(0);
740
+ }
741
+ async viewSource() {
742
+ const url = this.view.webContents.getURL();
743
+ let html;
744
+ try {
745
+ html = await this.view.webContents.executeJavaScript(
746
+ "document.documentElement.outerHTML"
747
+ );
748
+ } catch (err) {
749
+ logger$j.warn("Failed to retrieve page source:", err);
750
+ return;
751
+ }
752
+ const escaped = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
753
+ const win = new electron.BrowserWindow({
754
+ width: 960,
755
+ height: 700,
756
+ title: `view-source:${url}`,
757
+ backgroundColor: "#1a1a1e",
758
+ webPreferences: {
759
+ sandbox: true,
760
+ contextIsolation: true,
761
+ nodeIntegration: false,
762
+ spellcheck: false
763
+ }
764
+ });
765
+ const style = "background:#1a1a1e;color:#e0e0e0;font-family:monospace;font-size:12px;line-height:1.5;padding:16px;margin:0;white-space:pre-wrap;word-break:break-all;";
766
+ const dataUrl = `data:text/html;charset=utf-8,<!DOCTYPE html><html><head><title>view-source:${url}</title></head><body style="${style}"><pre>${escaped}</pre></body></html>`;
767
+ win.loadURL(dataUrl);
768
+ }
679
769
  setAdBlockingEnabled(enabled) {
680
770
  if (this._state.adBlockingEnabled === enabled) return false;
681
771
  this._state.adBlockingEnabled = enabled;
682
772
  this.onChange();
683
773
  return true;
684
774
  }
775
+ setPinned(pinned) {
776
+ if (this._state.isPinned === pinned) return;
777
+ this._state.isPinned = pinned;
778
+ this.onChange();
779
+ }
780
+ setGroup(groupId) {
781
+ if (this._state.groupId === groupId) return;
782
+ this._state.groupId = groupId;
783
+ this.onChange();
784
+ }
785
+ setMuted(muted) {
786
+ const wc = this.view.webContents;
787
+ wc.setAudioMuted(muted);
788
+ this._state.isMuted = wc.isAudioMuted();
789
+ this._state.isAudible = wc.isCurrentlyAudible();
790
+ this.onChange();
791
+ }
792
+ toggleMuted() {
793
+ this.setMuted(!this._state.isMuted);
794
+ return this._state.isMuted;
795
+ }
685
796
  get highlightModeActive() {
686
797
  return this._highlightModeActive;
687
798
  }
@@ -761,7 +872,7 @@ class Tab {
761
872
  document.addEventListener('mouseup', window.__vesselHighlightHandler);
762
873
  }
763
874
  })()
764
- `).catch((err) => logger$i.warn("Failed to inject highlight listener:", err));
875
+ `).catch((err) => logger$j.warn("Failed to inject highlight listener:", err));
765
876
  } else {
766
877
  void wc.executeJavaScript(`
767
878
  (function() {
@@ -772,7 +883,7 @@ class Tab {
772
883
  delete window.__vesselHighlightHandler;
773
884
  }
774
885
  })()
775
- `).catch((err) => logger$i.warn("Failed to remove highlight listener:", err));
886
+ `).catch((err) => logger$j.warn("Failed to remove highlight listener:", err));
776
887
  }
777
888
  }
778
889
  get webContentsId() {
@@ -783,7 +894,33 @@ class Tab {
783
894
  this.view.webContents.close();
784
895
  }
785
896
  }
786
- const logger$h = createLogger("JsonPersistence");
897
+ const TAB_GROUP_COLORS = [
898
+ "blue",
899
+ "green",
900
+ "yellow",
901
+ "orange",
902
+ "red",
903
+ "purple",
904
+ "gray"
905
+ ];
906
+ const TAB_GROUP_COLOR_LABELS = {
907
+ blue: "Blue",
908
+ green: "Green",
909
+ yellow: "Yellow",
910
+ orange: "Orange",
911
+ red: "Red",
912
+ purple: "Purple",
913
+ gray: "Gray"
914
+ };
915
+ const SEARCH_ENGINE_PRESETS = {
916
+ duckduckgo: { label: "DuckDuckGo", url: "https://duckduckgo.com/?q=" },
917
+ google: { label: "Google", url: "https://www.google.com/search?q=" },
918
+ bing: { label: "Bing", url: "https://www.bing.com/search?q=" },
919
+ brave: { label: "Brave Search", url: "https://search.brave.com/search?q=" },
920
+ ecosia: { label: "Ecosia", url: "https://www.ecosia.org/search?q=" },
921
+ kagi: { label: "Kagi", url: "https://kagi.com/search?q=" }
922
+ };
923
+ const logger$i = createLogger("JsonPersistence");
787
924
  function canUseSafeStorage() {
788
925
  try {
789
926
  return electron.safeStorage.isEncryptionAvailable();
@@ -848,7 +985,7 @@ function createDebouncedJsonPersistence({
848
985
  data,
849
986
  typeof data === "string" ? { encoding: "utf-8", mode: 384 } : { mode: 384 }
850
987
  )
851
- ).catch((err) => logger$h.error(`Failed to save ${logLabel}:`, err));
988
+ ).catch((err) => logger$i.error(`Failed to save ${logLabel}:`, err));
852
989
  };
853
990
  const schedule = () => {
854
991
  saveDirty2 = true;
@@ -876,6 +1013,20 @@ const SAVE_DEBOUNCE_MS$5 = 250;
876
1013
  function getHighlightsPath() {
877
1014
  return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
878
1015
  }
1016
+ function createPersistence$1() {
1017
+ return createDebouncedJsonPersistence({
1018
+ debounceMs: SAVE_DEBOUNCE_MS$5,
1019
+ filePath: getHighlightsPath(),
1020
+ getValue: () => state$4,
1021
+ logLabel: "highlights",
1022
+ resetOnSchedule: true
1023
+ });
1024
+ }
1025
+ let persistence$5 = null;
1026
+ function getPersistence$1() {
1027
+ persistence$5 ??= createPersistence$1();
1028
+ return persistence$5;
1029
+ }
879
1030
  function load$4() {
880
1031
  if (state$4) return state$4;
881
1032
  state$4 = loadJsonFile({
@@ -890,15 +1041,8 @@ function load$4() {
890
1041
  });
891
1042
  return state$4;
892
1043
  }
893
- const persistence$5 = createDebouncedJsonPersistence({
894
- debounceMs: SAVE_DEBOUNCE_MS$5,
895
- filePath: getHighlightsPath(),
896
- getValue: () => state$4,
897
- logLabel: "highlights",
898
- resetOnSchedule: true
899
- });
900
1044
  function save$2() {
901
- persistence$5.schedule();
1045
+ getPersistence$1().schedule();
902
1046
  }
903
1047
  function emit$2() {
904
1048
  if (!state$4) return;
@@ -980,7 +1124,7 @@ function clearHighlightsForUrl(url) {
980
1124
  return removed;
981
1125
  }
982
1126
  function flushPersist$4() {
983
- return persistence$5.flush();
1127
+ return getPersistence$1().flush();
984
1128
  }
985
1129
  const SKIP_TAGS_JS = "var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};";
986
1130
  const CONTENT_ROOTS_JS = `
@@ -2336,24 +2480,37 @@ function destroySession(tabId) {
2336
2480
  sessions.delete(tabId);
2337
2481
  }
2338
2482
  }
2339
- function destroyAllSessions() {
2340
- for (const session of sessions.values()) {
2341
- session.destroy();
2342
- }
2343
- sessions.clear();
2483
+ const logger$h = createLogger("TabManager");
2484
+ function sanitizePdfFilename(title) {
2485
+ const clean = title.replace(/[<>:"/\\|?*\x00-\x1f]/g, " ").replace(/\s+/g, " ").trim();
2486
+ const base = (clean || "Vessel Page").replace(/\.pdf$/i, "");
2487
+ return `${base}.pdf`;
2488
+ }
2489
+ function sanitizePageFilename(title, ext) {
2490
+ const clean = title.replace(/[<>:\"/\\|?*\x00-\x1f]/g, " ").replace(/\s+/g, " ").trim();
2491
+ const escapedExt = ext.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2492
+ const regex = new RegExp(`\\.${escapedExt}$`, "i");
2493
+ const base = (clean || "Vessel Page").replace(regex, "");
2494
+ return `${base}.${ext}`;
2344
2495
  }
2345
- const logger$g = createLogger("TabManager");
2346
2496
  class TabManager {
2347
2497
  tabs = /* @__PURE__ */ new Map();
2348
2498
  order = [];
2499
+ tabGroups = /* @__PURE__ */ new Map();
2349
2500
  activeTabId = null;
2350
2501
  window;
2351
2502
  onStateChange;
2352
2503
  highlightCaptureCallback = null;
2353
2504
  pageLoadCallback = null;
2354
- constructor(window2, onStateChange) {
2505
+ closedTabs = [];
2506
+ MAX_CLOSED_TABS = 20;
2507
+ isPrivate;
2508
+ sessionPartition;
2509
+ constructor(window2, onStateChange, options) {
2355
2510
  this.window = window2;
2356
2511
  this.onStateChange = onStateChange;
2512
+ this.isPrivate = options?.isPrivate ?? false;
2513
+ this.sessionPartition = options?.sessionPartition ?? (this.isPrivate ? "private-mode" : void 0);
2357
2514
  }
2358
2515
  onPageLoad(cb) {
2359
2516
  this.pageLoadCallback = cb;
@@ -2364,17 +2521,23 @@ class TabManager {
2364
2521
  const tab = new Tab(id, url, () => this.broadcastState(), {
2365
2522
  adBlockingEnabled: options?.adBlockingEnabled,
2366
2523
  parentWindow: this.window,
2524
+ sessionPartition: this.sessionPartition,
2367
2525
  onOpenUrl: ({ url: requestedUrl, background: background2, adBlockingEnabled }) => {
2368
2526
  this.createTab(requestedUrl, { background: background2, adBlockingEnabled });
2369
2527
  },
2370
2528
  onPageLoad: (pageUrl, wc) => {
2371
2529
  this.reapplyHighlights(pageUrl, wc);
2372
- addEntry$1(pageUrl, wc.getTitle());
2530
+ if (!this.isPrivate) {
2531
+ addEntry$1(pageUrl, wc.getTitle());
2532
+ }
2373
2533
  this.pageLoadCallback?.(pageUrl, wc);
2374
2534
  },
2375
2535
  onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
2376
2536
  onHighlightRemove: (url2, text) => this.removeHighlightByText(url2, text),
2377
- onHighlightRecolor: (url2, text, color) => this.recolorHighlightByText(url2, text, color)
2537
+ onHighlightRecolor: (url2, text, color) => this.recolorHighlightByText(url2, text, color),
2538
+ onSavePage: () => {
2539
+ void this.savePage(id);
2540
+ }
2378
2541
  });
2379
2542
  this.tabs.set(id, tab);
2380
2543
  this.order.push(id);
@@ -2401,6 +2564,16 @@ class TabManager {
2401
2564
  closeTab(id) {
2402
2565
  const tab = this.tabs.get(id);
2403
2566
  if (!tab) return;
2567
+ if (tab.state.isPinned) return;
2568
+ const groupId = tab.state.groupId;
2569
+ this.closedTabs.push({
2570
+ url: tab.state.url,
2571
+ title: tab.state.title,
2572
+ adBlockingEnabled: tab.state.adBlockingEnabled
2573
+ });
2574
+ if (this.closedTabs.length > this.MAX_CLOSED_TABS) {
2575
+ this.closedTabs.shift();
2576
+ }
2404
2577
  const wcId = tab.webContentsId;
2405
2578
  if (wcId !== void 0) {
2406
2579
  this.lastReapply.delete(wcId);
@@ -2410,6 +2583,7 @@ class TabManager {
2410
2583
  tab.destroy();
2411
2584
  this.tabs.delete(id);
2412
2585
  this.order = this.order.filter((tid) => tid !== id);
2586
+ this.removeGroupIfEmpty(groupId);
2413
2587
  if (this.activeTabId === id) {
2414
2588
  if (this.order.length > 0) {
2415
2589
  this.switchTab(this.order[this.order.length - 1]);
@@ -2434,6 +2608,137 @@ class TabManager {
2434
2608
  reloadTab(id) {
2435
2609
  this.tabs.get(id)?.reload();
2436
2610
  }
2611
+ zoomIn(id) {
2612
+ this.tabs.get(id)?.zoomIn();
2613
+ }
2614
+ zoomOut(id) {
2615
+ this.tabs.get(id)?.zoomOut();
2616
+ }
2617
+ zoomReset(id) {
2618
+ this.tabs.get(id)?.zoomReset();
2619
+ }
2620
+ reopenClosedTab() {
2621
+ const last = this.closedTabs.pop();
2622
+ if (!last) return null;
2623
+ return this.createTab(last.url, { adBlockingEnabled: last.adBlockingEnabled });
2624
+ }
2625
+ duplicateTab(id) {
2626
+ const tab = this.tabs.get(id);
2627
+ if (!tab) return null;
2628
+ return this.createTab(tab.state.url, { adBlockingEnabled: tab.state.adBlockingEnabled });
2629
+ }
2630
+ pinTab(id) {
2631
+ const tab = this.tabs.get(id);
2632
+ if (!tab) return;
2633
+ tab.setPinned(true);
2634
+ this.order = this.order.filter((tid) => tid !== id);
2635
+ const firstNonPinned = this.order.findIndex((tid) => !this.tabs.get(tid)?.state.isPinned);
2636
+ if (firstNonPinned === -1) {
2637
+ this.order.push(id);
2638
+ } else {
2639
+ this.order.splice(firstNonPinned, 0, id);
2640
+ }
2641
+ this.broadcastState();
2642
+ }
2643
+ unpinTab(id) {
2644
+ const tab = this.tabs.get(id);
2645
+ if (!tab) return;
2646
+ tab.setPinned(false);
2647
+ this.order = this.order.filter((tid) => tid !== id);
2648
+ const firstNonPinned = this.order.findIndex((tid) => !this.tabs.get(tid)?.state.isPinned);
2649
+ if (firstNonPinned === -1) {
2650
+ this.order.push(id);
2651
+ } else {
2652
+ this.order.splice(firstNonPinned, 0, id);
2653
+ }
2654
+ this.broadcastState();
2655
+ }
2656
+ createGroupFromTab(id, options) {
2657
+ const tab = this.tabs.get(id);
2658
+ if (!tab) return null;
2659
+ const previousGroupId = tab.state.groupId;
2660
+ const groupId = crypto$1.randomUUID();
2661
+ const color = options?.color && TAB_GROUP_COLORS.includes(options.color) ? options.color : TAB_GROUP_COLORS[this.tabGroups.size % TAB_GROUP_COLORS.length];
2662
+ this.tabGroups.set(groupId, {
2663
+ id: groupId,
2664
+ name: options?.name?.trim() || `Group ${this.tabGroups.size + 1}`,
2665
+ color,
2666
+ collapsed: false
2667
+ });
2668
+ this.assignTabToGroup(id, groupId);
2669
+ this.removeGroupIfEmpty(previousGroupId);
2670
+ return groupId;
2671
+ }
2672
+ assignTabToGroup(id, groupId) {
2673
+ const tab = this.tabs.get(id);
2674
+ if (!tab || !this.tabGroups.has(groupId)) return;
2675
+ const previousGroupId = tab.state.groupId;
2676
+ tab.setGroup(groupId);
2677
+ this.removeGroupIfEmpty(previousGroupId);
2678
+ this.broadcastState();
2679
+ }
2680
+ removeTabFromGroup(id) {
2681
+ const tab = this.tabs.get(id);
2682
+ if (!tab) return;
2683
+ const groupId = tab.state.groupId;
2684
+ tab.setGroup(void 0);
2685
+ this.removeGroupIfEmpty(groupId);
2686
+ this.broadcastState();
2687
+ }
2688
+ toggleGroupCollapsed(groupId) {
2689
+ const group = this.tabGroups.get(groupId);
2690
+ if (!group) return null;
2691
+ group.collapsed = !group.collapsed;
2692
+ this.broadcastState();
2693
+ return group.collapsed;
2694
+ }
2695
+ setGroupColor(groupId, color) {
2696
+ const group = this.tabGroups.get(groupId);
2697
+ if (!group || !TAB_GROUP_COLORS.includes(color)) return;
2698
+ group.color = color;
2699
+ this.broadcastState();
2700
+ }
2701
+ toggleMuted(id) {
2702
+ return this.tabs.get(id)?.toggleMuted() ?? null;
2703
+ }
2704
+ printTab(id) {
2705
+ const tab = this.tabs.get(id);
2706
+ if (!tab) return;
2707
+ tab.view.webContents.print();
2708
+ }
2709
+ async saveTabAsPdf(id) {
2710
+ const tab = this.tabs.get(id);
2711
+ if (!tab) return null;
2712
+ const { canceled, filePath } = await electron.dialog.showSaveDialog({
2713
+ title: "Save Page as PDF",
2714
+ defaultPath: sanitizePdfFilename(tab.state.title || "Vessel Page"),
2715
+ filters: [{ name: "PDF", extensions: ["pdf"] }]
2716
+ });
2717
+ if (canceled || !filePath) return null;
2718
+ const data = await tab.view.webContents.printToPDF({
2719
+ printBackground: true
2720
+ });
2721
+ await fs.promises.writeFile(filePath, data);
2722
+ return filePath;
2723
+ }
2724
+ async savePage(id, format = "MHTML") {
2725
+ const tab = this.tabs.get(id);
2726
+ if (!tab) return null;
2727
+ const ext = format === "MHTML" ? "mhtml" : "html";
2728
+ const { canceled, filePath } = await electron.dialog.showSaveDialog({
2729
+ title: "Save Page As",
2730
+ defaultPath: sanitizePageFilename(
2731
+ tab.state.title || "Vessel Page",
2732
+ ext
2733
+ ),
2734
+ filters: [
2735
+ { name: format === "MHTML" ? "MHTML" : "HTML", extensions: [ext] }
2736
+ ]
2737
+ });
2738
+ if (canceled || !filePath) return null;
2739
+ await tab.view.webContents.savePage(filePath, format);
2740
+ return filePath;
2741
+ }
2437
2742
  getActiveTab() {
2438
2743
  return this.activeTabId ? this.tabs.get(this.activeTabId) : void 0;
2439
2744
  }
@@ -2444,7 +2749,10 @@ class TabManager {
2444
2749
  return this.activeTabId;
2445
2750
  }
2446
2751
  getAllStates() {
2447
- return this.order.map((id) => this.tabs.get(id).state);
2752
+ return this.order.map((id) => this.withGroupState(this.tabs.get(id).state));
2753
+ }
2754
+ getGroups() {
2755
+ return Array.from(this.tabGroups.values());
2448
2756
  }
2449
2757
  findTabByWebContentsId(webContentsId) {
2450
2758
  for (const id of this.order) {
@@ -2477,7 +2785,10 @@ class TabManager {
2477
2785
  id: state2.id,
2478
2786
  url: state2.url || "about:blank",
2479
2787
  title: state2.title,
2480
- adBlockingEnabled: state2.adBlockingEnabled
2788
+ adBlockingEnabled: state2.adBlockingEnabled,
2789
+ isPinned: state2.isPinned,
2790
+ groupName: state2.groupName,
2791
+ groupColor: state2.groupColor
2481
2792
  })),
2482
2793
  activeIndex: activeIndex >= 0 ? activeIndex : 0,
2483
2794
  activeTabId: activeId || void 0,
@@ -2492,12 +2803,33 @@ class TabManager {
2492
2803
  Math.min(snapshot.activeIndex, tabs.length - 1)
2493
2804
  );
2494
2805
  this.destroyAllTabs();
2806
+ const restoredGroups = /* @__PURE__ */ new Map();
2495
2807
  const ids = tabs.map(
2496
2808
  (tab, index) => this.createTab(tab.url || "about:blank", {
2497
2809
  background: index !== activeIndex,
2498
2810
  adBlockingEnabled: tab.adBlockingEnabled ?? true
2499
2811
  })
2500
2812
  );
2813
+ tabs.forEach((tab, index) => {
2814
+ if (tab.isPinned && ids[index]) {
2815
+ this.pinTab(ids[index]);
2816
+ }
2817
+ if (tab.groupName && ids[index]) {
2818
+ const key = `${tab.groupName}|${tab.groupColor ?? "blue"}`;
2819
+ let groupId = restoredGroups.get(key);
2820
+ if (!groupId) {
2821
+ groupId = crypto$1.randomUUID();
2822
+ restoredGroups.set(key, groupId);
2823
+ this.tabGroups.set(groupId, {
2824
+ id: groupId,
2825
+ name: tab.groupName,
2826
+ color: tab.groupColor ?? "blue",
2827
+ collapsed: false
2828
+ });
2829
+ }
2830
+ this.assignTabToGroup(ids[index], groupId);
2831
+ }
2832
+ });
2501
2833
  const activeId = ids[activeIndex];
2502
2834
  if (activeId) {
2503
2835
  this.switchTab(activeId);
@@ -2507,15 +2839,16 @@ class TabManager {
2507
2839
  return ids;
2508
2840
  }
2509
2841
  destroyAllTabs() {
2510
- destroyAllSessions();
2511
- for (const id of this.order) {
2842
+ for (const id of [...this.order]) {
2512
2843
  const tab = this.tabs.get(id);
2513
2844
  if (!tab) continue;
2845
+ destroySession(id);
2514
2846
  this.window.contentView.removeChildView(tab.view);
2515
2847
  tab.destroy();
2516
2848
  }
2517
2849
  this.tabs.clear();
2518
2850
  this.order = [];
2851
+ this.tabGroups.clear();
2519
2852
  this.activeTabId = null;
2520
2853
  this.broadcastState();
2521
2854
  }
@@ -2536,7 +2869,7 @@ class TabManager {
2536
2869
  }));
2537
2870
  if (entries.length > 0) {
2538
2871
  void highlightBatchOnPage(wc, entries).catch(
2539
- (err) => logger$g.warn("Failed to batch highlight:", err)
2872
+ (err) => logger$h.warn("Failed to batch highlight:", err)
2540
2873
  );
2541
2874
  }
2542
2875
  }
@@ -2558,12 +2891,12 @@ class TabManager {
2558
2891
  const result = await captureSelectionHighlight(wc);
2559
2892
  if (result.success && result.text) {
2560
2893
  await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(
2561
- (err) => logger$g.warn("Failed to capture highlight:", err)
2894
+ (err) => logger$h.warn("Failed to capture highlight:", err)
2562
2895
  );
2563
2896
  }
2564
2897
  this.highlightCaptureCallback?.(result);
2565
2898
  } catch (err) {
2566
- logger$g.warn("Failed to capture highlight from page:", err);
2899
+ logger$h.warn("Failed to capture highlight from page:", err);
2567
2900
  this.highlightCaptureCallback?.({
2568
2901
  success: false,
2569
2902
  message: "Could not capture selection"
@@ -2588,7 +2921,7 @@ class TabManager {
2588
2921
  void this.removeHighlightMarksForText(wc, text);
2589
2922
  }
2590
2923
  } catch (err) {
2591
- logger$g.warn("Failed to remove highlight from matching tab:", err);
2924
+ logger$h.warn("Failed to remove highlight from matching tab:", err);
2592
2925
  }
2593
2926
  }
2594
2927
  this.highlightCaptureCallback?.({
@@ -2619,12 +2952,12 @@ class TabManager {
2619
2952
  void 0,
2620
2953
  color
2621
2954
  ).catch(
2622
- (err) => logger$g.warn("Failed to update highlight color:", err)
2955
+ (err) => logger$h.warn("Failed to update highlight color:", err)
2623
2956
  );
2624
2957
  });
2625
2958
  }
2626
2959
  } catch (err) {
2627
- logger$g.warn("Failed to iterate highlights for color change:", err);
2960
+ logger$h.warn("Failed to iterate highlights for color change:", err);
2628
2961
  }
2629
2962
  }
2630
2963
  this.highlightCaptureCallback?.({
@@ -2632,6 +2965,24 @@ class TabManager {
2632
2965
  message: `Color changed to ${color}`
2633
2966
  });
2634
2967
  }
2968
+ withGroupState(state2) {
2969
+ if (!state2.groupId) return state2;
2970
+ const group = this.tabGroups.get(state2.groupId);
2971
+ if (!group) return { ...state2, groupId: void 0 };
2972
+ return {
2973
+ ...state2,
2974
+ groupName: group.name,
2975
+ groupColor: group.color,
2976
+ groupCollapsed: group.collapsed
2977
+ };
2978
+ }
2979
+ removeGroupIfEmpty(groupId) {
2980
+ if (!groupId) return;
2981
+ for (const tab of this.tabs.values()) {
2982
+ if (tab.state.groupId === groupId) return;
2983
+ }
2984
+ this.tabGroups.delete(groupId);
2985
+ }
2635
2986
  async removeHighlightMarksForText(wc, text) {
2636
2987
  await wc.executeJavaScript(
2637
2988
  `(function() {
@@ -2646,7 +2997,7 @@ class TabManager {
2646
2997
  });
2647
2998
  })()`
2648
2999
  ).catch(
2649
- (err) => logger$g.warn("Failed to remove highlight marks:", err)
3000
+ (err) => logger$h.warn("Failed to remove highlight marks:", err)
2650
3001
  );
2651
3002
  }
2652
3003
  broadcastState() {
@@ -2713,6 +3064,8 @@ const Channels = {
2713
3064
  BOOKMARK_SAVE: "bookmarks:save",
2714
3065
  BOOKMARK_UPDATE: "bookmarks:update-item",
2715
3066
  BOOKMARK_REMOVE: "bookmarks:remove",
3067
+ BOOKMARKS_EXPORT_HTML: "bookmarks:export-html",
3068
+ BOOKMARKS_EXPORT_JSON: "bookmarks:export-json",
2716
3069
  BOOKMARK_ADD_CONTEXT_TO_CHAT: "bookmarks:add-context-to-chat",
2717
3070
  FOLDER_CREATE: "bookmarks:folder-create",
2718
3071
  FOLDER_REMOVE: "bookmarks:folder-remove",
@@ -2733,6 +3086,34 @@ const Channels = {
2733
3086
  DEVTOOLS_PANEL_RESIZE: "devtools-panel:resize",
2734
3087
  // Ad blocking
2735
3088
  TAB_TOGGLE_AD_BLOCK: "tab:toggle-ad-block",
3089
+ // Zoom
3090
+ TAB_ZOOM_IN: "tab:zoom-in",
3091
+ TAB_ZOOM_OUT: "tab:zoom-out",
3092
+ TAB_ZOOM_RESET: "tab:zoom-reset",
3093
+ // Closed tabs / duplication
3094
+ TAB_REOPEN_CLOSED: "tab:reopen-closed",
3095
+ TAB_DUPLICATE: "tab:duplicate",
3096
+ TAB_CONTEXT_MENU: "tab:context-menu",
3097
+ // Pin tabs
3098
+ TAB_PIN: "tab:pin",
3099
+ TAB_UNPIN: "tab:unpin",
3100
+ // Tab groups
3101
+ TAB_GROUP_CREATE: "tab-group:create",
3102
+ TAB_GROUP_ADD_TAB: "tab-group:add-tab",
3103
+ TAB_GROUP_REMOVE_TAB: "tab-group:remove-tab",
3104
+ TAB_GROUP_TOGGLE_COLLAPSED: "tab-group:toggle-collapsed",
3105
+ TAB_GROUP_SET_COLOR: "tab-group:set-color",
3106
+ TAB_GROUP_CONTEXT_MENU: "tab-group:context-menu",
3107
+ // Audio / mute
3108
+ TAB_TOGGLE_MUTE: "tab:toggle-mute",
3109
+ // Print
3110
+ TAB_PRINT: "tab:print",
3111
+ TAB_PRINT_TO_PDF: "tab:print-to-pdf",
3112
+ // Windows
3113
+ OPEN_NEW_WINDOW: "window:open-new",
3114
+ // Private browsing
3115
+ OPEN_PRIVATE_WINDOW: "private:open-window",
3116
+ IS_PRIVATE_MODE: "private:is-private",
2736
3117
  // Find in page
2737
3118
  FIND_IN_PAGE_START: "find:start",
2738
3119
  FIND_IN_PAGE_NEXT: "find:next",
@@ -3750,7 +4131,7 @@ function errorResult(error, value) {
3750
4131
  function getErrorMessage(error, fallback = "Unknown error") {
3751
4132
  return error instanceof Error && error.message ? error.message : fallback;
3752
4133
  }
3753
- const logger$f = createLogger("Premium");
4134
+ const logger$g = createLogger("Premium");
3754
4135
  const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
3755
4136
  const FREE_TOOL_ITERATION_LIMIT = 50;
3756
4137
  const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
@@ -3863,7 +4244,7 @@ async function verifySubscription(identifier) {
3863
4244
  });
3864
4245
  if (!res.ok) {
3865
4246
  const detail = await readApiErrorDetail(res);
3866
- logger$f.warn(
4247
+ logger$g.warn(
3867
4248
  "Verification API returned a non-OK status:",
3868
4249
  res.status,
3869
4250
  detail
@@ -3882,7 +4263,7 @@ async function verifySubscription(identifier) {
3882
4263
  setSetting("premium", updated);
3883
4264
  return updated;
3884
4265
  } catch (err) {
3885
- logger$f.warn("Verification failed:", err);
4266
+ logger$g.warn("Verification failed:", err);
3886
4267
  return current;
3887
4268
  }
3888
4269
  }
@@ -4455,7 +4836,7 @@ const EXTRACT_TIMEOUT_MAX_MS = 2e4;
4455
4836
  const MUTATION_CAPTURE_INTERVAL_MS = 5e3;
4456
4837
  const MUTATION_SETTLE_AFTER_MS = 1500;
4457
4838
  const AGENT_STREAM_IDLE_TIMEOUT_MS = 3e4;
4458
- const logger$e = createLogger("Extractor");
4839
+ const logger$f = createLogger("Extractor");
4459
4840
  const EMPTY_PAGE_CONTENT = {
4460
4841
  title: "",
4461
4842
  content: "",
@@ -5205,7 +5586,7 @@ async function executeScript(webContents, script) {
5205
5586
  })
5206
5587
  ]);
5207
5588
  } catch (err) {
5208
- logger$e.warn("Failed to execute page script:", err);
5589
+ logger$f.warn("Failed to execute page script:", err);
5209
5590
  return null;
5210
5591
  } finally {
5211
5592
  if (timer) {
@@ -5312,7 +5693,7 @@ async function estimateExtractionTimeout(webContents) {
5312
5693
  return EXTRACT_TIMEOUT_BASE_MS + extra;
5313
5694
  }
5314
5695
  } catch (err) {
5315
- logger$e.warn("Failed to estimate extraction timeout, using base timeout:", err);
5696
+ logger$f.warn("Failed to estimate extraction timeout, using base timeout:", err);
5316
5697
  }
5317
5698
  return EXTRACT_TIMEOUT_BASE_MS;
5318
5699
  }
@@ -5623,7 +6004,7 @@ function enableClipboardShortcuts(view) {
5623
6004
  }
5624
6005
  });
5625
6006
  }
5626
- const CHROME_HEIGHT = 110;
6007
+ const CHROME_HEIGHT$2 = 110;
5627
6008
  const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
5628
6009
  const MIN_DEVTOOLS_PANEL = 120;
5629
6010
  const MAX_DEVTOOLS_PANEL = 600;
@@ -5765,7 +6146,8 @@ function createMainWindow(onTabStateChange) {
5765
6146
  preload: path.join(__dirname, "../preload/index.js"),
5766
6147
  sandbox: true,
5767
6148
  contextIsolation: true,
5768
- nodeIntegration: false
6149
+ nodeIntegration: false,
6150
+ spellcheck: false
5769
6151
  }
5770
6152
  });
5771
6153
  chromeView.setBackgroundColor("#00000000");
@@ -5775,7 +6157,8 @@ function createMainWindow(onTabStateChange) {
5775
6157
  preload: path.join(__dirname, "../preload/index.js"),
5776
6158
  sandbox: true,
5777
6159
  contextIsolation: true,
5778
- nodeIntegration: false
6160
+ nodeIntegration: false,
6161
+ spellcheck: false
5779
6162
  }
5780
6163
  });
5781
6164
  sidebarView.setBackgroundColor("#00000000");
@@ -5789,7 +6172,8 @@ function createMainWindow(onTabStateChange) {
5789
6172
  preload: path.join(__dirname, "../preload/index.js"),
5790
6173
  sandbox: true,
5791
6174
  contextIsolation: true,
5792
- nodeIntegration: false
6175
+ nodeIntegration: false,
6176
+ spellcheck: false
5793
6177
  }
5794
6178
  });
5795
6179
  devtoolsPanelView.setBackgroundColor("#00000000");
@@ -5838,7 +6222,7 @@ function layoutViews(state2) {
5838
6222
  uiState
5839
6223
  } = state2;
5840
6224
  const [width, height] = mainWindow.getContentSize();
5841
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
6225
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$2;
5842
6226
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5843
6227
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5844
6228
  const chromeNeedsFullHeight = uiState.settingsOpen;
@@ -5888,7 +6272,7 @@ function layoutViews(state2) {
5888
6272
  function resizeSidebarViews(state2) {
5889
6273
  const { mainWindow, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
5890
6274
  const [width, height] = mainWindow.getContentSize();
5891
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
6275
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$2;
5892
6276
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5893
6277
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5894
6278
  const resizeHandleOverlap = 6;
@@ -6618,7 +7002,7 @@ const MAX_MCP_NAV_CONTENT_LENGTH = 3e4;
6618
7002
  const MAX_AGENT_DEBUG_CONTENT_LENGTH = 2e4;
6619
7003
  const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
6620
7004
  const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
6621
- const logger$d = createLogger("OpenAIProvider");
7005
+ const logger$e = createLogger("OpenAIProvider");
6622
7006
  function shouldDebugAgentLoop() {
6623
7007
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
6624
7008
  return value === "1" || value === "true";
@@ -7119,9 +7503,9 @@ function resolveToolCallName(rawName, args, availableToolNames) {
7119
7503
  function logAgentLoopDebug(payload) {
7120
7504
  if (!shouldDebugAgentLoop()) return;
7121
7505
  try {
7122
- logger$d.info(`[agent-debug] ${JSON.stringify(payload)}`);
7506
+ logger$e.info(`[agent-debug] ${JSON.stringify(payload)}`);
7123
7507
  } catch (err) {
7124
- logger$d.warn("Failed to serialize debug payload:", err);
7508
+ logger$e.warn("Failed to serialize debug payload:", err);
7125
7509
  }
7126
7510
  }
7127
7511
  function recoverTextEncodedToolCalls(text, availableToolNames) {
@@ -7755,7 +8139,7 @@ function createProvider(config) {
7755
8139
  return new OpenAICompatProvider(normalized);
7756
8140
  }
7757
8141
  const require$1 = node_module.createRequire(require("url").pathToFileURL(__filename).href);
7758
- const logger$c = createLogger("DevTrace");
8142
+ const logger$d = createLogger("DevTrace");
7759
8143
  let cachedFactory;
7760
8144
  function createNoopTraceSession() {
7761
8145
  return {
@@ -7788,7 +8172,7 @@ function loadLocalFactory() {
7788
8172
  return cachedFactory;
7789
8173
  }
7790
8174
  } catch (err) {
7791
- logger$c.warn("Failed to load local trace logger:", err);
8175
+ logger$d.warn("Failed to load local trace logger:", err);
7792
8176
  }
7793
8177
  }
7794
8178
  return cachedFactory;
@@ -9780,7 +10164,7 @@ function buildCompactScopedContext(page, mode, pageType = detectPageType(page))
9780
10164
  const primaryResults = primaryResultElements.map(formatElement);
9781
10165
  if (primaryResults.length > 0) {
9782
10166
  lines.push("");
9783
- lines.push("### Results — click one of these to open a product");
10167
+ lines.push("### Primary Results");
9784
10168
  lines.push(...primaryResults.map((item) => `- ${item}`));
9785
10169
  lines.push("");
9786
10170
  lines.push("IMPORTANT: Use click(index=N) on a result above. Do NOT click filter or sort links.");
@@ -9924,6 +10308,61 @@ const TOOL_DEFINITIONS = [
9924
10308
  },
9925
10309
  tier: 2
9926
10310
  },
10311
+ {
10312
+ name: "list_groups",
10313
+ title: "List Tab Groups",
10314
+ description: "List all tab groups with their IDs, names, colors, collapsed state, and member tab count.",
10315
+ tier: 2
10316
+ },
10317
+ {
10318
+ name: "create_group",
10319
+ title: "Create Tab Group",
10320
+ description: "Create a new tab group from the active tab or a specified tab. Optionally provide a name and color.",
10321
+ inputSchema: {
10322
+ tabId: zod.z.string().optional().describe("Tab ID to group (defaults to active tab)"),
10323
+ name: zod.z.string().optional().describe("Optional group name"),
10324
+ color: zod.z.enum(["blue", "green", "yellow", "orange", "red", "purple", "gray"]).optional().describe("Optional group color")
10325
+ },
10326
+ tier: 2
10327
+ },
10328
+ {
10329
+ name: "assign_to_group",
10330
+ title: "Assign Tab to Group",
10331
+ description: "Move a tab into an existing group by ID. Defaults to the active tab.",
10332
+ inputSchema: {
10333
+ groupId: zod.z.string().describe("Group ID to assign the tab to"),
10334
+ tabId: zod.z.string().optional().describe("Tab ID to move (defaults to active tab)")
10335
+ },
10336
+ tier: 2
10337
+ },
10338
+ {
10339
+ name: "remove_from_group",
10340
+ title: "Remove Tab from Group",
10341
+ description: "Ungroup a tab. Defaults to the active tab.",
10342
+ inputSchema: {
10343
+ tabId: zod.z.string().optional().describe("Tab ID to ungroup (defaults to active tab)")
10344
+ },
10345
+ tier: 2
10346
+ },
10347
+ {
10348
+ name: "toggle_group",
10349
+ title: "Toggle Group Collapsed",
10350
+ description: "Collapse or expand a tab group.",
10351
+ inputSchema: {
10352
+ groupId: zod.z.string().describe("Group ID to toggle")
10353
+ },
10354
+ tier: 2
10355
+ },
10356
+ {
10357
+ name: "set_group_color",
10358
+ title: "Set Group Color",
10359
+ description: "Change the color of a tab group.",
10360
+ inputSchema: {
10361
+ groupId: zod.z.string().describe("Group ID"),
10362
+ color: zod.z.enum(["blue", "green", "yellow", "orange", "red", "purple", "gray"]).describe("New color")
10363
+ },
10364
+ tier: 2
10365
+ },
9927
10366
  // --- Navigation ---
9928
10367
  {
9929
10368
  name: "navigate",
@@ -10706,6 +11145,12 @@ function shouldIncludeTool(toolName, pageType, intents, profile) {
10706
11145
  case "switch_tab":
10707
11146
  case "create_tab":
10708
11147
  case "set_ad_blocking":
11148
+ case "list_groups":
11149
+ case "create_group":
11150
+ case "assign_to_group":
11151
+ case "remove_from_group":
11152
+ case "toggle_group":
11153
+ case "set_group_color":
10709
11154
  return intents.has("tabs") || intents.has("debug");
10710
11155
  case "save_session":
10711
11156
  case "load_session":
@@ -10757,14 +11202,6 @@ function pruneToolsForContext(tools, pageType, query = "", options = {}) {
10757
11202
  return description !== tool.description ? { ...tool, description } : tool;
10758
11203
  });
10759
11204
  }
10760
- const SEARCH_ENGINE_PRESETS = {
10761
- duckduckgo: { label: "DuckDuckGo", url: "https://duckduckgo.com/?q=" },
10762
- google: { label: "Google", url: "https://www.google.com/search?q=" },
10763
- bing: { label: "Bing", url: "https://www.bing.com/search?q=" },
10764
- brave: { label: "Brave Search", url: "https://search.brave.com/search?q=" },
10765
- ecosia: { label: "Ecosia", url: "https://www.ecosia.org/search?q=" },
10766
- kagi: { label: "Kagi", url: "https://kagi.com/search?q=" }
10767
- };
10768
11205
  function trimText(value) {
10769
11206
  return typeof value === "string" ? value.trim() : "";
10770
11207
  }
@@ -10956,6 +11393,7 @@ function normalizeBookmarkMetadataUpdate(input) {
10956
11393
  }
10957
11394
  const UNSORTED_ID = "unsorted";
10958
11395
  const ARCHIVE_FOLDER_NAME = "Archive";
11396
+ const NETSCAPE_BOOKMARKS_DOCTYPE = "<!DOCTYPE NETSCAPE-Bookmark-file-1>";
10959
11397
  const SAVE_DEBOUNCE_MS$1 = 250;
10960
11398
  let state$1 = null;
10961
11399
  const listeners = /* @__PURE__ */ new Set();
@@ -10968,6 +11406,19 @@ function cloneState(current) {
10968
11406
  function getBookmarksPath() {
10969
11407
  return path.join(electron.app.getPath("userData"), "vessel-bookmarks.json");
10970
11408
  }
11409
+ function createPersistence() {
11410
+ return createDebouncedJsonPersistence({
11411
+ debounceMs: SAVE_DEBOUNCE_MS$1,
11412
+ filePath: getBookmarksPath(),
11413
+ getValue: () => state$1,
11414
+ logLabel: "bookmarks"
11415
+ });
11416
+ }
11417
+ let persistence$1 = null;
11418
+ function getPersistence() {
11419
+ persistence$1 ??= createPersistence();
11420
+ return persistence$1;
11421
+ }
10971
11422
  function load$1() {
10972
11423
  if (state$1) return state$1;
10973
11424
  state$1 = loadJsonFile({
@@ -10983,14 +11434,8 @@ function load$1() {
10983
11434
  });
10984
11435
  return state$1;
10985
11436
  }
10986
- const persistence$1 = createDebouncedJsonPersistence({
10987
- debounceMs: SAVE_DEBOUNCE_MS$1,
10988
- filePath: getBookmarksPath(),
10989
- getValue: () => state$1,
10990
- logLabel: "bookmarks"
10991
- });
10992
11437
  function save() {
10993
- persistence$1.schedule();
11438
+ getPersistence().schedule();
10994
11439
  }
10995
11440
  function assignDefinedBookmarkFields(bookmark, fields) {
10996
11441
  if (!fields) return;
@@ -11006,9 +11451,81 @@ function emit() {
11006
11451
  listener(snapshot);
11007
11452
  }
11008
11453
  }
11454
+ function escapeBookmarkHtml(value) {
11455
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11456
+ }
11457
+ function toNetscapeTimestamp(value) {
11458
+ if (!value) return Math.floor(Date.now() / 1e3);
11459
+ const time = Date.parse(value);
11460
+ return Number.isNaN(time) ? Math.floor(Date.now() / 1e3) : Math.floor(time / 1e3);
11461
+ }
11462
+ function getBookmarkDescription(bookmark) {
11463
+ const lines = [
11464
+ bookmark.note ? `Note: ${bookmark.note}` : "",
11465
+ bookmark.intent ? `Intent: ${bookmark.intent}` : "",
11466
+ bookmark.expectedContent ? `Expected content: ${bookmark.expectedContent}` : "",
11467
+ bookmark.keyFields?.length ? `Key fields: ${bookmark.keyFields.join(", ")}` : "",
11468
+ bookmark.agentHints && Object.keys(bookmark.agentHints).length > 0 ? `Agent hints: ${Object.entries(bookmark.agentHints).map(([key, value]) => `${key}: ${value}`).join("; ")}` : ""
11469
+ ].filter(Boolean);
11470
+ return lines.join("\n");
11471
+ }
11472
+ function appendBookmarkHtml(lines, bookmark, options, indent) {
11473
+ const addDate = toNetscapeTimestamp(bookmark.savedAt);
11474
+ lines.push(
11475
+ `${indent}<DT><A HREF="${escapeBookmarkHtml(bookmark.url)}" ADD_DATE="${addDate}">${escapeBookmarkHtml(bookmark.title || bookmark.url)}</A>`
11476
+ );
11477
+ if (!options.includeNotes) return;
11478
+ const description = getBookmarkDescription(bookmark);
11479
+ if (description) {
11480
+ lines.push(`${indent}<DD>${escapeBookmarkHtml(description)}`);
11481
+ }
11482
+ }
11009
11483
  function getState() {
11010
11484
  return cloneState(load$1());
11011
11485
  }
11486
+ function exportBookmarksHtml(options = {}) {
11487
+ const current = getState();
11488
+ const resolvedOptions = {
11489
+ includeNotes: options.includeNotes ?? false
11490
+ };
11491
+ const now = Math.floor(Date.now() / 1e3);
11492
+ const folders = [
11493
+ { id: UNSORTED_ID, name: "Vessel Bookmarks", createdAt: "", summary: "" },
11494
+ ...current.folders
11495
+ ];
11496
+ const lines = [
11497
+ NETSCAPE_BOOKMARKS_DOCTYPE,
11498
+ '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">',
11499
+ "<TITLE>Bookmarks</TITLE>",
11500
+ "<H1>Bookmarks</H1>",
11501
+ "<DL><p>"
11502
+ ];
11503
+ for (const folder of folders) {
11504
+ const items = current.bookmarks.filter(
11505
+ (bookmark) => bookmark.folderId === folder.id
11506
+ );
11507
+ if (items.length === 0) continue;
11508
+ const addDate = toNetscapeTimestamp(folder.createdAt) || now;
11509
+ lines.push(
11510
+ ` <DT><H3 ADD_DATE="${addDate}" LAST_MODIFIED="${now}">${escapeBookmarkHtml(folder.name)}</H3>`
11511
+ );
11512
+ if (resolvedOptions.includeNotes && folder.summary) {
11513
+ lines.push(` <DD>${escapeBookmarkHtml(folder.summary)}`);
11514
+ }
11515
+ lines.push(" <DL><p>");
11516
+ for (const bookmark of items) {
11517
+ appendBookmarkHtml(lines, bookmark, resolvedOptions, " ");
11518
+ }
11519
+ lines.push(" </DL><p>");
11520
+ }
11521
+ lines.push("</DL><p>");
11522
+ return `${lines.join("\n")}
11523
+ `;
11524
+ }
11525
+ function exportBookmarksJson() {
11526
+ return `${JSON.stringify(getState(), null, 2)}
11527
+ `;
11528
+ }
11012
11529
  function subscribe(listener) {
11013
11530
  listeners.add(listener);
11014
11531
  return () => {
@@ -11279,7 +11796,7 @@ function renameFolder(id, newName, summary) {
11279
11796
  return { ...folder };
11280
11797
  }
11281
11798
  function flushPersist$1() {
11282
- return persistence$1.flush();
11799
+ return getPersistence().flush();
11283
11800
  }
11284
11801
  function normalizeText(text) {
11285
11802
  return text?.trim() ?? "";
@@ -11603,7 +12120,7 @@ function formatDeadLinkMessage(label, result) {
11603
12120
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
11604
12121
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
11605
12122
  }
11606
- const logger$b = createLogger("Screenshot");
12123
+ const logger$c = createLogger("Screenshot");
11607
12124
  const SCREENSHOT_RETRY_COUNT = 3;
11608
12125
  const SCREENSHOT_RETRY_BASE_DELAY_MS = 120;
11609
12126
  async function captureScreenshot(wc) {
@@ -11625,7 +12142,7 @@ async function captureScreenshot(wc) {
11625
12142
  }
11626
12143
  }
11627
12144
  } catch (err) {
11628
- logger$b.debug(
12145
+ logger$c.debug(
11629
12146
  `capturePage attempt ${attempt + 1} failed; retrying if attempts remain.`,
11630
12147
  getErrorMessage(err)
11631
12148
  );
@@ -12499,7 +13016,7 @@ function buildHuggingFaceSearchShortcut(currentUrl, rawQuery) {
12499
13016
  appliedFilters
12500
13017
  };
12501
13018
  }
12502
- const logger$a = createLogger("PageActions");
13019
+ const logger$b = createLogger("PageActions");
12503
13020
  function getBookmarkMetadataFromArgs(args) {
12504
13021
  return normalizeBookmarkMetadata({
12505
13022
  intent: args.intent ?? args.intent,
@@ -12685,7 +13202,7 @@ async function executePageScript(wc, script, options) {
12685
13202
  return result;
12686
13203
  } catch (err) {
12687
13204
  const label = options?.label ? ` (${options.label})` : "";
12688
- logger$a.warn(`Failed to execute page script${label}:`, err);
13205
+ logger$b.warn(`Failed to execute page script${label}:`, err);
12689
13206
  return null;
12690
13207
  } finally {
12691
13208
  if (timer) {
@@ -12786,7 +13303,7 @@ Search results snapshot:
12786
13303
  ${truncated}`;
12787
13304
  }
12788
13305
  } catch (err) {
12789
- logger$a.warn("Failed to build post-search summary, falling back to nav summary:", err);
13306
+ logger$b.warn("Failed to build post-search summary, falling back to nav summary:", err);
12790
13307
  }
12791
13308
  const fallback = await getPostNavSummary(wc);
12792
13309
  return fallback ? `${fallback}
@@ -12809,7 +13326,7 @@ Page snapshot after navigation:
12809
13326
  ${truncated}`;
12810
13327
  }
12811
13328
  } catch (err) {
12812
- logger$a.warn("Failed to build post-click navigation summary:", err);
13329
+ logger$b.warn("Failed to build post-click navigation summary:", err);
12813
13330
  }
12814
13331
  return "";
12815
13332
  }
@@ -13303,7 +13820,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
13303
13820
  }
13304
13821
  }
13305
13822
  } catch (err) {
13306
- logger$a.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
13823
+ logger$b.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
13307
13824
  }
13308
13825
  if (snapshot.url && snapshot.url !== wc.getURL()) {
13309
13826
  try {
@@ -13312,7 +13829,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
13312
13829
  await waitForLoad(wc, 3e3);
13313
13830
  return;
13314
13831
  } catch (err) {
13315
- logger$a.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
13832
+ logger$b.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
13316
13833
  }
13317
13834
  }
13318
13835
  if (snapshot.url) {
@@ -13320,7 +13837,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
13320
13837
  await wc.reload();
13321
13838
  await waitForLoad(wc, 3e3);
13322
13839
  } catch (err) {
13323
- logger$a.warn("Failed to restore locale via page reload:", err);
13840
+ logger$b.warn("Failed to restore locale via page reload:", err);
13324
13841
  }
13325
13842
  }
13326
13843
  }
@@ -13672,7 +14189,7 @@ ${postActivationOverlayHint}`;
13672
14189
  return `${clickText} -> ${hrefFallbackUrl} (recovered via href fallback)`;
13673
14190
  }
13674
14191
  } catch (err) {
13675
- logger$a.warn("Failed href fallback after click, returning generic click result:", err);
14192
+ logger$b.warn("Failed href fallback after click, returning generic click result:", err);
13676
14193
  }
13677
14194
  }
13678
14195
  }
@@ -13717,7 +14234,7 @@ async function tryAutoDismissCartDialog(wc) {
13717
14234
  return result;
13718
14235
  }
13719
14236
  } catch (err) {
13720
- logger$a.warn("Failed to auto-dismiss cart dialog, falling back to dialog actions:", err);
14237
+ logger$b.warn("Failed to auto-dismiss cart dialog, falling back to dialog actions:", err);
13721
14238
  }
13722
14239
  return null;
13723
14240
  }
@@ -15987,7 +16504,7 @@ async function executeAction(name, args, ctx) {
15987
16504
  )
15988
16505
  ]);
15989
16506
  } catch (err) {
15990
- logger$a.warn("Failed to extract content for read_page, falling back to lighter recovery:", err);
16507
+ logger$b.warn("Failed to extract content for read_page, falling back to lighter recovery:", err);
15991
16508
  content = null;
15992
16509
  }
15993
16510
  if (!content || content.content.length === 0) {
@@ -16004,12 +16521,12 @@ async function executeAction(name, args, ctx) {
16004
16521
  new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
16005
16522
  ]);
16006
16523
  } catch (err) {
16007
- logger$a.warn("Failed to re-extract content after iframe consent dismissal:", err);
16524
+ logger$b.warn("Failed to re-extract content after iframe consent dismissal:", err);
16008
16525
  content = null;
16009
16526
  }
16010
16527
  }
16011
16528
  } catch (err) {
16012
- logger$a.warn("Failed iframe consent dismissal during read_page recovery:", err);
16529
+ logger$b.warn("Failed iframe consent dismissal during read_page recovery:", err);
16013
16530
  }
16014
16531
  }
16015
16532
  if (content && content.content.length > 0) {
@@ -16422,7 +16939,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
16422
16939
  try {
16423
16940
  page = await extractContent(wc);
16424
16941
  } catch (err) {
16425
- logger$a.warn("Failed to extract content for suggest:", err);
16942
+ logger$b.warn("Failed to extract content for suggest:", err);
16426
16943
  return "Could not read page. Try navigate to a working URL.";
16427
16944
  }
16428
16945
  const suggestions = [];
@@ -17753,7 +18270,7 @@ const ALGORITHM = "aes-256-gcm";
17753
18270
  const IV_LENGTH = 12;
17754
18271
  const AUTH_TAG_LENGTH = 16;
17755
18272
  let cachedEntries = null;
17756
- const logger$9 = createLogger("Vault");
18273
+ const logger$a = createLogger("Vault");
17757
18274
  function getVaultDir() {
17758
18275
  return electron.app.getPath("userData");
17759
18276
  }
@@ -17820,7 +18337,7 @@ function loadVault() {
17820
18337
  cachedEntries = JSON.parse(json);
17821
18338
  return cachedEntries;
17822
18339
  } catch (err) {
17823
- logger$9.error("Failed to load vault:", err);
18340
+ logger$a.error("Failed to load vault:", err);
17824
18341
  throw new Error(
17825
18342
  "Could not unlock the Agent Credential Vault. Check that OS secret storage is available and that the stored vault key can be decrypted."
17826
18343
  );
@@ -17962,7 +18479,7 @@ async function requestConsent(request) {
17962
18479
  }
17963
18480
  const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
17964
18481
  const MAX_ENTRIES = 1e3;
17965
- const logger$8 = createLogger("VaultAudit");
18482
+ const logger$9 = createLogger("VaultAudit");
17966
18483
  function getAuditPath() {
17967
18484
  return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
17968
18485
  }
@@ -17972,7 +18489,7 @@ function appendAuditEntry(entry) {
17972
18489
  fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
17973
18490
  fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
17974
18491
  } catch (err) {
17975
- logger$8.error("Failed to write audit log:", err);
18492
+ logger$9.error("Failed to write audit log:", err);
17976
18493
  }
17977
18494
  }
17978
18495
  function readAuditLog(limit = 100) {
@@ -17982,13 +18499,13 @@ function readAuditLog(limit = 100) {
17982
18499
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
17983
18500
  return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
17984
18501
  } catch (err) {
17985
- logger$8.error("Failed to read audit log:", err);
18502
+ logger$9.error("Failed to read audit log:", err);
17986
18503
  return [];
17987
18504
  }
17988
18505
  }
17989
18506
  let httpServer = null;
17990
18507
  let mcpAuthToken = null;
17991
- const logger$7 = createLogger("MCP");
18508
+ const logger$8 = createLogger("MCP");
17992
18509
  const MCP_AUTH_FILENAME = "mcp-auth.json";
17993
18510
  function getMcpAuthFilePath() {
17994
18511
  const configDir = process.env.VESSEL_CONFIG_DIR || path$1.join(
@@ -18024,7 +18541,7 @@ function writeMcpAuthFile(endpoint, token) {
18024
18541
  { mode: 384 }
18025
18542
  );
18026
18543
  } catch (err) {
18027
- logger$7.warn("Failed to write auth file:", err);
18544
+ logger$8.warn("Failed to write auth file:", err);
18028
18545
  }
18029
18546
  }
18030
18547
  function clearMcpAuthFile() {
@@ -18049,7 +18566,7 @@ function clearMcpAuthFile() {
18049
18566
  { mode: 384 }
18050
18567
  );
18051
18568
  } catch (err) {
18052
- logger$7.warn("Failed to clear auth file:", err);
18569
+ logger$8.warn("Failed to clear auth file:", err);
18053
18570
  }
18054
18571
  }
18055
18572
  function asTextResponse(text) {
@@ -18139,7 +18656,7 @@ async function getPostActionState(tabManager, name) {
18139
18656
  }
18140
18657
  }
18141
18658
  } catch (err) {
18142
- logger$7.warn("Failed to compute post-action state warning:", err);
18659
+ logger$8.warn("Failed to compute post-action state warning:", err);
18143
18660
  }
18144
18661
  return `${warning}
18145
18662
  [state: url=${wc.getURL()}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]`;
@@ -18241,7 +18758,7 @@ async function waitForConditionMcp(wc, text, selector, timeoutMs) {
18241
18758
  }
18242
18759
  })()
18243
18760
  `).catch((err) => {
18244
- logger$7.warn("Failed to gather wait_for timeout diagnostic:", err);
18761
+ logger$8.warn("Failed to gather wait_for timeout diagnostic:", err);
18245
18762
  return null;
18246
18763
  });
18247
18764
  if (typeof diagnostic === "string" && diagnostic.trim()) {
@@ -18328,7 +18845,7 @@ function registerTools(server, tabManager, runtime2) {
18328
18845
  const page = await extractContent(wc);
18329
18846
  pageType = detectPageType(page);
18330
18847
  } catch (err) {
18331
- logger$7.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
18848
+ logger$8.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
18332
18849
  }
18333
18850
  }
18334
18851
  const scored = TOOL_DEFINITIONS.map((def) => {
@@ -19234,54 +19751,168 @@ ${buildScopedContext(pageContent, mode)}`;
19234
19751
  })
19235
19752
  );
19236
19753
  server.registerTool(
19237
- "checkpoint_create",
19754
+ "list_groups",
19238
19755
  {
19239
- title: "Create Checkpoint",
19240
- description: "Capture the current session as a named checkpoint.",
19756
+ title: "List Tab Groups",
19757
+ description: "List all tab groups with their IDs, names, colors, collapsed state, and member tab count."
19758
+ },
19759
+ async () => {
19760
+ const groups = tabManager.getGroups();
19761
+ const tabs = tabManager.getAllStates();
19762
+ if (groups.length === 0) {
19763
+ return asTextResponse("No tab groups");
19764
+ }
19765
+ const lines = groups.map((g) => {
19766
+ const count = tabs.filter((t) => t.groupId === g.id).length;
19767
+ return `[${g.id}] ${g.name} — color:${g.color} collapsed:${g.collapsed} tabs:${count}`;
19768
+ });
19769
+ return asTextResponse(lines.join("\n"));
19770
+ }
19771
+ );
19772
+ server.registerTool(
19773
+ "create_group",
19774
+ {
19775
+ title: "Create Tab Group",
19776
+ description: "Create a new tab group from the active tab or a specified tab. Optionally provide a name and color.",
19241
19777
  inputSchema: {
19242
- name: zod.z.string().optional().describe("Optional checkpoint name"),
19243
- note: zod.z.string().optional().describe("Optional note")
19778
+ tabId: zod.z.string().optional().describe("Tab ID to group (defaults to active tab)"),
19779
+ name: zod.z.string().optional().describe("Optional group name"),
19780
+ color: zod.z.enum(["blue", "green", "yellow", "orange", "red", "purple", "gray"]).optional().describe("Optional group color")
19244
19781
  }
19245
19782
  },
19246
- async ({ name, note }) => withAction(
19247
- runtime2,
19248
- tabManager,
19249
- "create_checkpoint",
19250
- { name, note },
19251
- async () => {
19252
- const checkpoint = runtime2.createCheckpoint(name, note);
19253
- return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
19783
+ async ({ tabId, name, color }) => withAction(runtime2, tabManager, "create_group", { tabId, name, color }, async () => {
19784
+ const targetId = tabId || tabManager.getActiveTabId();
19785
+ if (!targetId) {
19786
+ return "Error: No active tab";
19787
+ }
19788
+ const groupId = tabManager.createGroupFromTab(targetId, {
19789
+ name: name || void 0,
19790
+ color: color || void 0
19791
+ });
19792
+ if (!groupId) {
19793
+ return "Error: Could not create group";
19254
19794
  }
19255
- )
19795
+ return `Created group ${groupId}`;
19796
+ })
19256
19797
  );
19257
19798
  server.registerTool(
19258
- "checkpoint_restore",
19799
+ "assign_to_group",
19259
19800
  {
19260
- title: "Restore Checkpoint",
19261
- description: "Restore a saved checkpoint by ID or exact name.",
19801
+ title: "Assign Tab to Group",
19802
+ description: "Move a tab into an existing group by ID. Defaults to the active tab.",
19262
19803
  inputSchema: {
19263
- checkpointId: zod.z.string().optional().describe("Checkpoint ID"),
19264
- name: zod.z.string().optional().describe("Exact checkpoint name")
19804
+ groupId: zod.z.string().describe("Group ID to assign the tab to"),
19805
+ tabId: zod.z.string().optional().describe("Tab ID to move (defaults to active tab)")
19265
19806
  }
19266
19807
  },
19267
- async ({ checkpointId, name }) => withAction(
19268
- runtime2,
19269
- tabManager,
19270
- "restore_checkpoint",
19271
- { checkpointId, name },
19272
- async () => {
19273
- const state2 = runtime2.getState();
19274
- const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
19275
- if (!checkpoint) {
19276
- return "Error: No matching checkpoint found";
19277
- }
19278
- runtime2.restoreCheckpoint(checkpoint.id);
19279
- return `Restored checkpoint ${checkpoint.name}`;
19808
+ async ({ groupId, tabId }) => withAction(runtime2, tabManager, "assign_to_group", { groupId, tabId }, async () => {
19809
+ const targetId = tabId || tabManager.getActiveTabId();
19810
+ if (!targetId) {
19811
+ return "Error: No active tab";
19280
19812
  }
19281
- )
19813
+ tabManager.assignTabToGroup(targetId, groupId);
19814
+ return `Assigned tab ${targetId} to group ${groupId}`;
19815
+ })
19282
19816
  );
19283
19817
  server.registerTool(
19284
- "save_session",
19818
+ "remove_from_group",
19819
+ {
19820
+ title: "Remove Tab from Group",
19821
+ description: "Ungroup a tab. Defaults to the active tab.",
19822
+ inputSchema: {
19823
+ tabId: zod.z.string().optional().describe("Tab ID to ungroup (defaults to active tab)")
19824
+ }
19825
+ },
19826
+ async ({ tabId }) => withAction(runtime2, tabManager, "remove_from_group", { tabId }, async () => {
19827
+ const targetId = tabId || tabManager.getActiveTabId();
19828
+ if (!targetId) {
19829
+ return "Error: No active tab";
19830
+ }
19831
+ tabManager.removeTabFromGroup(targetId);
19832
+ return `Removed tab ${targetId} from group`;
19833
+ })
19834
+ );
19835
+ server.registerTool(
19836
+ "toggle_group",
19837
+ {
19838
+ title: "Toggle Group Collapsed",
19839
+ description: "Collapse or expand a tab group.",
19840
+ inputSchema: {
19841
+ groupId: zod.z.string().describe("Group ID to toggle")
19842
+ }
19843
+ },
19844
+ async ({ groupId }) => withAction(runtime2, tabManager, "toggle_group", { groupId }, async () => {
19845
+ const collapsed = tabManager.toggleGroupCollapsed(groupId);
19846
+ if (collapsed === null) {
19847
+ return "Error: Group not found";
19848
+ }
19849
+ return collapsed ? `Collapsed group ${groupId}` : `Expanded group ${groupId}`;
19850
+ })
19851
+ );
19852
+ server.registerTool(
19853
+ "set_group_color",
19854
+ {
19855
+ title: "Set Group Color",
19856
+ description: "Change the color of a tab group.",
19857
+ inputSchema: {
19858
+ groupId: zod.z.string().describe("Group ID"),
19859
+ color: zod.z.enum(["blue", "green", "yellow", "orange", "red", "purple", "gray"]).describe("New color")
19860
+ }
19861
+ },
19862
+ async ({ groupId, color }) => withAction(runtime2, tabManager, "set_group_color", { groupId, color }, async () => {
19863
+ tabManager.setGroupColor(groupId, color);
19864
+ return `Set group ${groupId} color to ${color}`;
19865
+ })
19866
+ );
19867
+ server.registerTool(
19868
+ "checkpoint_create",
19869
+ {
19870
+ title: "Create Checkpoint",
19871
+ description: "Capture the current session as a named checkpoint.",
19872
+ inputSchema: {
19873
+ name: zod.z.string().optional().describe("Optional checkpoint name"),
19874
+ note: zod.z.string().optional().describe("Optional note")
19875
+ }
19876
+ },
19877
+ async ({ name, note }) => withAction(
19878
+ runtime2,
19879
+ tabManager,
19880
+ "create_checkpoint",
19881
+ { name, note },
19882
+ async () => {
19883
+ const checkpoint = runtime2.createCheckpoint(name, note);
19884
+ return `Created checkpoint ${checkpoint.name} (${checkpoint.id})`;
19885
+ }
19886
+ )
19887
+ );
19888
+ server.registerTool(
19889
+ "checkpoint_restore",
19890
+ {
19891
+ title: "Restore Checkpoint",
19892
+ description: "Restore a saved checkpoint by ID or exact name.",
19893
+ inputSchema: {
19894
+ checkpointId: zod.z.string().optional().describe("Checkpoint ID"),
19895
+ name: zod.z.string().optional().describe("Exact checkpoint name")
19896
+ }
19897
+ },
19898
+ async ({ checkpointId, name }) => withAction(
19899
+ runtime2,
19900
+ tabManager,
19901
+ "restore_checkpoint",
19902
+ { checkpointId, name },
19903
+ async () => {
19904
+ const state2 = runtime2.getState();
19905
+ const checkpoint = state2.checkpoints.find((item) => item.id === checkpointId) || state2.checkpoints.find((item) => item.name === name);
19906
+ if (!checkpoint) {
19907
+ return "Error: No matching checkpoint found";
19908
+ }
19909
+ runtime2.restoreCheckpoint(checkpoint.id);
19910
+ return `Restored checkpoint ${checkpoint.name}`;
19911
+ }
19912
+ )
19913
+ );
19914
+ server.registerTool(
19915
+ "save_session",
19285
19916
  {
19286
19917
  title: "Save Session",
19287
19918
  description: "Persist the current cookies, localStorage, and tab layout under a reusable session name.",
@@ -19612,7 +20243,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
19612
20243
  void 0,
19613
20244
  h.color
19614
20245
  ).catch(
19615
- (err) => logger$7.warn("Failed to restore highlight after removal:", err)
20246
+ (err) => logger$8.warn("Failed to restore highlight after removal:", err)
19616
20247
  );
19617
20248
  }
19618
20249
  }
@@ -20460,7 +21091,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
20460
21091
  try {
20461
21092
  page = await extractContent(wc);
20462
21093
  } catch (err) {
20463
- logger$7.warn("Failed to extract page while generating suggestions:", err);
21094
+ logger$8.warn("Failed to extract page while generating suggestions:", err);
20464
21095
  return asTextResponse(
20465
21096
  "Could not read page. Try navigate to a working URL."
20466
21097
  );
@@ -21069,7 +21700,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
21069
21700
  try {
21070
21701
  targetDomain = new URL(tab.state.url).hostname;
21071
21702
  } catch (err) {
21072
- logger$7.warn("Failed to parse active tab URL for vault_status:", err);
21703
+ logger$8.warn("Failed to parse active tab URL for vault_status:", err);
21073
21704
  return asErrorTextResponse("Could not parse active tab URL");
21074
21705
  }
21075
21706
  }
@@ -21135,7 +21766,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
21135
21766
  try {
21136
21767
  hostname = new URL(tab.state.url).hostname;
21137
21768
  } catch (err) {
21138
- logger$7.warn("Failed to parse active tab URL for vault_login:", err);
21769
+ logger$8.warn("Failed to parse active tab URL for vault_login:", err);
21139
21770
  return asErrorTextResponse("Could not parse active tab URL");
21140
21771
  }
21141
21772
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -21229,7 +21860,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
21229
21860
  try {
21230
21861
  hostname = new URL(tab.state.url).hostname;
21231
21862
  } catch (err) {
21232
- logger$7.warn("Failed to parse active tab URL for vault_totp:", err);
21863
+ logger$8.warn("Failed to parse active tab URL for vault_totp:", err);
21233
21864
  return asErrorTextResponse("Could not parse active tab URL");
21234
21865
  }
21235
21866
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -21401,7 +22032,7 @@ function startMcpServer(tabManager, runtime2, port) {
21401
22032
  await mcpServer.connect(transport);
21402
22033
  await transport.handleRequest(req, res);
21403
22034
  } catch (error) {
21404
- logger$7.error("Error handling request:", error);
22035
+ logger$8.error("Error handling request:", error);
21405
22036
  if (!res.headersSent) {
21406
22037
  res.writeHead(500, { "Content-Type": "application/json" });
21407
22038
  res.end(
@@ -21420,7 +22051,7 @@ function startMcpServer(tabManager, runtime2, port) {
21420
22051
  };
21421
22052
  server.once("error", (error) => {
21422
22053
  const message = error.code === "EADDRINUSE" ? `Port ${port} is already in use. MCP server not started.` : error.message;
21423
- logger$7.error("Server error:", error);
22054
+ logger$8.error("Server error:", error);
21424
22055
  clearMcpAuthFile();
21425
22056
  setMcpHealth({
21426
22057
  configuredPort: port,
@@ -21452,7 +22083,7 @@ function startMcpServer(tabManager, runtime2, port) {
21452
22083
  message: `MCP server listening on ${endpoint}.`
21453
22084
  });
21454
22085
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
21455
- logger$7.info(`Server listening on ${endpoint} (auth enabled)`);
22086
+ logger$8.info(`Server listening on ${endpoint} (auth enabled)`);
21456
22087
  }
21457
22088
  if (mcpAuthToken) {
21458
22089
  writeMcpAuthFile(endpoint, mcpAuthToken);
@@ -21491,7 +22122,7 @@ function stopMcpServer() {
21491
22122
  message: "MCP server is stopped."
21492
22123
  });
21493
22124
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
21494
- logger$7.info("Server stopped");
22125
+ logger$8.info("Server stopped");
21495
22126
  }
21496
22127
  resolve();
21497
22128
  });
@@ -21512,7 +22143,7 @@ const KIT_ID_UNSAFE_CHAR_PATTERN = /[\/\\\0]/;
21512
22143
  function isSafeAutomationKitId(id) {
21513
22144
  return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
21514
22145
  }
21515
- const logger$6 = createLogger("KitRegistry");
22146
+ const logger$7 = createLogger("KitRegistry");
21516
22147
  function getUserKitsDir() {
21517
22148
  return path$1.join(electron.app.getPath("userData"), "kits");
21518
22149
  }
@@ -21550,10 +22181,10 @@ function getInstalledKits() {
21550
22181
  if (isValidKit(parsed)) {
21551
22182
  kits.push(parsed);
21552
22183
  } else {
21553
- logger$6.warn(`Skipping invalid kit file: ${file}`);
22184
+ logger$7.warn(`Skipping invalid kit file: ${file}`);
21554
22185
  }
21555
22186
  } catch (err) {
21556
- logger$6.warn(`Failed to read kit file: ${file}`, err);
22187
+ logger$7.warn(`Failed to read kit file: ${file}`, err);
21557
22188
  }
21558
22189
  }
21559
22190
  return kits;
@@ -21625,7 +22256,7 @@ function uninstallKit(id, scheduledKitIds) {
21625
22256
  return errorResult("Failed to remove the kit file.");
21626
22257
  }
21627
22258
  }
21628
- const logger$5 = createLogger("Scheduler");
22259
+ const logger$6 = createLogger("Scheduler");
21629
22260
  let jobs = [];
21630
22261
  let removeIdleListener = null;
21631
22262
  let broadcastFn = null;
@@ -21650,7 +22281,7 @@ function saveJobs() {
21650
22281
  try {
21651
22282
  fs$1.writeFileSync(getJobsPath(), JSON.stringify(jobs, null, 2), "utf-8");
21652
22283
  } catch (err) {
21653
- logger$5.warn("Failed to save jobs:", err);
22284
+ logger$6.warn("Failed to save jobs:", err);
21654
22285
  }
21655
22286
  }
21656
22287
  function normalizeJob(job, now = /* @__PURE__ */ new Date()) {
@@ -21772,7 +22403,7 @@ async function fireJob(job, windowState, runtime2) {
21772
22403
  };
21773
22404
  startActivity();
21774
22405
  if (!settings2.chatProvider) {
21775
- logger$5.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
22406
+ logger$6.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
21776
22407
  appendActivity(
21777
22408
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
21778
22409
  );
@@ -21780,7 +22411,7 @@ async function fireJob(job, windowState, runtime2) {
21780
22411
  return;
21781
22412
  }
21782
22413
  if (process.env.VESSEL_DEBUG_SCHEDULER === "1" || process.env.VESSEL_DEBUG_SCHEDULER === "true") {
21783
- logger$5.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
22414
+ logger$6.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
21784
22415
  }
21785
22416
  try {
21786
22417
  const provider = createProvider(settings2.chatProvider);
@@ -21833,7 +22464,7 @@ function tick(windowState, runtime2) {
21833
22464
  saveJobs();
21834
22465
  broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
21835
22466
  void fireJob(job, windowState, runtime2).catch((err) => {
21836
- logger$5.warn("Unexpected error firing job:", err);
22467
+ logger$6.warn("Unexpected error firing job:", err);
21837
22468
  }).finally(fireNext);
21838
22469
  };
21839
22470
  fireNext();
@@ -22403,96 +23034,1078 @@ function registerWindowControlHandlers(mainWindow) {
22403
23034
  mainWindow.close();
22404
23035
  });
22405
23036
  }
22406
- let activeChatProvider = null;
22407
- const logger$4 = createLogger("IPC");
22408
- const VALID_APPROVAL_MODES = ["auto", "confirm-dangerous", "manual"];
22409
- function registerIpcHandlers(windowState, runtime2) {
22410
- const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
22411
- let sidebarResizeRecoveryTimer = null;
22412
- let sidebarResizeActive = false;
22413
- let runtimeUpdateTimer = null;
22414
- let pendingRuntimeState = null;
22415
- const premiumApiOrigin = process.env.VESSEL_PREMIUM_API ? new URL(process.env.VESSEL_PREMIUM_API).origin : "https://vesselpremium.quantaintellect.com";
22416
- const clearSidebarResizeRecoveryTimer = () => {
22417
- if (!sidebarResizeRecoveryTimer) return;
22418
- clearTimeout(sidebarResizeRecoveryTimer);
22419
- sidebarResizeRecoveryTimer = null;
22420
- };
22421
- const restoreSidebarLayoutAfterResize = () => {
22422
- clearSidebarResizeRecoveryTimer();
22423
- if (!sidebarResizeActive) return;
22424
- sidebarResizeActive = false;
22425
- layoutViews(windowState);
22426
- };
22427
- const scheduleSidebarResizeRecovery = () => {
22428
- clearSidebarResizeRecoveryTimer();
22429
- sidebarResizeRecoveryTimer = setTimeout(() => {
22430
- restoreSidebarLayoutAfterResize();
22431
- }, 1200);
22432
- };
22433
- const flushRuntimeUpdate = () => {
22434
- runtimeUpdateTimer = null;
22435
- if (!pendingRuntimeState) return;
22436
- if (!chromeView.webContents.isDestroyed()) {
22437
- chromeView.webContents.send(
22438
- Channels.AGENT_RUNTIME_UPDATE,
22439
- pendingRuntimeState
22440
- );
23037
+ const BLOCKED_RESOURCE_TYPES = /* @__PURE__ */ new Set([
23038
+ "script",
23039
+ "image",
23040
+ "xhr",
23041
+ "fetch",
23042
+ "subFrame",
23043
+ "media",
23044
+ "ping",
23045
+ "webSocket"
23046
+ ]);
23047
+ const BLOCKED_HOST_SUFFIXES = [
23048
+ "doubleclick.net",
23049
+ "googlesyndication.com",
23050
+ "googleadservices.com",
23051
+ "adservice.google.com",
23052
+ "adnxs.com",
23053
+ "adsrvr.org",
23054
+ "taboola.com",
23055
+ "outbrain.com",
23056
+ "criteo.com",
23057
+ "criteo.net",
23058
+ "pubmatic.com",
23059
+ "rubiconproject.com",
23060
+ "openx.net",
23061
+ "casalemedia.com",
23062
+ "advertising.com",
23063
+ "amazon-adsystem.com",
23064
+ "adsymptotic.com",
23065
+ "moatads.com",
23066
+ "quantserve.com",
23067
+ "scorecardresearch.com"
23068
+ ];
23069
+ const THIRD_PARTY_PATH_PATTERNS = [
23070
+ /\/ads?[/?._-]/i,
23071
+ /\/adservice/i,
23072
+ /\/advert/i,
23073
+ /\/prebid/i,
23074
+ /\/banner/i,
23075
+ /\/sponsor/i,
23076
+ /\/promotions?\//i,
23077
+ /\/trk\//i,
23078
+ /\/track(ing)?\//i,
23079
+ /\/beacon/i,
23080
+ /\/pixel/i
23081
+ ];
23082
+ let installed = false;
23083
+ const defaultSessionTabManagers = /* @__PURE__ */ new Set();
23084
+ function normalizeHostname(value) {
23085
+ return value.trim().toLowerCase().replace(/\.$/, "");
23086
+ }
23087
+ function hostnameMatches(hostname, suffix) {
23088
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
23089
+ }
23090
+ function parseHostname(url) {
23091
+ try {
23092
+ const parsed = new URL(url);
23093
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23094
+ return null;
22441
23095
  }
22442
- if (!sidebarView.webContents.isDestroyed()) {
22443
- sidebarView.webContents.send(
22444
- Channels.AGENT_RUNTIME_UPDATE,
22445
- pendingRuntimeState
22446
- );
23096
+ return normalizeHostname(parsed.hostname);
23097
+ } catch {
23098
+ return null;
23099
+ }
23100
+ }
23101
+ function isThirdParty(url, firstPartyHost) {
23102
+ if (!firstPartyHost) return true;
23103
+ const target = normalizeHostname(url.hostname);
23104
+ return !(target === firstPartyHost || target.endsWith(`.${firstPartyHost}`));
23105
+ }
23106
+ function shouldBlockRequest(details) {
23107
+ if (!BLOCKED_RESOURCE_TYPES.has(details.resourceType)) return false;
23108
+ let parsed;
23109
+ try {
23110
+ parsed = new URL(details.url);
23111
+ } catch {
23112
+ return false;
23113
+ }
23114
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23115
+ return false;
23116
+ }
23117
+ const hostname = normalizeHostname(parsed.hostname);
23118
+ if (BLOCKED_HOST_SUFFIXES.some((suffix) => hostnameMatches(hostname, suffix))) {
23119
+ return true;
23120
+ }
23121
+ const firstPartyHost = parseHostname(details.referrer) || parseHostname(details.initiator || "");
23122
+ if (!isThirdParty(parsed, firstPartyHost)) return false;
23123
+ const candidate = `${hostname}${parsed.pathname}${parsed.search}`;
23124
+ return THIRD_PARTY_PATH_PATTERNS.some((pattern) => pattern.test(candidate));
23125
+ }
23126
+ function installAdBlocking(tabManager) {
23127
+ defaultSessionTabManagers.add(tabManager);
23128
+ if (installed) return;
23129
+ installed = true;
23130
+ electron.session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
23131
+ const webContentsId = typeof details.webContentsId === "number" ? details.webContentsId : null;
23132
+ if (webContentsId == null) {
23133
+ callback({});
23134
+ return;
22447
23135
  }
22448
- pendingRuntimeState = null;
22449
- };
22450
- const scheduleRuntimeUpdate = (state2) => {
22451
- pendingRuntimeState = state2;
22452
- if (runtimeUpdateTimer) return;
22453
- runtimeUpdateTimer = setTimeout(() => {
22454
- flushRuntimeUpdate();
22455
- }, 32);
22456
- };
22457
- electron.app.on("before-quit", () => {
22458
- if (runtimeUpdateTimer) {
22459
- clearTimeout(runtimeUpdateTimer);
22460
- runtimeUpdateTimer = null;
23136
+ const manager = [...defaultSessionTabManagers].find(
23137
+ (candidate) => candidate.findTabByWebContentsId(webContentsId)
23138
+ );
23139
+ if (!manager?.isAdBlockingEnabledForWebContents(webContentsId)) {
23140
+ callback({});
23141
+ return;
22461
23142
  }
22462
- flushRuntimeUpdate();
23143
+ callback({ cancel: shouldBlockRequest(details) });
22463
23144
  });
22464
- const sendToRendererViews = (channel, ...args) => {
22465
- chromeView.webContents.send(channel, ...args);
22466
- sidebarView.webContents.send(channel, ...args);
22467
- devtoolsPanelView.webContents.send(channel, ...args);
22468
- };
22469
- const watchPremiumCheckoutTab = (tabId) => {
22470
- const tab = tabManager.getTab(tabId);
22471
- const wc = tab?.view.webContents;
22472
- if (!wc) return;
22473
- let completed = false;
22474
- const cleanup = () => {
22475
- wc.removeListener("did-navigate", onNavigate);
22476
- wc.removeListener("did-navigate-in-page", onNavigateInPage);
22477
- wc.removeListener("destroyed", cleanup);
22478
- };
22479
- const handleUrl = async (rawUrl) => {
22480
- if (completed) return;
22481
- let parsed;
22482
- try {
22483
- parsed = new URL(rawUrl);
22484
- } catch (err) {
22485
- logger$4.warn("Failed to parse premium checkout URL while watching checkout tab:", err);
22486
- return;
22487
- }
22488
- if (parsed.origin !== premiumApiOrigin) return;
22489
- if (parsed.pathname === "/canceled") {
22490
- completed = true;
22491
- trackPremiumFunnel("checkout_canceled");
22492
- cleanup();
22493
- return;
22494
- }
22495
- if (parsed.pathname !== "/success") return;
23145
+ }
23146
+ function unregisterAdBlockingTabManager(tabManager) {
23147
+ defaultSessionTabManagers.delete(tabManager);
23148
+ }
23149
+ function installAdBlockingForSession(ses, tabManager) {
23150
+ ses.webRequest.onBeforeRequest((details, callback) => {
23151
+ const webContentsId = typeof details.webContentsId === "number" ? details.webContentsId : null;
23152
+ if (webContentsId == null) {
23153
+ callback({});
23154
+ return;
23155
+ }
23156
+ if (!tabManager.isAdBlockingEnabledForWebContents(webContentsId)) {
23157
+ callback({});
23158
+ return;
23159
+ }
23160
+ callback({ cancel: shouldBlockRequest(details) });
23161
+ });
23162
+ }
23163
+ const defaultDownloadViews = /* @__PURE__ */ new Set();
23164
+ let defaultDownloadHandlerInstalled = false;
23165
+ function resolveDownloadPath(downloadDir, filename) {
23166
+ fs$1.mkdirSync(downloadDir, { recursive: true });
23167
+ const parsed = path.parse(filename);
23168
+ let attempt = 0;
23169
+ while (true) {
23170
+ const candidateName = attempt === 0 ? filename : `${parsed.name} (${attempt})${parsed.ext}`;
23171
+ const candidatePath = path.join(downloadDir, candidateName);
23172
+ if (!fs$1.existsSync(candidatePath)) {
23173
+ return candidatePath;
23174
+ }
23175
+ attempt += 1;
23176
+ }
23177
+ }
23178
+ function installDownloadHandler(chromeView) {
23179
+ defaultDownloadViews.add(chromeView);
23180
+ if (defaultDownloadHandlerInstalled) return;
23181
+ defaultDownloadHandlerInstalled = true;
23182
+ installDownloadHandlerForSession(electron.session.defaultSession, defaultDownloadViews);
23183
+ }
23184
+ function unregisterDownloadHandler(chromeView) {
23185
+ defaultDownloadViews.delete(chromeView);
23186
+ }
23187
+ function installDownloadHandlerForSession(targetSession, chromeView) {
23188
+ const send = (channel, info) => {
23189
+ const views = chromeView instanceof electron.WebContentsView ? [chromeView] : [...chromeView];
23190
+ for (const view of views) {
23191
+ if (!view.webContents.isDestroyed()) {
23192
+ view.webContents.send(channel, info);
23193
+ }
23194
+ }
23195
+ };
23196
+ targetSession.on("will-download", (_event, item) => {
23197
+ const settings2 = loadSettings();
23198
+ const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
23199
+ const filename = item.getFilename();
23200
+ const savePath = resolveDownloadPath(downloadDir, filename);
23201
+ item.setSavePath(savePath);
23202
+ const info = {
23203
+ filename,
23204
+ savePath,
23205
+ totalBytes: item.getTotalBytes(),
23206
+ receivedBytes: 0,
23207
+ state: "progressing"
23208
+ };
23209
+ send(Channels.DOWNLOAD_STARTED, info);
23210
+ item.on("updated", (_event2, state2) => {
23211
+ info.receivedBytes = item.getReceivedBytes();
23212
+ info.totalBytes = item.getTotalBytes();
23213
+ info.state = state2 === "progressing" ? "progressing" : "interrupted";
23214
+ send(Channels.DOWNLOAD_PROGRESS, info);
23215
+ });
23216
+ item.once("done", (_event2, state2) => {
23217
+ info.receivedBytes = item.getReceivedBytes();
23218
+ info.state = state2 === "completed" ? "completed" : "cancelled";
23219
+ send(Channels.DOWNLOAD_DONE, info);
23220
+ });
23221
+ });
23222
+ }
23223
+ const logger$5 = createLogger("PrivateWindow");
23224
+ const CHROME_HEIGHT$1 = 110;
23225
+ const privateWindows = /* @__PURE__ */ new Set();
23226
+ function resolveRendererFile$2() {
23227
+ const candidates = [
23228
+ path.join(__dirname, "../../out/renderer/index.html"),
23229
+ path.join(__dirname, "../../../out/renderer/index.html")
23230
+ ];
23231
+ for (const c of candidates) {
23232
+ try {
23233
+ fs.accessSync(c);
23234
+ return c;
23235
+ } catch {
23236
+ }
23237
+ }
23238
+ return path.join(__dirname, "../../out/renderer/index.html");
23239
+ }
23240
+ function layoutPrivateViews(state2) {
23241
+ const { window: win, chromeView, tabManager } = state2;
23242
+ const [width, height] = win.getContentSize();
23243
+ chromeView.setBounds({ x: 0, y: 0, width, height: CHROME_HEIGHT$1 });
23244
+ win.contentView.removeChildView(chromeView);
23245
+ win.contentView.addChildView(chromeView);
23246
+ const activeTab = tabManager.getActiveTab();
23247
+ if (activeTab) {
23248
+ activeTab.view.setBounds({
23249
+ x: 0,
23250
+ y: CHROME_HEIGHT$1,
23251
+ width,
23252
+ height: height - CHROME_HEIGHT$1
23253
+ });
23254
+ }
23255
+ }
23256
+ function loadPrivateRenderer(chromeView) {
23257
+ const devUrl = process.env.ELECTRON_RENDERER_URL;
23258
+ if (devUrl) {
23259
+ const url = new URL(devUrl);
23260
+ url.searchParams.set("view", "chrome");
23261
+ url.searchParams.set("private", "1");
23262
+ chromeView.webContents.loadURL(url.toString());
23263
+ } else {
23264
+ chromeView.webContents.loadFile(resolveRendererFile$2(), {
23265
+ query: { view: "chrome", private: "1" }
23266
+ });
23267
+ }
23268
+ }
23269
+ function registerPrivateIpcHandlers(state2) {
23270
+ const { chromeView, tabManager } = state2;
23271
+ const ipc = chromeView.webContents.ipc;
23272
+ let findResultListener = null;
23273
+ let findWiredWcId = null;
23274
+ const wireFindEvents = (wc) => {
23275
+ if (findWiredWcId === wc.id && findResultListener) return;
23276
+ if (findWiredWcId && findResultListener) {
23277
+ const previous = tabManager.findTabByWebContentsId(findWiredWcId);
23278
+ previous?.view.webContents.removeListener(
23279
+ "found-in-page",
23280
+ findResultListener
23281
+ );
23282
+ }
23283
+ findWiredWcId = wc.id;
23284
+ if (wc.isDestroyed()) return;
23285
+ const listener = (_event, result) => {
23286
+ if (!chromeView.webContents.isDestroyed()) {
23287
+ chromeView.webContents.send(Channels.FIND_IN_PAGE_RESULT, result);
23288
+ }
23289
+ };
23290
+ findResultListener = listener;
23291
+ wc.on("found-in-page", listener);
23292
+ const capturedWcId = wc.id;
23293
+ wc.once("destroyed", () => {
23294
+ if (findWiredWcId === capturedWcId) {
23295
+ findWiredWcId = null;
23296
+ findResultListener = null;
23297
+ }
23298
+ });
23299
+ };
23300
+ ipc.handle(Channels.TAB_CREATE, (_e, url) => {
23301
+ return tabManager.createTab(url);
23302
+ });
23303
+ ipc.handle(Channels.TAB_CLOSE, (_e, id) => {
23304
+ tabManager.closeTab(id);
23305
+ layoutPrivateViews(state2);
23306
+ });
23307
+ ipc.handle(Channels.TAB_SWITCH, (_e, id) => {
23308
+ tabManager.switchTab(id);
23309
+ layoutPrivateViews(state2);
23310
+ });
23311
+ ipc.handle(Channels.TAB_NAVIGATE, (_e, id, url) => {
23312
+ return tabManager.navigateTab(id, url);
23313
+ });
23314
+ ipc.handle(Channels.TAB_BACK, (_e, id) => {
23315
+ tabManager.goBack(id);
23316
+ });
23317
+ ipc.handle(Channels.TAB_FORWARD, (_e, id) => {
23318
+ tabManager.goForward(id);
23319
+ });
23320
+ ipc.handle(Channels.TAB_RELOAD, (_e, id) => {
23321
+ tabManager.reloadTab(id);
23322
+ });
23323
+ ipc.handle(Channels.TAB_TOGGLE_AD_BLOCK, (_e, id) => {
23324
+ const tab = tabManager.getTab(id);
23325
+ if (!tab) return null;
23326
+ const newState = !tab.state.adBlockingEnabled;
23327
+ tab.setAdBlockingEnabled(newState);
23328
+ return newState;
23329
+ });
23330
+ ipc.handle(Channels.TAB_ZOOM_IN, (_e, id) => {
23331
+ tabManager.zoomIn(id);
23332
+ });
23333
+ ipc.handle(Channels.TAB_ZOOM_OUT, (_e, id) => {
23334
+ tabManager.zoomOut(id);
23335
+ });
23336
+ ipc.handle(Channels.TAB_ZOOM_RESET, (_e, id) => {
23337
+ tabManager.zoomReset(id);
23338
+ });
23339
+ ipc.handle(Channels.TAB_STATE_GET, () => ({
23340
+ tabs: tabManager.getAllStates(),
23341
+ activeId: tabManager.getActiveTabId() || ""
23342
+ }));
23343
+ ipc.handle(Channels.TAB_REOPEN_CLOSED, () => {
23344
+ const id = tabManager.reopenClosedTab();
23345
+ if (id) layoutPrivateViews(state2);
23346
+ return id;
23347
+ });
23348
+ ipc.handle(Channels.TAB_DUPLICATE, (_e, id) => {
23349
+ const newId = tabManager.duplicateTab(id);
23350
+ if (newId) layoutPrivateViews(state2);
23351
+ return newId;
23352
+ });
23353
+ ipc.handle(Channels.TAB_PIN, (_e, id) => {
23354
+ tabManager.pinTab(id);
23355
+ });
23356
+ ipc.handle(Channels.TAB_UNPIN, (_e, id) => {
23357
+ tabManager.unpinTab(id);
23358
+ });
23359
+ ipc.handle(Channels.TAB_GROUP_CREATE, (_e, id) => {
23360
+ return tabManager.createGroupFromTab(id);
23361
+ });
23362
+ ipc.handle(Channels.TAB_GROUP_ADD_TAB, (_e, id, groupId) => {
23363
+ tabManager.assignTabToGroup(id, groupId);
23364
+ });
23365
+ ipc.handle(Channels.TAB_GROUP_REMOVE_TAB, (_e, id) => {
23366
+ tabManager.removeTabFromGroup(id);
23367
+ });
23368
+ ipc.handle(Channels.TAB_GROUP_TOGGLE_COLLAPSED, (_e, groupId) => {
23369
+ return tabManager.toggleGroupCollapsed(groupId);
23370
+ });
23371
+ ipc.handle(
23372
+ Channels.TAB_GROUP_SET_COLOR,
23373
+ (_e, groupId, color) => {
23374
+ tabManager.setGroupColor(groupId, color);
23375
+ }
23376
+ );
23377
+ ipc.handle(Channels.TAB_TOGGLE_MUTE, (_e, id) => {
23378
+ return tabManager.toggleMuted(id);
23379
+ });
23380
+ ipc.handle(Channels.TAB_PRINT, (_e, id) => {
23381
+ tabManager.printTab(id);
23382
+ });
23383
+ ipc.handle(Channels.TAB_PRINT_TO_PDF, (_e, id) => {
23384
+ return tabManager.saveTabAsPdf(id);
23385
+ });
23386
+ ipc.on(Channels.TAB_CONTEXT_MENU, (_e, id) => {
23387
+ const { Menu, MenuItem } = require("electron");
23388
+ const tab = tabManager.getTab(id);
23389
+ const isPinned = tab?.state.isPinned ?? false;
23390
+ const groupId = tab?.state.groupId;
23391
+ const isMuted = tab?.state.isMuted ?? false;
23392
+ const groups = tabManager.getAllStates().filter((state22) => state22.groupId && state22.groupId !== groupId).reduce(
23393
+ (map, state22) => map.set(state22.groupId, {
23394
+ id: state22.groupId,
23395
+ name: state22.groupName || "Group"
23396
+ }),
23397
+ /* @__PURE__ */ new Map()
23398
+ );
23399
+ const menu = new Menu();
23400
+ menu.append(
23401
+ new MenuItem({
23402
+ label: isPinned ? "Unpin Tab" : "Pin Tab",
23403
+ click: () => {
23404
+ if (isPinned) {
23405
+ tabManager.unpinTab(id);
23406
+ } else {
23407
+ tabManager.pinTab(id);
23408
+ }
23409
+ }
23410
+ })
23411
+ );
23412
+ menu.append(
23413
+ new MenuItem({
23414
+ label: "Duplicate Tab",
23415
+ click: () => {
23416
+ const newId = tabManager.duplicateTab(id);
23417
+ if (newId) layoutPrivateViews(state2);
23418
+ }
23419
+ })
23420
+ );
23421
+ menu.append(
23422
+ new MenuItem({
23423
+ label: "Add to New Group",
23424
+ click: () => {
23425
+ tabManager.createGroupFromTab(id);
23426
+ }
23427
+ })
23428
+ );
23429
+ if (groups.size > 0) {
23430
+ menu.append(
23431
+ new MenuItem({
23432
+ label: "Add to Group",
23433
+ submenu: [...groups.values()].map(
23434
+ (group) => new MenuItem({
23435
+ label: group.name,
23436
+ click: () => tabManager.assignTabToGroup(id, group.id)
23437
+ })
23438
+ )
23439
+ })
23440
+ );
23441
+ }
23442
+ if (groupId) {
23443
+ menu.append(
23444
+ new MenuItem({
23445
+ label: "Remove from Group",
23446
+ click: () => {
23447
+ tabManager.removeTabFromGroup(id);
23448
+ }
23449
+ })
23450
+ );
23451
+ }
23452
+ menu.append(
23453
+ new MenuItem({
23454
+ label: isMuted ? "Unmute Tab" : "Mute Tab",
23455
+ click: () => {
23456
+ tabManager.toggleMuted(id);
23457
+ }
23458
+ })
23459
+ );
23460
+ menu.append(new MenuItem({ type: "separator" }));
23461
+ menu.append(
23462
+ new MenuItem({
23463
+ label: "Print Page",
23464
+ click: () => {
23465
+ tabManager.printTab(id);
23466
+ }
23467
+ })
23468
+ );
23469
+ menu.append(
23470
+ new MenuItem({
23471
+ label: "Save Page as PDF",
23472
+ click: () => {
23473
+ void tabManager.saveTabAsPdf(id).catch((error) => {
23474
+ logger$5.warn("Failed to save private page as PDF:", error);
23475
+ });
23476
+ }
23477
+ })
23478
+ );
23479
+ if (!isPinned) {
23480
+ menu.append(new MenuItem({ type: "separator" }));
23481
+ menu.append(
23482
+ new MenuItem({
23483
+ label: "Close Tab",
23484
+ click: () => {
23485
+ tabManager.closeTab(id);
23486
+ layoutPrivateViews(state2);
23487
+ }
23488
+ })
23489
+ );
23490
+ }
23491
+ menu.popup({ window: state2.window });
23492
+ });
23493
+ ipc.on(Channels.TAB_GROUP_CONTEXT_MENU, (_e, groupId) => {
23494
+ const { Menu, MenuItem } = require("electron");
23495
+ const firstTab = tabManager.getAllStates().find((tab) => tab.groupId === groupId);
23496
+ if (!firstTab) return;
23497
+ const menu = new Menu();
23498
+ menu.append(
23499
+ new MenuItem({
23500
+ label: firstTab.groupCollapsed ? "Expand Group" : "Collapse Group",
23501
+ click: () => tabManager.toggleGroupCollapsed(groupId)
23502
+ })
23503
+ );
23504
+ menu.append(
23505
+ new MenuItem({
23506
+ label: "Group Color",
23507
+ submenu: TAB_GROUP_COLORS.map(
23508
+ (color) => new MenuItem({
23509
+ label: TAB_GROUP_COLOR_LABELS[color],
23510
+ type: "radio",
23511
+ checked: firstTab.groupColor === color,
23512
+ click: () => tabManager.setGroupColor(groupId, color)
23513
+ })
23514
+ )
23515
+ })
23516
+ );
23517
+ menu.popup({ window: state2.window });
23518
+ });
23519
+ ipc.handle(Channels.IS_PRIVATE_MODE, () => true);
23520
+ ipc.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
23521
+ createPrivateWindow();
23522
+ });
23523
+ ipc.handle(Channels.OPEN_NEW_WINDOW, () => {
23524
+ const { createSecondaryWindow: createSecondaryWindow2 } = require("../secondary/window");
23525
+ createSecondaryWindow2();
23526
+ });
23527
+ ipc.handle(Channels.WINDOW_MINIMIZE, () => {
23528
+ state2.window.minimize();
23529
+ });
23530
+ ipc.handle(Channels.WINDOW_MAXIMIZE, () => {
23531
+ if (state2.window.isMaximized()) {
23532
+ state2.window.unmaximize();
23533
+ } else {
23534
+ state2.window.maximize();
23535
+ }
23536
+ });
23537
+ ipc.handle(Channels.WINDOW_CLOSE, () => {
23538
+ state2.window.close();
23539
+ });
23540
+ ipc.handle(Channels.SETTINGS_VISIBILITY, () => {
23541
+ return false;
23542
+ });
23543
+ ipc.handle(Channels.FOCUS_MODE_TOGGLE, () => false);
23544
+ ipc.handle(Channels.SIDEBAR_TOGGLE, () => ({ open: false, width: 0 }));
23545
+ ipc.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => ({ open: false }));
23546
+ ipc.handle(
23547
+ Channels.FIND_IN_PAGE_START,
23548
+ (_e, text, options) => {
23549
+ const tab = tabManager.getActiveTab();
23550
+ if (!tab) return null;
23551
+ const wc = tab.view.webContents;
23552
+ if (wc.isDestroyed()) return null;
23553
+ wireFindEvents(wc);
23554
+ return wc.findInPage(text, {
23555
+ forward: options?.forward ?? true,
23556
+ findNext: options?.findNext ?? false
23557
+ });
23558
+ }
23559
+ );
23560
+ ipc.handle(Channels.FIND_IN_PAGE_NEXT, (_e, forward) => {
23561
+ const tab = tabManager.getActiveTab();
23562
+ if (!tab) return null;
23563
+ const wc = tab.view.webContents;
23564
+ if (wc.isDestroyed()) return null;
23565
+ wireFindEvents(wc);
23566
+ return wc.findInPage("", { forward: forward ?? true, findNext: true });
23567
+ });
23568
+ ipc.handle(
23569
+ Channels.FIND_IN_PAGE_STOP,
23570
+ (_e, action) => {
23571
+ const tab = tabManager.getActiveTab();
23572
+ if (!tab) return;
23573
+ const wc = tab.view.webContents;
23574
+ if (wc.isDestroyed()) return;
23575
+ wc.stopFindInPage(action ?? "clearSelection");
23576
+ }
23577
+ );
23578
+ }
23579
+ function createPrivateWindow() {
23580
+ const privateSessionPartition = `private-${crypto$1.randomUUID()}`;
23581
+ const privateSession = electron.session.fromPartition(privateSessionPartition);
23582
+ privateSession.setUserAgent(electron.session.defaultSession.getUserAgent());
23583
+ const win = new electron.BaseWindow({
23584
+ width: 1280,
23585
+ height: 800,
23586
+ minWidth: 800,
23587
+ minHeight: 600,
23588
+ frame: false,
23589
+ show: false,
23590
+ backgroundColor: "#1e1a2e",
23591
+ title: "Vessel - Private Browsing"
23592
+ });
23593
+ const chromeView = new electron.WebContentsView({
23594
+ webPreferences: {
23595
+ preload: path.join(__dirname, "../preload/index.js"),
23596
+ sandbox: true,
23597
+ contextIsolation: true,
23598
+ nodeIntegration: false
23599
+ }
23600
+ });
23601
+ chromeView.setBackgroundColor("#00000000");
23602
+ win.contentView.addChildView(chromeView);
23603
+ const tabManager = new TabManager(
23604
+ win,
23605
+ (tabs, activeId) => {
23606
+ if (!chromeView.webContents.isDestroyed()) {
23607
+ chromeView.webContents.send(Channels.TAB_STATE_UPDATE, tabs, activeId);
23608
+ }
23609
+ layoutPrivateViews(state2);
23610
+ },
23611
+ { isPrivate: true, sessionPartition: privateSessionPartition }
23612
+ );
23613
+ const state2 = {
23614
+ window: win,
23615
+ chromeView,
23616
+ tabManager,
23617
+ session: privateSession,
23618
+ sessionPartition: privateSessionPartition
23619
+ };
23620
+ installAdBlockingForSession(privateSession, tabManager);
23621
+ installDownloadHandlerForSession(privateSession, chromeView);
23622
+ registerPrivateIpcHandlers(state2);
23623
+ win.on("resize", () => layoutPrivateViews(state2));
23624
+ win.on("show", () => layoutPrivateViews(state2));
23625
+ win.on("closed", () => {
23626
+ privateWindows.delete(state2);
23627
+ tabManager.destroyAllTabs();
23628
+ void Promise.all([
23629
+ privateSession.clearStorageData(),
23630
+ privateSession.clearCache()
23631
+ ]).catch((error) => {
23632
+ logger$5.warn("Failed to clear private browsing session:", error);
23633
+ });
23634
+ });
23635
+ privateWindows.add(state2);
23636
+ chromeView.webContents.once("dom-ready", () => {
23637
+ tabManager.createTab("about:blank");
23638
+ layoutPrivateViews(state2);
23639
+ });
23640
+ loadPrivateRenderer(chromeView);
23641
+ win.show();
23642
+ logger$5.info("Private browsing window opened");
23643
+ return state2;
23644
+ }
23645
+ const CHROME_HEIGHT = 110;
23646
+ const secondaryWindows = /* @__PURE__ */ new Set();
23647
+ function resolveRendererFile$1() {
23648
+ const candidates = [
23649
+ path.join(__dirname, "../../out/renderer/index.html"),
23650
+ path.join(__dirname, "../../../out/renderer/index.html")
23651
+ ];
23652
+ for (const candidate of candidates) {
23653
+ try {
23654
+ fs.accessSync(candidate);
23655
+ return candidate;
23656
+ } catch {
23657
+ }
23658
+ }
23659
+ return path.join(__dirname, "../../out/renderer/index.html");
23660
+ }
23661
+ function layoutSecondaryViews(state2) {
23662
+ const { window: win, chromeView, tabManager } = state2;
23663
+ const [width, height] = win.getContentSize();
23664
+ chromeView.setBounds({ x: 0, y: 0, width, height: CHROME_HEIGHT });
23665
+ win.contentView.removeChildView(chromeView);
23666
+ win.contentView.addChildView(chromeView);
23667
+ const activeTab = tabManager.getActiveTab();
23668
+ if (activeTab) {
23669
+ activeTab.view.setBounds({
23670
+ x: 0,
23671
+ y: CHROME_HEIGHT,
23672
+ width,
23673
+ height: height - CHROME_HEIGHT
23674
+ });
23675
+ }
23676
+ }
23677
+ function loadSecondaryRenderer(chromeView) {
23678
+ const devUrl = process.env.ELECTRON_RENDERER_URL;
23679
+ if (devUrl) {
23680
+ const url = new URL(devUrl);
23681
+ url.searchParams.set("view", "chrome");
23682
+ url.searchParams.set("secondary", "1");
23683
+ chromeView.webContents.loadURL(url.toString());
23684
+ } else {
23685
+ chromeView.webContents.loadFile(resolveRendererFile$1(), {
23686
+ query: { view: "chrome", secondary: "1" }
23687
+ });
23688
+ }
23689
+ }
23690
+ function showTabContextMenu(state2, id) {
23691
+ const { tabManager } = state2;
23692
+ const tab = tabManager.getTab(id);
23693
+ const isPinned = tab?.state.isPinned ?? false;
23694
+ const groupId = tab?.state.groupId;
23695
+ const isMuted = tab?.state.isMuted ?? false;
23696
+ const groups = tabManager.getAllStates().filter((state22) => state22.groupId && state22.groupId !== groupId).reduce(
23697
+ (map, state22) => map.set(state22.groupId, {
23698
+ id: state22.groupId,
23699
+ name: state22.groupName || "Group"
23700
+ }),
23701
+ /* @__PURE__ */ new Map()
23702
+ );
23703
+ const menu = new electron.Menu();
23704
+ menu.append(
23705
+ new electron.MenuItem({
23706
+ label: isPinned ? "Unpin Tab" : "Pin Tab",
23707
+ click: () => isPinned ? tabManager.unpinTab(id) : tabManager.pinTab(id)
23708
+ })
23709
+ );
23710
+ menu.append(
23711
+ new electron.MenuItem({
23712
+ label: "Duplicate Tab",
23713
+ click: () => {
23714
+ const newId = tabManager.duplicateTab(id);
23715
+ if (newId) layoutSecondaryViews(state2);
23716
+ }
23717
+ })
23718
+ );
23719
+ menu.append(
23720
+ new electron.MenuItem({
23721
+ label: "Add to New Group",
23722
+ click: () => {
23723
+ tabManager.createGroupFromTab(id);
23724
+ }
23725
+ })
23726
+ );
23727
+ if (groups.size > 0) {
23728
+ menu.append(
23729
+ new electron.MenuItem({
23730
+ label: "Add to Group",
23731
+ submenu: [...groups.values()].map(
23732
+ (group) => new electron.MenuItem({
23733
+ label: group.name,
23734
+ click: () => tabManager.assignTabToGroup(id, group.id)
23735
+ })
23736
+ )
23737
+ })
23738
+ );
23739
+ }
23740
+ if (groupId) {
23741
+ menu.append(
23742
+ new electron.MenuItem({
23743
+ label: "Remove from Group",
23744
+ click: () => tabManager.removeTabFromGroup(id)
23745
+ })
23746
+ );
23747
+ }
23748
+ menu.append(
23749
+ new electron.MenuItem({
23750
+ label: isMuted ? "Unmute Tab" : "Mute Tab",
23751
+ click: () => tabManager.toggleMuted(id)
23752
+ })
23753
+ );
23754
+ menu.append(new electron.MenuItem({ type: "separator" }));
23755
+ menu.append(
23756
+ new electron.MenuItem({ label: "Print Page", click: () => tabManager.printTab(id) })
23757
+ );
23758
+ menu.append(
23759
+ new electron.MenuItem({
23760
+ label: "Save Page as PDF",
23761
+ click: () => void tabManager.saveTabAsPdf(id)
23762
+ })
23763
+ );
23764
+ if (!isPinned) {
23765
+ menu.append(new electron.MenuItem({ type: "separator" }));
23766
+ menu.append(
23767
+ new electron.MenuItem({
23768
+ label: "Close Tab",
23769
+ click: () => {
23770
+ tabManager.closeTab(id);
23771
+ layoutSecondaryViews(state2);
23772
+ }
23773
+ })
23774
+ );
23775
+ }
23776
+ menu.popup({ window: state2.window });
23777
+ }
23778
+ function showGroupContextMenu(state2, groupId) {
23779
+ const { tabManager } = state2;
23780
+ const firstTab = tabManager.getAllStates().find((tab) => tab.groupId === groupId);
23781
+ if (!firstTab) return;
23782
+ const menu = new electron.Menu();
23783
+ menu.append(
23784
+ new electron.MenuItem({
23785
+ label: firstTab.groupCollapsed ? "Expand Group" : "Collapse Group",
23786
+ click: () => tabManager.toggleGroupCollapsed(groupId)
23787
+ })
23788
+ );
23789
+ menu.append(
23790
+ new electron.MenuItem({
23791
+ label: "Group Color",
23792
+ submenu: TAB_GROUP_COLORS.map(
23793
+ (color) => new electron.MenuItem({
23794
+ label: TAB_GROUP_COLOR_LABELS[color],
23795
+ type: "radio",
23796
+ checked: firstTab.groupColor === color,
23797
+ click: () => tabManager.setGroupColor(groupId, color)
23798
+ })
23799
+ )
23800
+ })
23801
+ );
23802
+ menu.popup({ window: state2.window });
23803
+ }
23804
+ function registerSecondaryIpcHandlers(state2) {
23805
+ const { chromeView, tabManager } = state2;
23806
+ const ipc = chromeView.webContents.ipc;
23807
+ let findResultListener = null;
23808
+ let findWiredWcId = null;
23809
+ const wireFindEvents = (wc) => {
23810
+ if (findWiredWcId === wc.id && findResultListener) return;
23811
+ if (findWiredWcId && findResultListener) {
23812
+ const previous = tabManager.findTabByWebContentsId(findWiredWcId);
23813
+ previous?.view.webContents.removeListener(
23814
+ "found-in-page",
23815
+ findResultListener
23816
+ );
23817
+ }
23818
+ findWiredWcId = wc.id;
23819
+ if (wc.isDestroyed()) return;
23820
+ const listener = (_event, result) => {
23821
+ if (!chromeView.webContents.isDestroyed()) {
23822
+ chromeView.webContents.send(Channels.FIND_IN_PAGE_RESULT, result);
23823
+ }
23824
+ };
23825
+ findResultListener = listener;
23826
+ wc.on("found-in-page", listener);
23827
+ const capturedWcId = wc.id;
23828
+ wc.once("destroyed", () => {
23829
+ if (findWiredWcId === capturedWcId) {
23830
+ findWiredWcId = null;
23831
+ findResultListener = null;
23832
+ }
23833
+ });
23834
+ };
23835
+ ipc.handle(Channels.TAB_CREATE, (_e, url) => {
23836
+ return tabManager.createTab(url || loadSettings().defaultUrl);
23837
+ });
23838
+ ipc.handle(Channels.TAB_CLOSE, (_e, id) => {
23839
+ tabManager.closeTab(id);
23840
+ layoutSecondaryViews(state2);
23841
+ });
23842
+ ipc.handle(Channels.TAB_SWITCH, (_e, id) => {
23843
+ tabManager.switchTab(id);
23844
+ layoutSecondaryViews(state2);
23845
+ });
23846
+ ipc.handle(Channels.TAB_NAVIGATE, (_e, id, url) => {
23847
+ return tabManager.navigateTab(id, url);
23848
+ });
23849
+ ipc.handle(Channels.TAB_BACK, (_e, id) => tabManager.goBack(id));
23850
+ ipc.handle(Channels.TAB_FORWARD, (_e, id) => tabManager.goForward(id));
23851
+ ipc.handle(Channels.TAB_RELOAD, (_e, id) => tabManager.reloadTab(id));
23852
+ ipc.handle(Channels.TAB_TOGGLE_AD_BLOCK, (_e, id) => {
23853
+ const tab = tabManager.getTab(id);
23854
+ if (!tab) return null;
23855
+ const enabled = !tab.state.adBlockingEnabled;
23856
+ tab.setAdBlockingEnabled(enabled);
23857
+ return enabled;
23858
+ });
23859
+ ipc.handle(Channels.TAB_ZOOM_IN, (_e, id) => tabManager.zoomIn(id));
23860
+ ipc.handle(Channels.TAB_ZOOM_OUT, (_e, id) => tabManager.zoomOut(id));
23861
+ ipc.handle(Channels.TAB_ZOOM_RESET, (_e, id) => tabManager.zoomReset(id));
23862
+ ipc.handle(Channels.TAB_REOPEN_CLOSED, () => {
23863
+ const id = tabManager.reopenClosedTab();
23864
+ if (id) layoutSecondaryViews(state2);
23865
+ return id;
23866
+ });
23867
+ ipc.handle(Channels.TAB_DUPLICATE, (_e, id) => {
23868
+ const newId = tabManager.duplicateTab(id);
23869
+ if (newId) layoutSecondaryViews(state2);
23870
+ return newId;
23871
+ });
23872
+ ipc.handle(Channels.TAB_PIN, (_e, id) => tabManager.pinTab(id));
23873
+ ipc.handle(Channels.TAB_UNPIN, (_e, id) => tabManager.unpinTab(id));
23874
+ ipc.handle(
23875
+ Channels.TAB_GROUP_CREATE,
23876
+ (_e, id) => tabManager.createGroupFromTab(id)
23877
+ );
23878
+ ipc.handle(
23879
+ Channels.TAB_GROUP_ADD_TAB,
23880
+ (_e, id, groupId) => tabManager.assignTabToGroup(id, groupId)
23881
+ );
23882
+ ipc.handle(
23883
+ Channels.TAB_GROUP_REMOVE_TAB,
23884
+ (_e, id) => tabManager.removeTabFromGroup(id)
23885
+ );
23886
+ ipc.handle(
23887
+ Channels.TAB_GROUP_TOGGLE_COLLAPSED,
23888
+ (_e, groupId) => tabManager.toggleGroupCollapsed(groupId)
23889
+ );
23890
+ ipc.handle(
23891
+ Channels.TAB_GROUP_SET_COLOR,
23892
+ (_e, groupId, color) => tabManager.setGroupColor(groupId, color)
23893
+ );
23894
+ ipc.handle(
23895
+ Channels.TAB_TOGGLE_MUTE,
23896
+ (_e, id) => tabManager.toggleMuted(id)
23897
+ );
23898
+ ipc.handle(Channels.TAB_PRINT, (_e, id) => tabManager.printTab(id));
23899
+ ipc.handle(
23900
+ Channels.TAB_PRINT_TO_PDF,
23901
+ (_e, id) => tabManager.saveTabAsPdf(id)
23902
+ );
23903
+ ipc.handle(Channels.TAB_STATE_GET, () => ({
23904
+ tabs: tabManager.getAllStates(),
23905
+ activeId: tabManager.getActiveTabId() || ""
23906
+ }));
23907
+ ipc.on(
23908
+ Channels.TAB_CONTEXT_MENU,
23909
+ (_e, id) => showTabContextMenu(state2, id)
23910
+ );
23911
+ ipc.on(
23912
+ Channels.TAB_GROUP_CONTEXT_MENU,
23913
+ (_e, groupId) => showGroupContextMenu(state2, groupId)
23914
+ );
23915
+ ipc.handle(Channels.OPEN_NEW_WINDOW, () => createSecondaryWindow());
23916
+ ipc.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
23917
+ const { createPrivateWindow: createPrivateWindow2 } = require("../private/window");
23918
+ createPrivateWindow2();
23919
+ });
23920
+ ipc.handle(Channels.IS_PRIVATE_MODE, () => false);
23921
+ ipc.handle(Channels.WINDOW_MINIMIZE, () => state2.window.minimize());
23922
+ ipc.handle(Channels.WINDOW_MAXIMIZE, () => {
23923
+ if (state2.window.isMaximized()) state2.window.unmaximize();
23924
+ else state2.window.maximize();
23925
+ });
23926
+ ipc.handle(Channels.WINDOW_CLOSE, () => state2.window.close());
23927
+ ipc.handle(Channels.SETTINGS_VISIBILITY, () => false);
23928
+ ipc.handle(Channels.FOCUS_MODE_TOGGLE, () => false);
23929
+ ipc.handle(Channels.SIDEBAR_TOGGLE, () => ({ open: false, width: 0 }));
23930
+ ipc.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => ({ open: false }));
23931
+ ipc.handle(
23932
+ Channels.FIND_IN_PAGE_START,
23933
+ (_e, text, options) => {
23934
+ const tab = tabManager.getActiveTab();
23935
+ if (!tab) return null;
23936
+ const wc = tab.view.webContents;
23937
+ if (wc.isDestroyed()) return null;
23938
+ wireFindEvents(wc);
23939
+ return wc.findInPage(text, {
23940
+ forward: options?.forward ?? true,
23941
+ findNext: options?.findNext ?? false
23942
+ });
23943
+ }
23944
+ );
23945
+ ipc.handle(Channels.FIND_IN_PAGE_NEXT, (_e, forward) => {
23946
+ const tab = tabManager.getActiveTab();
23947
+ if (!tab) return null;
23948
+ const wc = tab.view.webContents;
23949
+ if (wc.isDestroyed()) return null;
23950
+ wireFindEvents(wc);
23951
+ return wc.findInPage("", { forward: forward ?? true, findNext: true });
23952
+ });
23953
+ ipc.handle(
23954
+ Channels.FIND_IN_PAGE_STOP,
23955
+ (_e, action) => {
23956
+ const tab = tabManager.getActiveTab();
23957
+ if (!tab) return;
23958
+ const wc = tab.view.webContents;
23959
+ if (wc.isDestroyed()) return;
23960
+ wc.stopFindInPage(action ?? "clearSelection");
23961
+ }
23962
+ );
23963
+ }
23964
+ function createSecondaryWindow() {
23965
+ const win = new electron.BaseWindow({
23966
+ width: 1280,
23967
+ height: 800,
23968
+ minWidth: 800,
23969
+ minHeight: 600,
23970
+ frame: false,
23971
+ show: false,
23972
+ backgroundColor: "#1a1a1e",
23973
+ title: "Vessel"
23974
+ });
23975
+ const chromeView = new electron.WebContentsView({
23976
+ webPreferences: {
23977
+ preload: path.join(__dirname, "../preload/index.js"),
23978
+ sandbox: true,
23979
+ contextIsolation: true,
23980
+ nodeIntegration: false
23981
+ }
23982
+ });
23983
+ chromeView.setBackgroundColor("#00000000");
23984
+ win.contentView.addChildView(chromeView);
23985
+ const tabManager = new TabManager(win, (tabs, activeId) => {
23986
+ if (!chromeView.webContents.isDestroyed()) {
23987
+ chromeView.webContents.send(Channels.TAB_STATE_UPDATE, tabs, activeId);
23988
+ }
23989
+ layoutSecondaryViews(state2);
23990
+ });
23991
+ const state2 = { window: win, chromeView, tabManager };
23992
+ installAdBlocking(tabManager);
23993
+ installDownloadHandler(chromeView);
23994
+ registerSecondaryIpcHandlers(state2);
23995
+ win.on("resize", () => layoutSecondaryViews(state2));
23996
+ win.on("show", () => layoutSecondaryViews(state2));
23997
+ win.on("closed", () => {
23998
+ secondaryWindows.delete(state2);
23999
+ unregisterAdBlockingTabManager(tabManager);
24000
+ unregisterDownloadHandler(chromeView);
24001
+ tabManager.destroyAllTabs();
24002
+ });
24003
+ secondaryWindows.add(state2);
24004
+ chromeView.webContents.once("dom-ready", () => {
24005
+ tabManager.createTab(loadSettings().defaultUrl);
24006
+ layoutSecondaryViews(state2);
24007
+ });
24008
+ loadSecondaryRenderer(chromeView);
24009
+ win.show();
24010
+ return state2;
24011
+ }
24012
+ let activeChatProvider = null;
24013
+ const logger$4 = createLogger("IPC");
24014
+ const VALID_APPROVAL_MODES = ["auto", "confirm-dangerous", "manual"];
24015
+ function registerIpcHandlers(windowState, runtime2) {
24016
+ const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
24017
+ electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
24018
+ createPrivateWindow();
24019
+ });
24020
+ electron.ipcMain.handle(Channels.OPEN_NEW_WINDOW, () => {
24021
+ createSecondaryWindow();
24022
+ });
24023
+ electron.ipcMain.handle(Channels.IS_PRIVATE_MODE, () => false);
24024
+ let sidebarResizeRecoveryTimer = null;
24025
+ let sidebarResizeActive = false;
24026
+ let runtimeUpdateTimer = null;
24027
+ let pendingRuntimeState = null;
24028
+ const premiumApiOrigin = process.env.VESSEL_PREMIUM_API ? new URL(process.env.VESSEL_PREMIUM_API).origin : "https://vesselpremium.quantaintellect.com";
24029
+ const clearSidebarResizeRecoveryTimer = () => {
24030
+ if (!sidebarResizeRecoveryTimer) return;
24031
+ clearTimeout(sidebarResizeRecoveryTimer);
24032
+ sidebarResizeRecoveryTimer = null;
24033
+ };
24034
+ const restoreSidebarLayoutAfterResize = () => {
24035
+ clearSidebarResizeRecoveryTimer();
24036
+ if (!sidebarResizeActive) return;
24037
+ sidebarResizeActive = false;
24038
+ layoutViews(windowState);
24039
+ };
24040
+ const scheduleSidebarResizeRecovery = () => {
24041
+ clearSidebarResizeRecoveryTimer();
24042
+ sidebarResizeRecoveryTimer = setTimeout(() => {
24043
+ restoreSidebarLayoutAfterResize();
24044
+ }, 1200);
24045
+ };
24046
+ const flushRuntimeUpdate = () => {
24047
+ runtimeUpdateTimer = null;
24048
+ if (!pendingRuntimeState) return;
24049
+ if (!chromeView.webContents.isDestroyed()) {
24050
+ chromeView.webContents.send(
24051
+ Channels.AGENT_RUNTIME_UPDATE,
24052
+ pendingRuntimeState
24053
+ );
24054
+ }
24055
+ if (!sidebarView.webContents.isDestroyed()) {
24056
+ sidebarView.webContents.send(
24057
+ Channels.AGENT_RUNTIME_UPDATE,
24058
+ pendingRuntimeState
24059
+ );
24060
+ }
24061
+ pendingRuntimeState = null;
24062
+ };
24063
+ const scheduleRuntimeUpdate = (state2) => {
24064
+ pendingRuntimeState = state2;
24065
+ if (runtimeUpdateTimer) return;
24066
+ runtimeUpdateTimer = setTimeout(() => {
24067
+ flushRuntimeUpdate();
24068
+ }, 32);
24069
+ };
24070
+ electron.app.on("before-quit", () => {
24071
+ if (runtimeUpdateTimer) {
24072
+ clearTimeout(runtimeUpdateTimer);
24073
+ runtimeUpdateTimer = null;
24074
+ }
24075
+ flushRuntimeUpdate();
24076
+ });
24077
+ const sendToRendererViews = (channel, ...args) => {
24078
+ chromeView.webContents.send(channel, ...args);
24079
+ sidebarView.webContents.send(channel, ...args);
24080
+ devtoolsPanelView.webContents.send(channel, ...args);
24081
+ };
24082
+ const watchPremiumCheckoutTab = (tabId) => {
24083
+ const tab = tabManager.getTab(tabId);
24084
+ const wc = tab?.view.webContents;
24085
+ if (!wc) return;
24086
+ let completed = false;
24087
+ const cleanup = () => {
24088
+ wc.removeListener("did-navigate", onNavigate);
24089
+ wc.removeListener("did-navigate-in-page", onNavigateInPage);
24090
+ wc.removeListener("destroyed", cleanup);
24091
+ };
24092
+ const handleUrl = async (rawUrl) => {
24093
+ if (completed) return;
24094
+ let parsed;
24095
+ try {
24096
+ parsed = new URL(rawUrl);
24097
+ } catch (err) {
24098
+ logger$4.warn("Failed to parse premium checkout URL while watching checkout tab:", err);
24099
+ return;
24100
+ }
24101
+ if (parsed.origin !== premiumApiOrigin) return;
24102
+ if (parsed.pathname === "/canceled") {
24103
+ completed = true;
24104
+ trackPremiumFunnel("checkout_canceled");
24105
+ cleanup();
24106
+ return;
24107
+ }
24108
+ if (parsed.pathname !== "/success") return;
22496
24109
  completed = true;
22497
24110
  trackPremiumFunnel("checkout_success_seen");
22498
24111
  const sessionId = parsed.searchParams.get("session_id")?.trim();
@@ -22577,23 +24190,226 @@ function registerIpcHandlers(windowState, runtime2) {
22577
24190
  assertString(url, "url");
22578
24191
  return tabManager.navigateTab(id, url, postBody);
22579
24192
  }
22580
- );
22581
- electron.ipcMain.handle(Channels.TAB_BACK, (_, id) => {
22582
- tabManager.goBack(id);
22583
- });
22584
- electron.ipcMain.handle(Channels.TAB_FORWARD, (_, id) => {
22585
- tabManager.goForward(id);
22586
- });
22587
- electron.ipcMain.handle(Channels.TAB_RELOAD, (_, id) => {
22588
- tabManager.reloadTab(id);
24193
+ );
24194
+ electron.ipcMain.handle(Channels.TAB_BACK, (_, id) => {
24195
+ tabManager.goBack(id);
24196
+ });
24197
+ electron.ipcMain.handle(Channels.TAB_FORWARD, (_, id) => {
24198
+ tabManager.goForward(id);
24199
+ });
24200
+ electron.ipcMain.handle(Channels.TAB_RELOAD, (_, id) => {
24201
+ tabManager.reloadTab(id);
24202
+ });
24203
+ electron.ipcMain.handle(Channels.TAB_TOGGLE_AD_BLOCK, (_, id) => {
24204
+ assertString(id, "id");
24205
+ const tab = tabManager.getTab(id);
24206
+ if (!tab) return null;
24207
+ const newState = !tab.state.adBlockingEnabled;
24208
+ tab.setAdBlockingEnabled(newState);
24209
+ return newState;
24210
+ });
24211
+ electron.ipcMain.handle(Channels.TAB_ZOOM_IN, (_, id) => {
24212
+ assertString(id, "id");
24213
+ tabManager.zoomIn(id);
24214
+ });
24215
+ electron.ipcMain.handle(Channels.TAB_ZOOM_OUT, (_, id) => {
24216
+ assertString(id, "id");
24217
+ tabManager.zoomOut(id);
24218
+ });
24219
+ electron.ipcMain.handle(Channels.TAB_ZOOM_RESET, (_, id) => {
24220
+ assertString(id, "id");
24221
+ tabManager.zoomReset(id);
24222
+ });
24223
+ electron.ipcMain.handle(Channels.TAB_REOPEN_CLOSED, () => {
24224
+ const id = tabManager.reopenClosedTab();
24225
+ if (id) layoutViews(windowState);
24226
+ return id;
24227
+ });
24228
+ electron.ipcMain.handle(Channels.TAB_DUPLICATE, (_, id) => {
24229
+ assertString(id, "id");
24230
+ const newId = tabManager.duplicateTab(id);
24231
+ if (newId) layoutViews(windowState);
24232
+ return newId;
24233
+ });
24234
+ electron.ipcMain.handle(Channels.TAB_PIN, (_, id) => {
24235
+ assertString(id, "id");
24236
+ tabManager.pinTab(id);
24237
+ });
24238
+ electron.ipcMain.handle(Channels.TAB_UNPIN, (_, id) => {
24239
+ assertString(id, "id");
24240
+ tabManager.unpinTab(id);
24241
+ });
24242
+ electron.ipcMain.handle(Channels.TAB_GROUP_CREATE, (_, id) => {
24243
+ assertString(id, "id");
24244
+ return tabManager.createGroupFromTab(id);
24245
+ });
24246
+ electron.ipcMain.handle(Channels.TAB_GROUP_ADD_TAB, (_, id, groupId) => {
24247
+ assertString(id, "id");
24248
+ assertString(groupId, "groupId");
24249
+ tabManager.assignTabToGroup(id, groupId);
24250
+ });
24251
+ electron.ipcMain.handle(Channels.TAB_GROUP_REMOVE_TAB, (_, id) => {
24252
+ assertString(id, "id");
24253
+ tabManager.removeTabFromGroup(id);
24254
+ });
24255
+ electron.ipcMain.handle(Channels.TAB_GROUP_TOGGLE_COLLAPSED, (_, groupId) => {
24256
+ assertString(groupId, "groupId");
24257
+ return tabManager.toggleGroupCollapsed(groupId);
24258
+ });
24259
+ electron.ipcMain.handle(
24260
+ Channels.TAB_GROUP_SET_COLOR,
24261
+ (_, groupId, color) => {
24262
+ assertString(groupId, "groupId");
24263
+ assertString(color, "color");
24264
+ tabManager.setGroupColor(groupId, color);
24265
+ }
24266
+ );
24267
+ electron.ipcMain.handle(Channels.TAB_TOGGLE_MUTE, (_, id) => {
24268
+ assertString(id, "id");
24269
+ return tabManager.toggleMuted(id);
24270
+ });
24271
+ electron.ipcMain.handle(Channels.TAB_PRINT, (_, id) => {
24272
+ assertString(id, "id");
24273
+ tabManager.printTab(id);
24274
+ });
24275
+ electron.ipcMain.handle(Channels.TAB_PRINT_TO_PDF, (_, id) => {
24276
+ assertString(id, "id");
24277
+ return tabManager.saveTabAsPdf(id);
24278
+ });
24279
+ electron.ipcMain.on(Channels.TAB_CONTEXT_MENU, (_event, id) => {
24280
+ assertString(id, "id");
24281
+ const tab = tabManager.getTab(id);
24282
+ const isPinned = tab?.state.isPinned ?? false;
24283
+ const groupId = tab?.state.groupId;
24284
+ const isMuted = tab?.state.isMuted ?? false;
24285
+ const groups = tabManager.getAllStates().filter((state2) => state2.groupId && state2.groupId !== groupId).reduce(
24286
+ (map, state2) => map.set(state2.groupId, {
24287
+ id: state2.groupId,
24288
+ name: state2.groupName || "Group"
24289
+ }),
24290
+ /* @__PURE__ */ new Map()
24291
+ );
24292
+ const menu = new electron.Menu();
24293
+ menu.append(
24294
+ new electron.MenuItem({
24295
+ label: isPinned ? "Unpin Tab" : "Pin Tab",
24296
+ click: () => {
24297
+ if (isPinned) {
24298
+ tabManager.unpinTab(id);
24299
+ } else {
24300
+ tabManager.pinTab(id);
24301
+ }
24302
+ }
24303
+ })
24304
+ );
24305
+ menu.append(
24306
+ new electron.MenuItem({
24307
+ label: "Duplicate Tab",
24308
+ click: () => {
24309
+ const newId = tabManager.duplicateTab(id);
24310
+ if (newId) layoutViews(windowState);
24311
+ }
24312
+ })
24313
+ );
24314
+ menu.append(
24315
+ new electron.MenuItem({
24316
+ label: "Add to New Group",
24317
+ click: () => {
24318
+ tabManager.createGroupFromTab(id);
24319
+ }
24320
+ })
24321
+ );
24322
+ if (groups.size > 0) {
24323
+ menu.append(
24324
+ new electron.MenuItem({
24325
+ label: "Add to Group",
24326
+ submenu: [...groups.values()].map(
24327
+ (group) => new electron.MenuItem({
24328
+ label: group.name,
24329
+ click: () => tabManager.assignTabToGroup(id, group.id)
24330
+ })
24331
+ )
24332
+ })
24333
+ );
24334
+ }
24335
+ if (groupId) {
24336
+ menu.append(
24337
+ new electron.MenuItem({
24338
+ label: "Remove from Group",
24339
+ click: () => {
24340
+ tabManager.removeTabFromGroup(id);
24341
+ }
24342
+ })
24343
+ );
24344
+ }
24345
+ menu.append(
24346
+ new electron.MenuItem({
24347
+ label: isMuted ? "Unmute Tab" : "Mute Tab",
24348
+ click: () => {
24349
+ tabManager.toggleMuted(id);
24350
+ }
24351
+ })
24352
+ );
24353
+ menu.append(new electron.MenuItem({ type: "separator" }));
24354
+ menu.append(
24355
+ new electron.MenuItem({
24356
+ label: "Print Page",
24357
+ click: () => {
24358
+ tabManager.printTab(id);
24359
+ }
24360
+ })
24361
+ );
24362
+ menu.append(
24363
+ new electron.MenuItem({
24364
+ label: "Save Page as PDF",
24365
+ click: () => {
24366
+ void tabManager.saveTabAsPdf(id).catch((error) => {
24367
+ logger$4.warn("Failed to save page as PDF:", error);
24368
+ });
24369
+ }
24370
+ })
24371
+ );
24372
+ if (!isPinned) {
24373
+ menu.append(new electron.MenuItem({ type: "separator" }));
24374
+ menu.append(
24375
+ new electron.MenuItem({
24376
+ label: "Close Tab",
24377
+ click: () => {
24378
+ tabManager.closeTab(id);
24379
+ layoutViews(windowState);
24380
+ }
24381
+ })
24382
+ );
24383
+ }
24384
+ menu.popup({ window: mainWindow });
22589
24385
  });
22590
- electron.ipcMain.handle(Channels.TAB_TOGGLE_AD_BLOCK, (_, id) => {
22591
- assertString(id, "id");
22592
- const tab = tabManager.getTab(id);
22593
- if (!tab) return null;
22594
- const newState = !tab.state.adBlockingEnabled;
22595
- tab.setAdBlockingEnabled(newState);
22596
- return newState;
24386
+ electron.ipcMain.on(Channels.TAB_GROUP_CONTEXT_MENU, (_event, groupId) => {
24387
+ assertString(groupId, "groupId");
24388
+ const firstTab = tabManager.getAllStates().find((tab) => tab.groupId === groupId);
24389
+ if (!firstTab) return;
24390
+ const menu = new electron.Menu();
24391
+ menu.append(
24392
+ new electron.MenuItem({
24393
+ label: firstTab.groupCollapsed ? "Expand Group" : "Collapse Group",
24394
+ click: () => {
24395
+ tabManager.toggleGroupCollapsed(groupId);
24396
+ }
24397
+ })
24398
+ );
24399
+ menu.append(
24400
+ new electron.MenuItem({
24401
+ label: "Group Color",
24402
+ submenu: TAB_GROUP_COLORS.map(
24403
+ (color) => new electron.MenuItem({
24404
+ label: TAB_GROUP_COLOR_LABELS[color],
24405
+ type: "radio",
24406
+ checked: firstTab.groupColor === color,
24407
+ click: () => tabManager.setGroupColor(groupId, color)
24408
+ })
24409
+ )
24410
+ })
24411
+ );
24412
+ menu.popup({ window: mainWindow });
22597
24413
  });
22598
24414
  electron.ipcMain.handle(Channels.TAB_STATE_GET, () => ({
22599
24415
  tabs: tabManager.getAllStates(),
@@ -22859,6 +24675,41 @@ function registerIpcHandlers(windowState, runtime2) {
22859
24675
  trackBookmarkAction("remove");
22860
24676
  return removeBookmark(id);
22861
24677
  });
24678
+ electron.ipcMain.handle(
24679
+ Channels.BOOKMARKS_EXPORT_HTML,
24680
+ async (_, options) => {
24681
+ const { canceled, filePath } = await electron.dialog.showSaveDialog({
24682
+ title: "Export Bookmarks",
24683
+ defaultPath: "vessel-bookmarks.html",
24684
+ filters: [{ name: "HTML Bookmarks", extensions: ["html"] }]
24685
+ });
24686
+ if (canceled || !filePath) return null;
24687
+ const content = exportBookmarksHtml({
24688
+ includeNotes: options?.includeNotes ?? false
24689
+ });
24690
+ await fs.promises.writeFile(filePath, content, "utf-8");
24691
+ trackBookmarkAction("export");
24692
+ return {
24693
+ filePath,
24694
+ count: getState().bookmarks.length
24695
+ };
24696
+ }
24697
+ );
24698
+ electron.ipcMain.handle(Channels.BOOKMARKS_EXPORT_JSON, async () => {
24699
+ const { canceled, filePath } = await electron.dialog.showSaveDialog({
24700
+ title: "Export Vessel Bookmark Archive",
24701
+ defaultPath: "vessel-bookmarks.json",
24702
+ filters: [{ name: "Vessel Bookmark Archive", extensions: ["json"] }]
24703
+ });
24704
+ if (canceled || !filePath) return null;
24705
+ const content = exportBookmarksJson();
24706
+ await fs.promises.writeFile(filePath, content, "utf-8");
24707
+ trackBookmarkAction("export");
24708
+ return {
24709
+ filePath,
24710
+ count: getState().bookmarks.length
24711
+ };
24712
+ });
22862
24713
  electron.ipcMain.handle(Channels.FOLDER_REMOVE, (_, id, deleteContents) => {
22863
24714
  trackBookmarkAction("folder_remove");
22864
24715
  return removeFolder(id, deleteContents ?? false);
@@ -24102,157 +25953,6 @@ ${progress}
24102
25953
  });
24103
25954
  }
24104
25955
  }
24105
- const BLOCKED_RESOURCE_TYPES = /* @__PURE__ */ new Set([
24106
- "script",
24107
- "image",
24108
- "xhr",
24109
- "fetch",
24110
- "subFrame",
24111
- "media",
24112
- "ping",
24113
- "webSocket"
24114
- ]);
24115
- const BLOCKED_HOST_SUFFIXES = [
24116
- "doubleclick.net",
24117
- "googlesyndication.com",
24118
- "googleadservices.com",
24119
- "adservice.google.com",
24120
- "adnxs.com",
24121
- "adsrvr.org",
24122
- "taboola.com",
24123
- "outbrain.com",
24124
- "criteo.com",
24125
- "criteo.net",
24126
- "pubmatic.com",
24127
- "rubiconproject.com",
24128
- "openx.net",
24129
- "casalemedia.com",
24130
- "advertising.com",
24131
- "amazon-adsystem.com",
24132
- "adsymptotic.com",
24133
- "moatads.com",
24134
- "quantserve.com",
24135
- "scorecardresearch.com"
24136
- ];
24137
- const THIRD_PARTY_PATH_PATTERNS = [
24138
- /\/ads?[/?._-]/i,
24139
- /\/adservice/i,
24140
- /\/advert/i,
24141
- /\/prebid/i,
24142
- /\/banner/i,
24143
- /\/sponsor/i,
24144
- /\/promotions?\//i,
24145
- /\/trk\//i,
24146
- /\/track(ing)?\//i,
24147
- /\/beacon/i,
24148
- /\/pixel/i
24149
- ];
24150
- let installed = false;
24151
- function normalizeHostname(value) {
24152
- return value.trim().toLowerCase().replace(/\.$/, "");
24153
- }
24154
- function hostnameMatches(hostname, suffix) {
24155
- return hostname === suffix || hostname.endsWith(`.${suffix}`);
24156
- }
24157
- function parseHostname(url) {
24158
- try {
24159
- const parsed = new URL(url);
24160
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
24161
- return null;
24162
- }
24163
- return normalizeHostname(parsed.hostname);
24164
- } catch {
24165
- return null;
24166
- }
24167
- }
24168
- function isThirdParty(url, firstPartyHost) {
24169
- if (!firstPartyHost) return true;
24170
- const target = normalizeHostname(url.hostname);
24171
- return !(target === firstPartyHost || target.endsWith(`.${firstPartyHost}`));
24172
- }
24173
- function shouldBlockRequest(details) {
24174
- if (!BLOCKED_RESOURCE_TYPES.has(details.resourceType)) return false;
24175
- let parsed;
24176
- try {
24177
- parsed = new URL(details.url);
24178
- } catch {
24179
- return false;
24180
- }
24181
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
24182
- return false;
24183
- }
24184
- const hostname = normalizeHostname(parsed.hostname);
24185
- if (BLOCKED_HOST_SUFFIXES.some((suffix) => hostnameMatches(hostname, suffix))) {
24186
- return true;
24187
- }
24188
- const firstPartyHost = parseHostname(details.referrer) || parseHostname(details.initiator || "");
24189
- if (!isThirdParty(parsed, firstPartyHost)) return false;
24190
- const candidate = `${hostname}${parsed.pathname}${parsed.search}`;
24191
- return THIRD_PARTY_PATH_PATTERNS.some((pattern) => pattern.test(candidate));
24192
- }
24193
- function installAdBlocking(tabManager) {
24194
- if (installed) return;
24195
- installed = true;
24196
- electron.session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
24197
- const webContentsId = typeof details.webContentsId === "number" ? details.webContentsId : null;
24198
- if (webContentsId == null) {
24199
- callback({});
24200
- return;
24201
- }
24202
- if (!tabManager.isAdBlockingEnabledForWebContents(webContentsId)) {
24203
- callback({});
24204
- return;
24205
- }
24206
- callback({ cancel: shouldBlockRequest(details) });
24207
- });
24208
- }
24209
- function resolveDownloadPath(downloadDir, filename) {
24210
- fs$1.mkdirSync(downloadDir, { recursive: true });
24211
- const parsed = path.parse(filename);
24212
- let attempt = 0;
24213
- while (true) {
24214
- const candidateName = attempt === 0 ? filename : `${parsed.name} (${attempt})${parsed.ext}`;
24215
- const candidatePath = path.join(downloadDir, candidateName);
24216
- if (!fs$1.existsSync(candidatePath)) {
24217
- return candidatePath;
24218
- }
24219
- attempt += 1;
24220
- }
24221
- }
24222
- function installDownloadHandler(chromeView) {
24223
- electron.session.defaultSession.on("will-download", (_event, item) => {
24224
- const settings2 = loadSettings();
24225
- const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
24226
- const filename = item.getFilename();
24227
- const savePath = resolveDownloadPath(downloadDir, filename);
24228
- item.setSavePath(savePath);
24229
- const info = {
24230
- filename,
24231
- savePath,
24232
- totalBytes: item.getTotalBytes(),
24233
- receivedBytes: 0,
24234
- state: "progressing"
24235
- };
24236
- if (!chromeView.webContents.isDestroyed()) {
24237
- chromeView.webContents.send(Channels.DOWNLOAD_STARTED, info);
24238
- }
24239
- item.on("updated", (_event2, state2) => {
24240
- info.receivedBytes = item.getReceivedBytes();
24241
- info.totalBytes = item.getTotalBytes();
24242
- info.state = state2 === "progressing" ? "progressing" : "interrupted";
24243
- if (!chromeView.webContents.isDestroyed()) {
24244
- chromeView.webContents.send(Channels.DOWNLOAD_PROGRESS, info);
24245
- }
24246
- });
24247
- item.once("done", (_event2, state2) => {
24248
- info.receivedBytes = item.getReceivedBytes();
24249
- info.state = state2 === "completed" ? "completed" : "cancelled";
24250
- if (!chromeView.webContents.isDestroyed()) {
24251
- chromeView.webContents.send(Channels.DOWNLOAD_DONE, info);
24252
- }
24253
- });
24254
- });
24255
- }
24256
25956
  const logger$2 = createLogger("Shortcuts");
24257
25957
  function registerHighlightShortcut(mainWindow, tabManager) {
24258
25958
  const register = () => {
@@ -24273,8 +25973,28 @@ function registerHighlightShortcut(mainWindow, tabManager) {
24273
25973
  mainWindow.removeListener("focus", register);
24274
25974
  };
24275
25975
  }
24276
- function setupAppMenu() {
25976
+ function setupAppMenu(handlers) {
24277
25977
  const appMenu = electron.Menu.buildFromTemplate([
25978
+ {
25979
+ label: "File",
25980
+ submenu: [
25981
+ {
25982
+ label: "New Window",
25983
+ accelerator: "CommandOrControl+N",
25984
+ click: handlers.newWindow
25985
+ },
25986
+ {
25987
+ label: "Save Page As...",
25988
+ accelerator: "CommandOrControl+S",
25989
+ click: handlers.savePageAs
25990
+ },
25991
+ {
25992
+ label: "Reopen Closed Tab",
25993
+ accelerator: "CommandOrControl+Shift+T",
25994
+ click: handlers.reopenClosedTab
25995
+ }
25996
+ ]
25997
+ },
24278
25998
  {
24279
25999
  label: "Edit",
24280
26000
  submenu: [
@@ -24286,6 +26006,32 @@ function setupAppMenu() {
24286
26006
  { role: "paste" },
24287
26007
  { role: "selectAll" }
24288
26008
  ]
26009
+ },
26010
+ {
26011
+ label: "View",
26012
+ submenu: [
26013
+ {
26014
+ label: "Zoom In",
26015
+ accelerator: "CommandOrControl+Plus",
26016
+ click: handlers.zoomIn
26017
+ },
26018
+ {
26019
+ label: "Zoom Out",
26020
+ accelerator: "CommandOrControl+-",
26021
+ click: handlers.zoomOut
26022
+ },
26023
+ {
26024
+ label: "Actual Size",
26025
+ accelerator: "CommandOrControl+0",
26026
+ click: handlers.zoomReset
26027
+ },
26028
+ { type: "separator" },
26029
+ {
26030
+ label: "View Page Source",
26031
+ accelerator: "CommandOrControl+U",
26032
+ click: handlers.viewPageSource
26033
+ }
26034
+ ]
24289
26035
  }
24290
26036
  ]);
24291
26037
  electron.Menu.setApplicationMenu(appMenu);
@@ -24644,7 +26390,37 @@ async function bootstrap() {
24644
26390
  });
24645
26391
  registerIpcHandlers(windowState, runtime);
24646
26392
  registerHighlightShortcut(windowState.mainWindow, tabManager);
24647
- setupAppMenu();
26393
+ setupAppMenu({
26394
+ newWindow: () => {
26395
+ createSecondaryWindow();
26396
+ },
26397
+ reopenClosedTab: () => {
26398
+ const id = tabManager.reopenClosedTab();
26399
+ if (id) layoutViews(windowState);
26400
+ },
26401
+ zoomIn: () => {
26402
+ const id = tabManager.getActiveTabId();
26403
+ if (id) tabManager.zoomIn(id);
26404
+ },
26405
+ zoomOut: () => {
26406
+ const id = tabManager.getActiveTabId();
26407
+ if (id) tabManager.zoomOut(id);
26408
+ },
26409
+ zoomReset: () => {
26410
+ const id = tabManager.getActiveTabId();
26411
+ if (id) tabManager.zoomReset(id);
26412
+ },
26413
+ viewPageSource: () => {
26414
+ const activeTab = tabManager.getActiveTab();
26415
+ if (activeTab) activeTab.viewSource();
26416
+ },
26417
+ savePageAs: () => {
26418
+ const activeTabId = tabManager.getActiveTabId();
26419
+ if (activeTabId) {
26420
+ void tabManager.savePage(activeTabId);
26421
+ }
26422
+ }
26423
+ });
24648
26424
  subscribe((state2) => {
24649
26425
  chromeView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
24650
26426
  sidebarView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);