@quanta-intellect/vessel-browser 0.1.71 → 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
@@ -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
@@ -383,11 +384,13 @@ class Tab {
383
384
  this.onHighlightSelection = options?.onHighlightSelection;
384
385
  this.onHighlightRemove = options?.onHighlightRemove;
385
386
  this.onHighlightRecolor = options?.onHighlightRecolor;
387
+ this.onSavePage = options?.onSavePage;
386
388
  const webPreferences = {
387
389
  preload: path.join(__dirname, "../preload/content-script.js"),
388
390
  sandbox: true,
389
391
  contextIsolation: true,
390
- nodeIntegration: false
392
+ nodeIntegration: false,
393
+ spellcheck: false
391
394
  };
392
395
  if (options?.sessionPartition) {
393
396
  webPreferences.session = electron.session.fromPartition(options.sessionPartition);
@@ -404,6 +407,9 @@ class Tab {
404
407
  canGoForward: false,
405
408
  isReaderMode: false,
406
409
  adBlockingEnabled: options?.adBlockingEnabled ?? true,
410
+ isPinned: false,
411
+ isAudible: false,
412
+ isMuted: false,
407
413
  role: options?.role
408
414
  };
409
415
  this.view.webContents.on("before-input-event", (event, input) => {
@@ -530,6 +536,19 @@ class Tab {
530
536
  this._state.favicon = favicons[0] || "";
531
537
  this.onChange();
532
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
+ });
533
552
  wc.on("context-menu", (_event, params) => {
534
553
  const x = params.x;
535
554
  const y = params.y;
@@ -621,6 +640,19 @@ class Tab {
621
640
  })
622
641
  );
623
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
+ );
624
656
  menu.popup({ window: this.parentWindow });
625
657
  }
626
658
  get state() {
@@ -706,12 +738,61 @@ class Tab {
706
738
  zoomReset() {
707
739
  this.view.webContents.setZoomLevel(0);
708
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
+ }
709
769
  setAdBlockingEnabled(enabled) {
710
770
  if (this._state.adBlockingEnabled === enabled) return false;
711
771
  this._state.adBlockingEnabled = enabled;
712
772
  this.onChange();
713
773
  return true;
714
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
+ }
715
796
  get highlightModeActive() {
716
797
  return this._highlightModeActive;
717
798
  }
@@ -813,6 +894,32 @@ class Tab {
813
894
  this.view.webContents.close();
814
895
  }
815
896
  }
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
+ };
816
923
  const logger$i = createLogger("JsonPersistence");
817
924
  function canUseSafeStorage() {
818
925
  try {
@@ -906,6 +1013,20 @@ const SAVE_DEBOUNCE_MS$5 = 250;
906
1013
  function getHighlightsPath() {
907
1014
  return path.join(electron.app.getPath("userData"), "vessel-highlights.json");
908
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
+ }
909
1030
  function load$4() {
910
1031
  if (state$4) return state$4;
911
1032
  state$4 = loadJsonFile({
@@ -920,15 +1041,8 @@ function load$4() {
920
1041
  });
921
1042
  return state$4;
922
1043
  }
923
- const persistence$5 = createDebouncedJsonPersistence({
924
- debounceMs: SAVE_DEBOUNCE_MS$5,
925
- filePath: getHighlightsPath(),
926
- getValue: () => state$4,
927
- logLabel: "highlights",
928
- resetOnSchedule: true
929
- });
930
1044
  function save$2() {
931
- persistence$5.schedule();
1045
+ getPersistence$1().schedule();
932
1046
  }
933
1047
  function emit$2() {
934
1048
  if (!state$4) return;
@@ -1010,7 +1124,7 @@ function clearHighlightsForUrl(url) {
1010
1124
  return removed;
1011
1125
  }
1012
1126
  function flushPersist$4() {
1013
- return persistence$5.flush();
1127
+ return getPersistence$1().flush();
1014
1128
  }
1015
1129
  const SKIP_TAGS_JS = "var SKIP_TAGS = {SCRIPT:1,STYLE:1,NOSCRIPT:1,TEMPLATE:1,IFRAME:1,SVG:1};";
1016
1130
  const CONTENT_ROOTS_JS = `
@@ -2367,9 +2481,22 @@ function destroySession(tabId) {
2367
2481
  }
2368
2482
  }
2369
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}`;
2495
+ }
2370
2496
  class TabManager {
2371
2497
  tabs = /* @__PURE__ */ new Map();
2372
2498
  order = [];
2499
+ tabGroups = /* @__PURE__ */ new Map();
2373
2500
  activeTabId = null;
2374
2501
  window;
2375
2502
  onStateChange;
@@ -2407,7 +2534,10 @@ class TabManager {
2407
2534
  },
2408
2535
  onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
2409
2536
  onHighlightRemove: (url2, text) => this.removeHighlightByText(url2, text),
2410
- 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
+ }
2411
2541
  });
2412
2542
  this.tabs.set(id, tab);
2413
2543
  this.order.push(id);
@@ -2434,6 +2564,8 @@ class TabManager {
2434
2564
  closeTab(id) {
2435
2565
  const tab = this.tabs.get(id);
2436
2566
  if (!tab) return;
2567
+ if (tab.state.isPinned) return;
2568
+ const groupId = tab.state.groupId;
2437
2569
  this.closedTabs.push({
2438
2570
  url: tab.state.url,
2439
2571
  title: tab.state.title,
@@ -2451,6 +2583,7 @@ class TabManager {
2451
2583
  tab.destroy();
2452
2584
  this.tabs.delete(id);
2453
2585
  this.order = this.order.filter((tid) => tid !== id);
2586
+ this.removeGroupIfEmpty(groupId);
2454
2587
  if (this.activeTabId === id) {
2455
2588
  if (this.order.length > 0) {
2456
2589
  this.switchTab(this.order[this.order.length - 1]);
@@ -2494,6 +2627,118 @@ class TabManager {
2494
2627
  if (!tab) return null;
2495
2628
  return this.createTab(tab.state.url, { adBlockingEnabled: tab.state.adBlockingEnabled });
2496
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
+ }
2497
2742
  getActiveTab() {
2498
2743
  return this.activeTabId ? this.tabs.get(this.activeTabId) : void 0;
2499
2744
  }
@@ -2504,7 +2749,10 @@ class TabManager {
2504
2749
  return this.activeTabId;
2505
2750
  }
2506
2751
  getAllStates() {
2507
- 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());
2508
2756
  }
2509
2757
  findTabByWebContentsId(webContentsId) {
2510
2758
  for (const id of this.order) {
@@ -2537,7 +2785,10 @@ class TabManager {
2537
2785
  id: state2.id,
2538
2786
  url: state2.url || "about:blank",
2539
2787
  title: state2.title,
2540
- adBlockingEnabled: state2.adBlockingEnabled
2788
+ adBlockingEnabled: state2.adBlockingEnabled,
2789
+ isPinned: state2.isPinned,
2790
+ groupName: state2.groupName,
2791
+ groupColor: state2.groupColor
2541
2792
  })),
2542
2793
  activeIndex: activeIndex >= 0 ? activeIndex : 0,
2543
2794
  activeTabId: activeId || void 0,
@@ -2552,12 +2803,33 @@ class TabManager {
2552
2803
  Math.min(snapshot.activeIndex, tabs.length - 1)
2553
2804
  );
2554
2805
  this.destroyAllTabs();
2806
+ const restoredGroups = /* @__PURE__ */ new Map();
2555
2807
  const ids = tabs.map(
2556
2808
  (tab, index) => this.createTab(tab.url || "about:blank", {
2557
2809
  background: index !== activeIndex,
2558
2810
  adBlockingEnabled: tab.adBlockingEnabled ?? true
2559
2811
  })
2560
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
+ });
2561
2833
  const activeId = ids[activeIndex];
2562
2834
  if (activeId) {
2563
2835
  this.switchTab(activeId);
@@ -2576,6 +2848,7 @@ class TabManager {
2576
2848
  }
2577
2849
  this.tabs.clear();
2578
2850
  this.order = [];
2851
+ this.tabGroups.clear();
2579
2852
  this.activeTabId = null;
2580
2853
  this.broadcastState();
2581
2854
  }
@@ -2692,6 +2965,24 @@ class TabManager {
2692
2965
  message: `Color changed to ${color}`
2693
2966
  });
2694
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
+ }
2695
2986
  async removeHighlightMarksForText(wc, text) {
2696
2987
  await wc.executeJavaScript(
2697
2988
  `(function() {
@@ -2773,6 +3064,8 @@ const Channels = {
2773
3064
  BOOKMARK_SAVE: "bookmarks:save",
2774
3065
  BOOKMARK_UPDATE: "bookmarks:update-item",
2775
3066
  BOOKMARK_REMOVE: "bookmarks:remove",
3067
+ BOOKMARKS_EXPORT_HTML: "bookmarks:export-html",
3068
+ BOOKMARKS_EXPORT_JSON: "bookmarks:export-json",
2776
3069
  BOOKMARK_ADD_CONTEXT_TO_CHAT: "bookmarks:add-context-to-chat",
2777
3070
  FOLDER_CREATE: "bookmarks:folder-create",
2778
3071
  FOLDER_REMOVE: "bookmarks:folder-remove",
@@ -2801,6 +3094,23 @@ const Channels = {
2801
3094
  TAB_REOPEN_CLOSED: "tab:reopen-closed",
2802
3095
  TAB_DUPLICATE: "tab:duplicate",
2803
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",
2804
3114
  // Private browsing
2805
3115
  OPEN_PRIVATE_WINDOW: "private:open-window",
2806
3116
  IS_PRIVATE_MODE: "private:is-private",
@@ -5694,7 +6004,7 @@ function enableClipboardShortcuts(view) {
5694
6004
  }
5695
6005
  });
5696
6006
  }
5697
- const CHROME_HEIGHT$1 = 110;
6007
+ const CHROME_HEIGHT$2 = 110;
5698
6008
  const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
5699
6009
  const MIN_DEVTOOLS_PANEL = 120;
5700
6010
  const MAX_DEVTOOLS_PANEL = 600;
@@ -5836,7 +6146,8 @@ function createMainWindow(onTabStateChange) {
5836
6146
  preload: path.join(__dirname, "../preload/index.js"),
5837
6147
  sandbox: true,
5838
6148
  contextIsolation: true,
5839
- nodeIntegration: false
6149
+ nodeIntegration: false,
6150
+ spellcheck: false
5840
6151
  }
5841
6152
  });
5842
6153
  chromeView.setBackgroundColor("#00000000");
@@ -5846,7 +6157,8 @@ function createMainWindow(onTabStateChange) {
5846
6157
  preload: path.join(__dirname, "../preload/index.js"),
5847
6158
  sandbox: true,
5848
6159
  contextIsolation: true,
5849
- nodeIntegration: false
6160
+ nodeIntegration: false,
6161
+ spellcheck: false
5850
6162
  }
5851
6163
  });
5852
6164
  sidebarView.setBackgroundColor("#00000000");
@@ -5860,7 +6172,8 @@ function createMainWindow(onTabStateChange) {
5860
6172
  preload: path.join(__dirname, "../preload/index.js"),
5861
6173
  sandbox: true,
5862
6174
  contextIsolation: true,
5863
- nodeIntegration: false
6175
+ nodeIntegration: false,
6176
+ spellcheck: false
5864
6177
  }
5865
6178
  });
5866
6179
  devtoolsPanelView.setBackgroundColor("#00000000");
@@ -5909,7 +6222,7 @@ function layoutViews(state2) {
5909
6222
  uiState
5910
6223
  } = state2;
5911
6224
  const [width, height] = mainWindow.getContentSize();
5912
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$1;
6225
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$2;
5913
6226
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5914
6227
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5915
6228
  const chromeNeedsFullHeight = uiState.settingsOpen;
@@ -5959,7 +6272,7 @@ function layoutViews(state2) {
5959
6272
  function resizeSidebarViews(state2) {
5960
6273
  const { mainWindow, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
5961
6274
  const [width, height] = mainWindow.getContentSize();
5962
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$1;
6275
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$2;
5963
6276
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5964
6277
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5965
6278
  const resizeHandleOverlap = 6;
@@ -9851,7 +10164,7 @@ function buildCompactScopedContext(page, mode, pageType = detectPageType(page))
9851
10164
  const primaryResults = primaryResultElements.map(formatElement);
9852
10165
  if (primaryResults.length > 0) {
9853
10166
  lines.push("");
9854
- lines.push("### Results — click one of these to open a product");
10167
+ lines.push("### Primary Results");
9855
10168
  lines.push(...primaryResults.map((item) => `- ${item}`));
9856
10169
  lines.push("");
9857
10170
  lines.push("IMPORTANT: Use click(index=N) on a result above. Do NOT click filter or sort links.");
@@ -9995,6 +10308,61 @@ const TOOL_DEFINITIONS = [
9995
10308
  },
9996
10309
  tier: 2
9997
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
+ },
9998
10366
  // --- Navigation ---
9999
10367
  {
10000
10368
  name: "navigate",
@@ -10777,6 +11145,12 @@ function shouldIncludeTool(toolName, pageType, intents, profile) {
10777
11145
  case "switch_tab":
10778
11146
  case "create_tab":
10779
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":
10780
11154
  return intents.has("tabs") || intents.has("debug");
10781
11155
  case "save_session":
10782
11156
  case "load_session":
@@ -10828,14 +11202,6 @@ function pruneToolsForContext(tools, pageType, query = "", options = {}) {
10828
11202
  return description !== tool.description ? { ...tool, description } : tool;
10829
11203
  });
10830
11204
  }
10831
- const SEARCH_ENGINE_PRESETS = {
10832
- duckduckgo: { label: "DuckDuckGo", url: "https://duckduckgo.com/?q=" },
10833
- google: { label: "Google", url: "https://www.google.com/search?q=" },
10834
- bing: { label: "Bing", url: "https://www.bing.com/search?q=" },
10835
- brave: { label: "Brave Search", url: "https://search.brave.com/search?q=" },
10836
- ecosia: { label: "Ecosia", url: "https://www.ecosia.org/search?q=" },
10837
- kagi: { label: "Kagi", url: "https://kagi.com/search?q=" }
10838
- };
10839
11205
  function trimText(value) {
10840
11206
  return typeof value === "string" ? value.trim() : "";
10841
11207
  }
@@ -11027,6 +11393,7 @@ function normalizeBookmarkMetadataUpdate(input) {
11027
11393
  }
11028
11394
  const UNSORTED_ID = "unsorted";
11029
11395
  const ARCHIVE_FOLDER_NAME = "Archive";
11396
+ const NETSCAPE_BOOKMARKS_DOCTYPE = "<!DOCTYPE NETSCAPE-Bookmark-file-1>";
11030
11397
  const SAVE_DEBOUNCE_MS$1 = 250;
11031
11398
  let state$1 = null;
11032
11399
  const listeners = /* @__PURE__ */ new Set();
@@ -11039,6 +11406,19 @@ function cloneState(current) {
11039
11406
  function getBookmarksPath() {
11040
11407
  return path.join(electron.app.getPath("userData"), "vessel-bookmarks.json");
11041
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
+ }
11042
11422
  function load$1() {
11043
11423
  if (state$1) return state$1;
11044
11424
  state$1 = loadJsonFile({
@@ -11054,14 +11434,8 @@ function load$1() {
11054
11434
  });
11055
11435
  return state$1;
11056
11436
  }
11057
- const persistence$1 = createDebouncedJsonPersistence({
11058
- debounceMs: SAVE_DEBOUNCE_MS$1,
11059
- filePath: getBookmarksPath(),
11060
- getValue: () => state$1,
11061
- logLabel: "bookmarks"
11062
- });
11063
11437
  function save() {
11064
- persistence$1.schedule();
11438
+ getPersistence().schedule();
11065
11439
  }
11066
11440
  function assignDefinedBookmarkFields(bookmark, fields) {
11067
11441
  if (!fields) return;
@@ -11077,9 +11451,81 @@ function emit() {
11077
11451
  listener(snapshot);
11078
11452
  }
11079
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
+ }
11080
11483
  function getState() {
11081
11484
  return cloneState(load$1());
11082
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
+ }
11083
11529
  function subscribe(listener) {
11084
11530
  listeners.add(listener);
11085
11531
  return () => {
@@ -11350,7 +11796,7 @@ function renameFolder(id, newName, summary) {
11350
11796
  return { ...folder };
11351
11797
  }
11352
11798
  function flushPersist$1() {
11353
- return persistence$1.flush();
11799
+ return getPersistence().flush();
11354
11800
  }
11355
11801
  function normalizeText(text) {
11356
11802
  return text?.trim() ?? "";
@@ -19304,6 +19750,120 @@ ${buildScopedContext(pageContent, mode)}`;
19304
19750
  return `Closed tab ${tabId}`;
19305
19751
  })
19306
19752
  );
19753
+ server.registerTool(
19754
+ "list_groups",
19755
+ {
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.",
19777
+ inputSchema: {
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")
19781
+ }
19782
+ },
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";
19794
+ }
19795
+ return `Created group ${groupId}`;
19796
+ })
19797
+ );
19798
+ server.registerTool(
19799
+ "assign_to_group",
19800
+ {
19801
+ title: "Assign Tab to Group",
19802
+ description: "Move a tab into an existing group by ID. Defaults to the active tab.",
19803
+ inputSchema: {
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)")
19806
+ }
19807
+ },
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";
19812
+ }
19813
+ tabManager.assignTabToGroup(targetId, groupId);
19814
+ return `Assigned tab ${targetId} to group ${groupId}`;
19815
+ })
19816
+ );
19817
+ server.registerTool(
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
+ );
19307
19867
  server.registerTool(
19308
19868
  "checkpoint_create",
19309
19869
  {
@@ -22520,6 +23080,7 @@ const THIRD_PARTY_PATH_PATTERNS = [
22520
23080
  /\/pixel/i
22521
23081
  ];
22522
23082
  let installed = false;
23083
+ const defaultSessionTabManagers = /* @__PURE__ */ new Set();
22523
23084
  function normalizeHostname(value) {
22524
23085
  return value.trim().toLowerCase().replace(/\.$/, "");
22525
23086
  }
@@ -22563,6 +23124,7 @@ function shouldBlockRequest(details) {
22563
23124
  return THIRD_PARTY_PATH_PATTERNS.some((pattern) => pattern.test(candidate));
22564
23125
  }
22565
23126
  function installAdBlocking(tabManager) {
23127
+ defaultSessionTabManagers.add(tabManager);
22566
23128
  if (installed) return;
22567
23129
  installed = true;
22568
23130
  electron.session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
@@ -22571,13 +23133,19 @@ function installAdBlocking(tabManager) {
22571
23133
  callback({});
22572
23134
  return;
22573
23135
  }
22574
- if (!tabManager.isAdBlockingEnabledForWebContents(webContentsId)) {
23136
+ const manager = [...defaultSessionTabManagers].find(
23137
+ (candidate) => candidate.findTabByWebContentsId(webContentsId)
23138
+ );
23139
+ if (!manager?.isAdBlockingEnabledForWebContents(webContentsId)) {
22575
23140
  callback({});
22576
23141
  return;
22577
23142
  }
22578
23143
  callback({ cancel: shouldBlockRequest(details) });
22579
23144
  });
22580
23145
  }
23146
+ function unregisterAdBlockingTabManager(tabManager) {
23147
+ defaultSessionTabManagers.delete(tabManager);
23148
+ }
22581
23149
  function installAdBlockingForSession(ses, tabManager) {
22582
23150
  ses.webRequest.onBeforeRequest((details, callback) => {
22583
23151
  const webContentsId = typeof details.webContentsId === "number" ? details.webContentsId : null;
@@ -22592,6 +23160,8 @@ function installAdBlockingForSession(ses, tabManager) {
22592
23160
  callback({ cancel: shouldBlockRequest(details) });
22593
23161
  });
22594
23162
  }
23163
+ const defaultDownloadViews = /* @__PURE__ */ new Set();
23164
+ let defaultDownloadHandlerInstalled = false;
22595
23165
  function resolveDownloadPath(downloadDir, filename) {
22596
23166
  fs$1.mkdirSync(downloadDir, { recursive: true });
22597
23167
  const parsed = path.parse(filename);
@@ -22606,9 +23176,23 @@ function resolveDownloadPath(downloadDir, filename) {
22606
23176
  }
22607
23177
  }
22608
23178
  function installDownloadHandler(chromeView) {
22609
- installDownloadHandlerForSession(electron.session.defaultSession, 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);
22610
23186
  }
22611
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
+ };
22612
23196
  targetSession.on("will-download", (_event, item) => {
22613
23197
  const settings2 = loadSettings();
22614
23198
  const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
@@ -22622,30 +23206,24 @@ function installDownloadHandlerForSession(targetSession, chromeView) {
22622
23206
  receivedBytes: 0,
22623
23207
  state: "progressing"
22624
23208
  };
22625
- if (!chromeView.webContents.isDestroyed()) {
22626
- chromeView.webContents.send(Channels.DOWNLOAD_STARTED, info);
22627
- }
23209
+ send(Channels.DOWNLOAD_STARTED, info);
22628
23210
  item.on("updated", (_event2, state2) => {
22629
23211
  info.receivedBytes = item.getReceivedBytes();
22630
23212
  info.totalBytes = item.getTotalBytes();
22631
23213
  info.state = state2 === "progressing" ? "progressing" : "interrupted";
22632
- if (!chromeView.webContents.isDestroyed()) {
22633
- chromeView.webContents.send(Channels.DOWNLOAD_PROGRESS, info);
22634
- }
23214
+ send(Channels.DOWNLOAD_PROGRESS, info);
22635
23215
  });
22636
23216
  item.once("done", (_event2, state2) => {
22637
23217
  info.receivedBytes = item.getReceivedBytes();
22638
23218
  info.state = state2 === "completed" ? "completed" : "cancelled";
22639
- if (!chromeView.webContents.isDestroyed()) {
22640
- chromeView.webContents.send(Channels.DOWNLOAD_DONE, info);
22641
- }
23219
+ send(Channels.DOWNLOAD_DONE, info);
22642
23220
  });
22643
23221
  });
22644
23222
  }
22645
23223
  const logger$5 = createLogger("PrivateWindow");
22646
- const CHROME_HEIGHT = 110;
23224
+ const CHROME_HEIGHT$1 = 110;
22647
23225
  const privateWindows = /* @__PURE__ */ new Set();
22648
- function resolveRendererFile$1() {
23226
+ function resolveRendererFile$2() {
22649
23227
  const candidates = [
22650
23228
  path.join(__dirname, "../../out/renderer/index.html"),
22651
23229
  path.join(__dirname, "../../../out/renderer/index.html")
@@ -22662,16 +23240,16 @@ function resolveRendererFile$1() {
22662
23240
  function layoutPrivateViews(state2) {
22663
23241
  const { window: win, chromeView, tabManager } = state2;
22664
23242
  const [width, height] = win.getContentSize();
22665
- chromeView.setBounds({ x: 0, y: 0, width, height: CHROME_HEIGHT });
23243
+ chromeView.setBounds({ x: 0, y: 0, width, height: CHROME_HEIGHT$1 });
22666
23244
  win.contentView.removeChildView(chromeView);
22667
23245
  win.contentView.addChildView(chromeView);
22668
23246
  const activeTab = tabManager.getActiveTab();
22669
23247
  if (activeTab) {
22670
23248
  activeTab.view.setBounds({
22671
23249
  x: 0,
22672
- y: CHROME_HEIGHT,
23250
+ y: CHROME_HEIGHT$1,
22673
23251
  width,
22674
- height: height - CHROME_HEIGHT
23252
+ height: height - CHROME_HEIGHT$1
22675
23253
  });
22676
23254
  }
22677
23255
  }
@@ -22683,7 +23261,7 @@ function loadPrivateRenderer(chromeView) {
22683
23261
  url.searchParams.set("private", "1");
22684
23262
  chromeView.webContents.loadURL(url.toString());
22685
23263
  } else {
22686
- chromeView.webContents.loadFile(resolveRendererFile$1(), {
23264
+ chromeView.webContents.loadFile(resolveRendererFile$2(), {
22687
23265
  query: { view: "chrome", private: "1" }
22688
23266
  });
22689
23267
  }
@@ -22772,12 +23350,68 @@ function registerPrivateIpcHandlers(state2) {
22772
23350
  if (newId) layoutPrivateViews(state2);
22773
23351
  return newId;
22774
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
+ });
22775
23386
  ipc.on(Channels.TAB_CONTEXT_MENU, (_e, id) => {
22776
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
+ );
22777
23399
  const menu = new Menu();
22778
23400
  menu.append(
22779
23401
  new MenuItem({
22780
- label: "Duplicate Tab",
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",
22781
23415
  click: () => {
22782
23416
  const newId = tabManager.duplicateTab(id);
22783
23417
  if (newId) layoutPrivateViews(state2);
@@ -22786,19 +23420,110 @@ function registerPrivateIpcHandlers(state2) {
22786
23420
  );
22787
23421
  menu.append(
22788
23422
  new MenuItem({
22789
- label: "Close Tab",
23423
+ label: "Add to New Group",
22790
23424
  click: () => {
22791
- tabManager.closeTab(id);
22792
- layoutPrivateViews(state2);
23425
+ tabManager.createGroupFromTab(id);
22793
23426
  }
22794
23427
  })
22795
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
+ );
22796
23517
  menu.popup({ window: state2.window });
22797
23518
  });
22798
23519
  ipc.handle(Channels.IS_PRIVATE_MODE, () => true);
22799
23520
  ipc.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
22800
23521
  createPrivateWindow();
22801
23522
  });
23523
+ ipc.handle(Channels.OPEN_NEW_WINDOW, () => {
23524
+ const { createSecondaryWindow: createSecondaryWindow2 } = require("../secondary/window");
23525
+ createSecondaryWindow2();
23526
+ });
22802
23527
  ipc.handle(Channels.WINDOW_MINIMIZE, () => {
22803
23528
  state2.window.minimize();
22804
23529
  });
@@ -22917,6 +23642,373 @@ function createPrivateWindow() {
22917
23642
  logger$5.info("Private browsing window opened");
22918
23643
  return state2;
22919
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
+ }
22920
24012
  let activeChatProvider = null;
22921
24013
  const logger$4 = createLogger("IPC");
22922
24014
  const VALID_APPROVAL_MODES = ["auto", "confirm-dangerous", "manual"];
@@ -22925,6 +24017,9 @@ function registerIpcHandlers(windowState, runtime2) {
22925
24017
  electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
22926
24018
  createPrivateWindow();
22927
24019
  });
24020
+ electron.ipcMain.handle(Channels.OPEN_NEW_WINDOW, () => {
24021
+ createSecondaryWindow();
24022
+ });
22928
24023
  electron.ipcMain.handle(Channels.IS_PRIVATE_MODE, () => false);
22929
24024
  let sidebarResizeRecoveryTimer = null;
22930
24025
  let sidebarResizeActive = false;
@@ -23136,9 +24231,77 @@ function registerIpcHandlers(windowState, runtime2) {
23136
24231
  if (newId) layoutViews(windowState);
23137
24232
  return newId;
23138
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
+ });
23139
24279
  electron.ipcMain.on(Channels.TAB_CONTEXT_MENU, (_event, id) => {
23140
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
+ );
23141
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
+ );
23142
24305
  menu.append(
23143
24306
  new electron.MenuItem({
23144
24307
  label: "Duplicate Tab",
@@ -23150,13 +24313,102 @@ function registerIpcHandlers(windowState, runtime2) {
23150
24313
  );
23151
24314
  menu.append(
23152
24315
  new electron.MenuItem({
23153
- label: "Close Tab",
24316
+ label: "Add to New Group",
23154
24317
  click: () => {
23155
- tabManager.closeTab(id);
23156
- layoutViews(windowState);
24318
+ tabManager.createGroupFromTab(id);
23157
24319
  }
23158
24320
  })
23159
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 });
24385
+ });
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
+ );
23160
24412
  menu.popup({ window: mainWindow });
23161
24413
  });
23162
24414
  electron.ipcMain.handle(Channels.TAB_STATE_GET, () => ({
@@ -23423,6 +24675,41 @@ function registerIpcHandlers(windowState, runtime2) {
23423
24675
  trackBookmarkAction("remove");
23424
24676
  return removeBookmark(id);
23425
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
+ });
23426
24713
  electron.ipcMain.handle(Channels.FOLDER_REMOVE, (_, id, deleteContents) => {
23427
24714
  trackBookmarkAction("folder_remove");
23428
24715
  return removeFolder(id, deleteContents ?? false);
@@ -24691,6 +25978,16 @@ function setupAppMenu(handlers) {
24691
25978
  {
24692
25979
  label: "File",
24693
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
+ },
24694
25991
  {
24695
25992
  label: "Reopen Closed Tab",
24696
25993
  accelerator: "CommandOrControl+Shift+T",
@@ -24727,6 +26024,12 @@ function setupAppMenu(handlers) {
24727
26024
  label: "Actual Size",
24728
26025
  accelerator: "CommandOrControl+0",
24729
26026
  click: handlers.zoomReset
26027
+ },
26028
+ { type: "separator" },
26029
+ {
26030
+ label: "View Page Source",
26031
+ accelerator: "CommandOrControl+U",
26032
+ click: handlers.viewPageSource
24730
26033
  }
24731
26034
  ]
24732
26035
  }
@@ -25088,6 +26391,9 @@ async function bootstrap() {
25088
26391
  registerIpcHandlers(windowState, runtime);
25089
26392
  registerHighlightShortcut(windowState.mainWindow, tabManager);
25090
26393
  setupAppMenu({
26394
+ newWindow: () => {
26395
+ createSecondaryWindow();
26396
+ },
25091
26397
  reopenClosedTab: () => {
25092
26398
  const id = tabManager.reopenClosedTab();
25093
26399
  if (id) layoutViews(windowState);
@@ -25103,6 +26409,16 @@ async function bootstrap() {
25103
26409
  zoomReset: () => {
25104
26410
  const id = tabManager.getActiveTabId();
25105
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
+ }
25106
26422
  }
25107
26423
  });
25108
26424
  subscribe((state2) => {