@quanta-intellect/vessel-browser 0.1.69 → 0.1.71

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;
@@ -368,7 +368,7 @@ class Tab {
368
368
  guardedLoadURL(url, options) {
369
369
  const blockReason = this.getNavigationBlockReason(url);
370
370
  if (blockReason) {
371
- logger$i.warn(blockReason);
371
+ logger$j.warn(blockReason);
372
372
  return blockReason;
373
373
  }
374
374
  void this.view.webContents.loadURL(url, options);
@@ -383,14 +383,16 @@ class Tab {
383
383
  this.onHighlightSelection = options?.onHighlightSelection;
384
384
  this.onHighlightRemove = options?.onHighlightRemove;
385
385
  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
- });
386
+ const webPreferences = {
387
+ preload: path.join(__dirname, "../preload/content-script.js"),
388
+ sandbox: true,
389
+ contextIsolation: true,
390
+ nodeIntegration: false
391
+ };
392
+ if (options?.sessionPartition) {
393
+ webPreferences.session = electron.session.fromPartition(options.sessionPartition);
394
+ }
395
+ this.view = new electron.WebContentsView({ webPreferences });
394
396
  const initialUrl = url || "about:blank";
395
397
  this._state = {
396
398
  id,
@@ -404,11 +406,26 @@ class Tab {
404
406
  adBlockingEnabled: options?.adBlockingEnabled ?? true,
405
407
  role: options?.role
406
408
  };
407
- this.view.webContents.on("before-input-event", (_event, input) => {
409
+ this.view.webContents.on("before-input-event", (event, input) => {
408
410
  if (!input.control && !input.meta) return;
409
411
  if (input.type !== "keyDown") return;
410
412
  const key = input.key.toLowerCase();
411
413
  const wc = this.view.webContents;
414
+ if (key === "+" || key === "=") {
415
+ this.zoomIn();
416
+ event.preventDefault();
417
+ return;
418
+ }
419
+ if (key === "-") {
420
+ this.zoomOut();
421
+ event.preventDefault();
422
+ return;
423
+ }
424
+ if (key === "0") {
425
+ this.zoomReset();
426
+ event.preventDefault();
427
+ return;
428
+ }
412
429
  if (key === "c") wc.copy();
413
430
  else if (key === "v") wc.paste();
414
431
  else if (key === "x") wc.cut();
@@ -429,7 +446,7 @@ class Tab {
429
446
  wc.setWindowOpenHandler(({ url, disposition }) => {
430
447
  const error = this.getNavigationBlockReason(url);
431
448
  if (error) {
432
- logger$i.warn(error);
449
+ logger$j.warn(error);
433
450
  return { action: "deny" };
434
451
  }
435
452
  this.onOpenUrl?.({
@@ -443,7 +460,7 @@ class Tab {
443
460
  const error = this.getNavigationBlockReason(url);
444
461
  if (!error) return;
445
462
  event.preventDefault();
446
- logger$i.warn(`${context}: ${error}`);
463
+ logger$j.warn(`${context}: ${error}`);
447
464
  };
448
465
  wc.on("will-navigate", (event, url) => {
449
466
  blockNavigation(event, url, "Blocked top-level navigation");
@@ -507,7 +524,7 @@ class Tab {
507
524
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 999px; }
508
525
  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
509
526
  ::-webkit-scrollbar-corner { background: transparent; }
510
- `).catch((err) => logger$i.warn("Failed to inject scrollbar CSS:", err));
527
+ `).catch((err) => logger$j.warn("Failed to inject scrollbar CSS:", err));
511
528
  });
512
529
  wc.on("page-favicon-updated", (_, favicons) => {
513
530
  this._state.favicon = favicons[0] || "";
@@ -530,7 +547,7 @@ class Tab {
530
547
  ).then((highlightedText) => {
531
548
  this.buildContextMenu(wc, params, highlightedText.trim());
532
549
  }).catch((err) => {
533
- logger$i.warn("Failed to inspect highlighted text for context menu:", err);
550
+ logger$j.warn("Failed to inspect highlighted text for context menu:", err);
534
551
  this.buildContextMenu(wc, params, "");
535
552
  });
536
553
  });
@@ -676,6 +693,19 @@ class Tab {
676
693
  reload() {
677
694
  this.view.webContents.reload();
678
695
  }
696
+ zoomIn() {
697
+ const wc = this.view.webContents;
698
+ const level = wc.getZoomLevel();
699
+ wc.setZoomLevel(level + 0.5);
700
+ }
701
+ zoomOut() {
702
+ const wc = this.view.webContents;
703
+ const level = wc.getZoomLevel();
704
+ wc.setZoomLevel(level - 0.5);
705
+ }
706
+ zoomReset() {
707
+ this.view.webContents.setZoomLevel(0);
708
+ }
679
709
  setAdBlockingEnabled(enabled) {
680
710
  if (this._state.adBlockingEnabled === enabled) return false;
681
711
  this._state.adBlockingEnabled = enabled;
@@ -761,7 +791,7 @@ class Tab {
761
791
  document.addEventListener('mouseup', window.__vesselHighlightHandler);
762
792
  }
763
793
  })()
764
- `).catch((err) => logger$i.warn("Failed to inject highlight listener:", err));
794
+ `).catch((err) => logger$j.warn("Failed to inject highlight listener:", err));
765
795
  } else {
766
796
  void wc.executeJavaScript(`
767
797
  (function() {
@@ -772,7 +802,7 @@ class Tab {
772
802
  delete window.__vesselHighlightHandler;
773
803
  }
774
804
  })()
775
- `).catch((err) => logger$i.warn("Failed to remove highlight listener:", err));
805
+ `).catch((err) => logger$j.warn("Failed to remove highlight listener:", err));
776
806
  }
777
807
  }
778
808
  get webContentsId() {
@@ -783,7 +813,7 @@ class Tab {
783
813
  this.view.webContents.close();
784
814
  }
785
815
  }
786
- const logger$h = createLogger("JsonPersistence");
816
+ const logger$i = createLogger("JsonPersistence");
787
817
  function canUseSafeStorage() {
788
818
  try {
789
819
  return electron.safeStorage.isEncryptionAvailable();
@@ -848,7 +878,7 @@ function createDebouncedJsonPersistence({
848
878
  data,
849
879
  typeof data === "string" ? { encoding: "utf-8", mode: 384 } : { mode: 384 }
850
880
  )
851
- ).catch((err) => logger$h.error(`Failed to save ${logLabel}:`, err));
881
+ ).catch((err) => logger$i.error(`Failed to save ${logLabel}:`, err));
852
882
  };
853
883
  const schedule = () => {
854
884
  saveDirty2 = true;
@@ -2336,13 +2366,7 @@ function destroySession(tabId) {
2336
2366
  sessions.delete(tabId);
2337
2367
  }
2338
2368
  }
2339
- function destroyAllSessions() {
2340
- for (const session of sessions.values()) {
2341
- session.destroy();
2342
- }
2343
- sessions.clear();
2344
- }
2345
- const logger$g = createLogger("TabManager");
2369
+ const logger$h = createLogger("TabManager");
2346
2370
  class TabManager {
2347
2371
  tabs = /* @__PURE__ */ new Map();
2348
2372
  order = [];
@@ -2351,9 +2375,15 @@ class TabManager {
2351
2375
  onStateChange;
2352
2376
  highlightCaptureCallback = null;
2353
2377
  pageLoadCallback = null;
2354
- constructor(window2, onStateChange) {
2378
+ closedTabs = [];
2379
+ MAX_CLOSED_TABS = 20;
2380
+ isPrivate;
2381
+ sessionPartition;
2382
+ constructor(window2, onStateChange, options) {
2355
2383
  this.window = window2;
2356
2384
  this.onStateChange = onStateChange;
2385
+ this.isPrivate = options?.isPrivate ?? false;
2386
+ this.sessionPartition = options?.sessionPartition ?? (this.isPrivate ? "private-mode" : void 0);
2357
2387
  }
2358
2388
  onPageLoad(cb) {
2359
2389
  this.pageLoadCallback = cb;
@@ -2364,12 +2394,15 @@ class TabManager {
2364
2394
  const tab = new Tab(id, url, () => this.broadcastState(), {
2365
2395
  adBlockingEnabled: options?.adBlockingEnabled,
2366
2396
  parentWindow: this.window,
2397
+ sessionPartition: this.sessionPartition,
2367
2398
  onOpenUrl: ({ url: requestedUrl, background: background2, adBlockingEnabled }) => {
2368
2399
  this.createTab(requestedUrl, { background: background2, adBlockingEnabled });
2369
2400
  },
2370
2401
  onPageLoad: (pageUrl, wc) => {
2371
2402
  this.reapplyHighlights(pageUrl, wc);
2372
- addEntry$1(pageUrl, wc.getTitle());
2403
+ if (!this.isPrivate) {
2404
+ addEntry$1(pageUrl, wc.getTitle());
2405
+ }
2373
2406
  this.pageLoadCallback?.(pageUrl, wc);
2374
2407
  },
2375
2408
  onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
@@ -2401,6 +2434,14 @@ class TabManager {
2401
2434
  closeTab(id) {
2402
2435
  const tab = this.tabs.get(id);
2403
2436
  if (!tab) return;
2437
+ this.closedTabs.push({
2438
+ url: tab.state.url,
2439
+ title: tab.state.title,
2440
+ adBlockingEnabled: tab.state.adBlockingEnabled
2441
+ });
2442
+ if (this.closedTabs.length > this.MAX_CLOSED_TABS) {
2443
+ this.closedTabs.shift();
2444
+ }
2404
2445
  const wcId = tab.webContentsId;
2405
2446
  if (wcId !== void 0) {
2406
2447
  this.lastReapply.delete(wcId);
@@ -2434,6 +2475,25 @@ class TabManager {
2434
2475
  reloadTab(id) {
2435
2476
  this.tabs.get(id)?.reload();
2436
2477
  }
2478
+ zoomIn(id) {
2479
+ this.tabs.get(id)?.zoomIn();
2480
+ }
2481
+ zoomOut(id) {
2482
+ this.tabs.get(id)?.zoomOut();
2483
+ }
2484
+ zoomReset(id) {
2485
+ this.tabs.get(id)?.zoomReset();
2486
+ }
2487
+ reopenClosedTab() {
2488
+ const last = this.closedTabs.pop();
2489
+ if (!last) return null;
2490
+ return this.createTab(last.url, { adBlockingEnabled: last.adBlockingEnabled });
2491
+ }
2492
+ duplicateTab(id) {
2493
+ const tab = this.tabs.get(id);
2494
+ if (!tab) return null;
2495
+ return this.createTab(tab.state.url, { adBlockingEnabled: tab.state.adBlockingEnabled });
2496
+ }
2437
2497
  getActiveTab() {
2438
2498
  return this.activeTabId ? this.tabs.get(this.activeTabId) : void 0;
2439
2499
  }
@@ -2507,10 +2567,10 @@ class TabManager {
2507
2567
  return ids;
2508
2568
  }
2509
2569
  destroyAllTabs() {
2510
- destroyAllSessions();
2511
- for (const id of this.order) {
2570
+ for (const id of [...this.order]) {
2512
2571
  const tab = this.tabs.get(id);
2513
2572
  if (!tab) continue;
2573
+ destroySession(id);
2514
2574
  this.window.contentView.removeChildView(tab.view);
2515
2575
  tab.destroy();
2516
2576
  }
@@ -2536,7 +2596,7 @@ class TabManager {
2536
2596
  }));
2537
2597
  if (entries.length > 0) {
2538
2598
  void highlightBatchOnPage(wc, entries).catch(
2539
- (err) => logger$g.warn("Failed to batch highlight:", err)
2599
+ (err) => logger$h.warn("Failed to batch highlight:", err)
2540
2600
  );
2541
2601
  }
2542
2602
  }
@@ -2558,12 +2618,12 @@ class TabManager {
2558
2618
  const result = await captureSelectionHighlight(wc);
2559
2619
  if (result.success && result.text) {
2560
2620
  await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(
2561
- (err) => logger$g.warn("Failed to capture highlight:", err)
2621
+ (err) => logger$h.warn("Failed to capture highlight:", err)
2562
2622
  );
2563
2623
  }
2564
2624
  this.highlightCaptureCallback?.(result);
2565
2625
  } catch (err) {
2566
- logger$g.warn("Failed to capture highlight from page:", err);
2626
+ logger$h.warn("Failed to capture highlight from page:", err);
2567
2627
  this.highlightCaptureCallback?.({
2568
2628
  success: false,
2569
2629
  message: "Could not capture selection"
@@ -2588,7 +2648,7 @@ class TabManager {
2588
2648
  void this.removeHighlightMarksForText(wc, text);
2589
2649
  }
2590
2650
  } catch (err) {
2591
- logger$g.warn("Failed to remove highlight from matching tab:", err);
2651
+ logger$h.warn("Failed to remove highlight from matching tab:", err);
2592
2652
  }
2593
2653
  }
2594
2654
  this.highlightCaptureCallback?.({
@@ -2619,12 +2679,12 @@ class TabManager {
2619
2679
  void 0,
2620
2680
  color
2621
2681
  ).catch(
2622
- (err) => logger$g.warn("Failed to update highlight color:", err)
2682
+ (err) => logger$h.warn("Failed to update highlight color:", err)
2623
2683
  );
2624
2684
  });
2625
2685
  }
2626
2686
  } catch (err) {
2627
- logger$g.warn("Failed to iterate highlights for color change:", err);
2687
+ logger$h.warn("Failed to iterate highlights for color change:", err);
2628
2688
  }
2629
2689
  }
2630
2690
  this.highlightCaptureCallback?.({
@@ -2646,7 +2706,7 @@ class TabManager {
2646
2706
  });
2647
2707
  })()`
2648
2708
  ).catch(
2649
- (err) => logger$g.warn("Failed to remove highlight marks:", err)
2709
+ (err) => logger$h.warn("Failed to remove highlight marks:", err)
2650
2710
  );
2651
2711
  }
2652
2712
  broadcastState() {
@@ -2733,6 +2793,17 @@ const Channels = {
2733
2793
  DEVTOOLS_PANEL_RESIZE: "devtools-panel:resize",
2734
2794
  // Ad blocking
2735
2795
  TAB_TOGGLE_AD_BLOCK: "tab:toggle-ad-block",
2796
+ // Zoom
2797
+ TAB_ZOOM_IN: "tab:zoom-in",
2798
+ TAB_ZOOM_OUT: "tab:zoom-out",
2799
+ TAB_ZOOM_RESET: "tab:zoom-reset",
2800
+ // Closed tabs / duplication
2801
+ TAB_REOPEN_CLOSED: "tab:reopen-closed",
2802
+ TAB_DUPLICATE: "tab:duplicate",
2803
+ TAB_CONTEXT_MENU: "tab:context-menu",
2804
+ // Private browsing
2805
+ OPEN_PRIVATE_WINDOW: "private:open-window",
2806
+ IS_PRIVATE_MODE: "private:is-private",
2736
2807
  // Find in page
2737
2808
  FIND_IN_PAGE_START: "find:start",
2738
2809
  FIND_IN_PAGE_NEXT: "find:next",
@@ -3750,7 +3821,7 @@ function errorResult(error, value) {
3750
3821
  function getErrorMessage(error, fallback = "Unknown error") {
3751
3822
  return error instanceof Error && error.message ? error.message : fallback;
3752
3823
  }
3753
- const logger$f = createLogger("Premium");
3824
+ const logger$g = createLogger("Premium");
3754
3825
  const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
3755
3826
  const FREE_TOOL_ITERATION_LIMIT = 50;
3756
3827
  const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
@@ -3863,7 +3934,7 @@ async function verifySubscription(identifier) {
3863
3934
  });
3864
3935
  if (!res.ok) {
3865
3936
  const detail = await readApiErrorDetail(res);
3866
- logger$f.warn(
3937
+ logger$g.warn(
3867
3938
  "Verification API returned a non-OK status:",
3868
3939
  res.status,
3869
3940
  detail
@@ -3882,7 +3953,7 @@ async function verifySubscription(identifier) {
3882
3953
  setSetting("premium", updated);
3883
3954
  return updated;
3884
3955
  } catch (err) {
3885
- logger$f.warn("Verification failed:", err);
3956
+ logger$g.warn("Verification failed:", err);
3886
3957
  return current;
3887
3958
  }
3888
3959
  }
@@ -4455,7 +4526,7 @@ const EXTRACT_TIMEOUT_MAX_MS = 2e4;
4455
4526
  const MUTATION_CAPTURE_INTERVAL_MS = 5e3;
4456
4527
  const MUTATION_SETTLE_AFTER_MS = 1500;
4457
4528
  const AGENT_STREAM_IDLE_TIMEOUT_MS = 3e4;
4458
- const logger$e = createLogger("Extractor");
4529
+ const logger$f = createLogger("Extractor");
4459
4530
  const EMPTY_PAGE_CONTENT = {
4460
4531
  title: "",
4461
4532
  content: "",
@@ -5205,7 +5276,7 @@ async function executeScript(webContents, script) {
5205
5276
  })
5206
5277
  ]);
5207
5278
  } catch (err) {
5208
- logger$e.warn("Failed to execute page script:", err);
5279
+ logger$f.warn("Failed to execute page script:", err);
5209
5280
  return null;
5210
5281
  } finally {
5211
5282
  if (timer) {
@@ -5312,7 +5383,7 @@ async function estimateExtractionTimeout(webContents) {
5312
5383
  return EXTRACT_TIMEOUT_BASE_MS + extra;
5313
5384
  }
5314
5385
  } catch (err) {
5315
- logger$e.warn("Failed to estimate extraction timeout, using base timeout:", err);
5386
+ logger$f.warn("Failed to estimate extraction timeout, using base timeout:", err);
5316
5387
  }
5317
5388
  return EXTRACT_TIMEOUT_BASE_MS;
5318
5389
  }
@@ -5623,7 +5694,7 @@ function enableClipboardShortcuts(view) {
5623
5694
  }
5624
5695
  });
5625
5696
  }
5626
- const CHROME_HEIGHT = 110;
5697
+ const CHROME_HEIGHT$1 = 110;
5627
5698
  const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
5628
5699
  const MIN_DEVTOOLS_PANEL = 120;
5629
5700
  const MAX_DEVTOOLS_PANEL = 600;
@@ -5838,7 +5909,7 @@ function layoutViews(state2) {
5838
5909
  uiState
5839
5910
  } = state2;
5840
5911
  const [width, height] = mainWindow.getContentSize();
5841
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
5912
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$1;
5842
5913
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5843
5914
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5844
5915
  const chromeNeedsFullHeight = uiState.settingsOpen;
@@ -5888,7 +5959,7 @@ function layoutViews(state2) {
5888
5959
  function resizeSidebarViews(state2) {
5889
5960
  const { mainWindow, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
5890
5961
  const [width, height] = mainWindow.getContentSize();
5891
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
5962
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$1;
5892
5963
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5893
5964
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5894
5965
  const resizeHandleOverlap = 6;
@@ -6618,7 +6689,7 @@ const MAX_MCP_NAV_CONTENT_LENGTH = 3e4;
6618
6689
  const MAX_AGENT_DEBUG_CONTENT_LENGTH = 2e4;
6619
6690
  const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
6620
6691
  const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
6621
- const logger$d = createLogger("OpenAIProvider");
6692
+ const logger$e = createLogger("OpenAIProvider");
6622
6693
  function shouldDebugAgentLoop() {
6623
6694
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
6624
6695
  return value === "1" || value === "true";
@@ -7119,9 +7190,9 @@ function resolveToolCallName(rawName, args, availableToolNames) {
7119
7190
  function logAgentLoopDebug(payload) {
7120
7191
  if (!shouldDebugAgentLoop()) return;
7121
7192
  try {
7122
- logger$d.info(`[agent-debug] ${JSON.stringify(payload)}`);
7193
+ logger$e.info(`[agent-debug] ${JSON.stringify(payload)}`);
7123
7194
  } catch (err) {
7124
- logger$d.warn("Failed to serialize debug payload:", err);
7195
+ logger$e.warn("Failed to serialize debug payload:", err);
7125
7196
  }
7126
7197
  }
7127
7198
  function recoverTextEncodedToolCalls(text, availableToolNames) {
@@ -7755,7 +7826,7 @@ function createProvider(config) {
7755
7826
  return new OpenAICompatProvider(normalized);
7756
7827
  }
7757
7828
  const require$1 = node_module.createRequire(require("url").pathToFileURL(__filename).href);
7758
- const logger$c = createLogger("DevTrace");
7829
+ const logger$d = createLogger("DevTrace");
7759
7830
  let cachedFactory;
7760
7831
  function createNoopTraceSession() {
7761
7832
  return {
@@ -7788,7 +7859,7 @@ function loadLocalFactory() {
7788
7859
  return cachedFactory;
7789
7860
  }
7790
7861
  } catch (err) {
7791
- logger$c.warn("Failed to load local trace logger:", err);
7862
+ logger$d.warn("Failed to load local trace logger:", err);
7792
7863
  }
7793
7864
  }
7794
7865
  return cachedFactory;
@@ -11603,7 +11674,7 @@ function formatDeadLinkMessage(label, result) {
11603
11674
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
11604
11675
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
11605
11676
  }
11606
- const logger$b = createLogger("Screenshot");
11677
+ const logger$c = createLogger("Screenshot");
11607
11678
  const SCREENSHOT_RETRY_COUNT = 3;
11608
11679
  const SCREENSHOT_RETRY_BASE_DELAY_MS = 120;
11609
11680
  async function captureScreenshot(wc) {
@@ -11625,7 +11696,7 @@ async function captureScreenshot(wc) {
11625
11696
  }
11626
11697
  }
11627
11698
  } catch (err) {
11628
- logger$b.debug(
11699
+ logger$c.debug(
11629
11700
  `capturePage attempt ${attempt + 1} failed; retrying if attempts remain.`,
11630
11701
  getErrorMessage(err)
11631
11702
  );
@@ -12499,7 +12570,7 @@ function buildHuggingFaceSearchShortcut(currentUrl, rawQuery) {
12499
12570
  appliedFilters
12500
12571
  };
12501
12572
  }
12502
- const logger$a = createLogger("PageActions");
12573
+ const logger$b = createLogger("PageActions");
12503
12574
  function getBookmarkMetadataFromArgs(args) {
12504
12575
  return normalizeBookmarkMetadata({
12505
12576
  intent: args.intent ?? args.intent,
@@ -12685,7 +12756,7 @@ async function executePageScript(wc, script, options) {
12685
12756
  return result;
12686
12757
  } catch (err) {
12687
12758
  const label = options?.label ? ` (${options.label})` : "";
12688
- logger$a.warn(`Failed to execute page script${label}:`, err);
12759
+ logger$b.warn(`Failed to execute page script${label}:`, err);
12689
12760
  return null;
12690
12761
  } finally {
12691
12762
  if (timer) {
@@ -12786,7 +12857,7 @@ Search results snapshot:
12786
12857
  ${truncated}`;
12787
12858
  }
12788
12859
  } catch (err) {
12789
- logger$a.warn("Failed to build post-search summary, falling back to nav summary:", err);
12860
+ logger$b.warn("Failed to build post-search summary, falling back to nav summary:", err);
12790
12861
  }
12791
12862
  const fallback = await getPostNavSummary(wc);
12792
12863
  return fallback ? `${fallback}
@@ -12809,7 +12880,7 @@ Page snapshot after navigation:
12809
12880
  ${truncated}`;
12810
12881
  }
12811
12882
  } catch (err) {
12812
- logger$a.warn("Failed to build post-click navigation summary:", err);
12883
+ logger$b.warn("Failed to build post-click navigation summary:", err);
12813
12884
  }
12814
12885
  return "";
12815
12886
  }
@@ -13303,7 +13374,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
13303
13374
  }
13304
13375
  }
13305
13376
  } catch (err) {
13306
- logger$a.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
13377
+ logger$b.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
13307
13378
  }
13308
13379
  if (snapshot.url && snapshot.url !== wc.getURL()) {
13309
13380
  try {
@@ -13312,7 +13383,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
13312
13383
  await waitForLoad(wc, 3e3);
13313
13384
  return;
13314
13385
  } catch (err) {
13315
- logger$a.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
13386
+ logger$b.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
13316
13387
  }
13317
13388
  }
13318
13389
  if (snapshot.url) {
@@ -13320,7 +13391,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
13320
13391
  await wc.reload();
13321
13392
  await waitForLoad(wc, 3e3);
13322
13393
  } catch (err) {
13323
- logger$a.warn("Failed to restore locale via page reload:", err);
13394
+ logger$b.warn("Failed to restore locale via page reload:", err);
13324
13395
  }
13325
13396
  }
13326
13397
  }
@@ -13672,7 +13743,7 @@ ${postActivationOverlayHint}`;
13672
13743
  return `${clickText} -> ${hrefFallbackUrl} (recovered via href fallback)`;
13673
13744
  }
13674
13745
  } catch (err) {
13675
- logger$a.warn("Failed href fallback after click, returning generic click result:", err);
13746
+ logger$b.warn("Failed href fallback after click, returning generic click result:", err);
13676
13747
  }
13677
13748
  }
13678
13749
  }
@@ -13717,7 +13788,7 @@ async function tryAutoDismissCartDialog(wc) {
13717
13788
  return result;
13718
13789
  }
13719
13790
  } catch (err) {
13720
- logger$a.warn("Failed to auto-dismiss cart dialog, falling back to dialog actions:", err);
13791
+ logger$b.warn("Failed to auto-dismiss cart dialog, falling back to dialog actions:", err);
13721
13792
  }
13722
13793
  return null;
13723
13794
  }
@@ -15987,7 +16058,7 @@ async function executeAction(name, args, ctx) {
15987
16058
  )
15988
16059
  ]);
15989
16060
  } catch (err) {
15990
- logger$a.warn("Failed to extract content for read_page, falling back to lighter recovery:", err);
16061
+ logger$b.warn("Failed to extract content for read_page, falling back to lighter recovery:", err);
15991
16062
  content = null;
15992
16063
  }
15993
16064
  if (!content || content.content.length === 0) {
@@ -16004,12 +16075,12 @@ async function executeAction(name, args, ctx) {
16004
16075
  new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
16005
16076
  ]);
16006
16077
  } catch (err) {
16007
- logger$a.warn("Failed to re-extract content after iframe consent dismissal:", err);
16078
+ logger$b.warn("Failed to re-extract content after iframe consent dismissal:", err);
16008
16079
  content = null;
16009
16080
  }
16010
16081
  }
16011
16082
  } catch (err) {
16012
- logger$a.warn("Failed iframe consent dismissal during read_page recovery:", err);
16083
+ logger$b.warn("Failed iframe consent dismissal during read_page recovery:", err);
16013
16084
  }
16014
16085
  }
16015
16086
  if (content && content.content.length > 0) {
@@ -16422,7 +16493,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
16422
16493
  try {
16423
16494
  page = await extractContent(wc);
16424
16495
  } catch (err) {
16425
- logger$a.warn("Failed to extract content for suggest:", err);
16496
+ logger$b.warn("Failed to extract content for suggest:", err);
16426
16497
  return "Could not read page. Try navigate to a working URL.";
16427
16498
  }
16428
16499
  const suggestions = [];
@@ -17753,7 +17824,7 @@ const ALGORITHM = "aes-256-gcm";
17753
17824
  const IV_LENGTH = 12;
17754
17825
  const AUTH_TAG_LENGTH = 16;
17755
17826
  let cachedEntries = null;
17756
- const logger$9 = createLogger("Vault");
17827
+ const logger$a = createLogger("Vault");
17757
17828
  function getVaultDir() {
17758
17829
  return electron.app.getPath("userData");
17759
17830
  }
@@ -17820,7 +17891,7 @@ function loadVault() {
17820
17891
  cachedEntries = JSON.parse(json);
17821
17892
  return cachedEntries;
17822
17893
  } catch (err) {
17823
- logger$9.error("Failed to load vault:", err);
17894
+ logger$a.error("Failed to load vault:", err);
17824
17895
  throw new Error(
17825
17896
  "Could not unlock the Agent Credential Vault. Check that OS secret storage is available and that the stored vault key can be decrypted."
17826
17897
  );
@@ -17962,7 +18033,7 @@ async function requestConsent(request) {
17962
18033
  }
17963
18034
  const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
17964
18035
  const MAX_ENTRIES = 1e3;
17965
- const logger$8 = createLogger("VaultAudit");
18036
+ const logger$9 = createLogger("VaultAudit");
17966
18037
  function getAuditPath() {
17967
18038
  return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
17968
18039
  }
@@ -17972,7 +18043,7 @@ function appendAuditEntry(entry) {
17972
18043
  fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
17973
18044
  fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
17974
18045
  } catch (err) {
17975
- logger$8.error("Failed to write audit log:", err);
18046
+ logger$9.error("Failed to write audit log:", err);
17976
18047
  }
17977
18048
  }
17978
18049
  function readAuditLog(limit = 100) {
@@ -17982,13 +18053,13 @@ function readAuditLog(limit = 100) {
17982
18053
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
17983
18054
  return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
17984
18055
  } catch (err) {
17985
- logger$8.error("Failed to read audit log:", err);
18056
+ logger$9.error("Failed to read audit log:", err);
17986
18057
  return [];
17987
18058
  }
17988
18059
  }
17989
18060
  let httpServer = null;
17990
18061
  let mcpAuthToken = null;
17991
- const logger$7 = createLogger("MCP");
18062
+ const logger$8 = createLogger("MCP");
17992
18063
  const MCP_AUTH_FILENAME = "mcp-auth.json";
17993
18064
  function getMcpAuthFilePath() {
17994
18065
  const configDir = process.env.VESSEL_CONFIG_DIR || path$1.join(
@@ -18024,7 +18095,7 @@ function writeMcpAuthFile(endpoint, token) {
18024
18095
  { mode: 384 }
18025
18096
  );
18026
18097
  } catch (err) {
18027
- logger$7.warn("Failed to write auth file:", err);
18098
+ logger$8.warn("Failed to write auth file:", err);
18028
18099
  }
18029
18100
  }
18030
18101
  function clearMcpAuthFile() {
@@ -18049,7 +18120,7 @@ function clearMcpAuthFile() {
18049
18120
  { mode: 384 }
18050
18121
  );
18051
18122
  } catch (err) {
18052
- logger$7.warn("Failed to clear auth file:", err);
18123
+ logger$8.warn("Failed to clear auth file:", err);
18053
18124
  }
18054
18125
  }
18055
18126
  function asTextResponse(text) {
@@ -18139,7 +18210,7 @@ async function getPostActionState(tabManager, name) {
18139
18210
  }
18140
18211
  }
18141
18212
  } catch (err) {
18142
- logger$7.warn("Failed to compute post-action state warning:", err);
18213
+ logger$8.warn("Failed to compute post-action state warning:", err);
18143
18214
  }
18144
18215
  return `${warning}
18145
18216
  [state: url=${wc.getURL()}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]`;
@@ -18241,7 +18312,7 @@ async function waitForConditionMcp(wc, text, selector, timeoutMs) {
18241
18312
  }
18242
18313
  })()
18243
18314
  `).catch((err) => {
18244
- logger$7.warn("Failed to gather wait_for timeout diagnostic:", err);
18315
+ logger$8.warn("Failed to gather wait_for timeout diagnostic:", err);
18245
18316
  return null;
18246
18317
  });
18247
18318
  if (typeof diagnostic === "string" && diagnostic.trim()) {
@@ -18328,7 +18399,7 @@ function registerTools(server, tabManager, runtime2) {
18328
18399
  const page = await extractContent(wc);
18329
18400
  pageType = detectPageType(page);
18330
18401
  } catch (err) {
18331
- logger$7.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
18402
+ logger$8.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
18332
18403
  }
18333
18404
  }
18334
18405
  const scored = TOOL_DEFINITIONS.map((def) => {
@@ -19612,7 +19683,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
19612
19683
  void 0,
19613
19684
  h.color
19614
19685
  ).catch(
19615
- (err) => logger$7.warn("Failed to restore highlight after removal:", err)
19686
+ (err) => logger$8.warn("Failed to restore highlight after removal:", err)
19616
19687
  );
19617
19688
  }
19618
19689
  }
@@ -20460,7 +20531,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
20460
20531
  try {
20461
20532
  page = await extractContent(wc);
20462
20533
  } catch (err) {
20463
- logger$7.warn("Failed to extract page while generating suggestions:", err);
20534
+ logger$8.warn("Failed to extract page while generating suggestions:", err);
20464
20535
  return asTextResponse(
20465
20536
  "Could not read page. Try navigate to a working URL."
20466
20537
  );
@@ -21069,7 +21140,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
21069
21140
  try {
21070
21141
  targetDomain = new URL(tab.state.url).hostname;
21071
21142
  } catch (err) {
21072
- logger$7.warn("Failed to parse active tab URL for vault_status:", err);
21143
+ logger$8.warn("Failed to parse active tab URL for vault_status:", err);
21073
21144
  return asErrorTextResponse("Could not parse active tab URL");
21074
21145
  }
21075
21146
  }
@@ -21135,7 +21206,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
21135
21206
  try {
21136
21207
  hostname = new URL(tab.state.url).hostname;
21137
21208
  } catch (err) {
21138
- logger$7.warn("Failed to parse active tab URL for vault_login:", err);
21209
+ logger$8.warn("Failed to parse active tab URL for vault_login:", err);
21139
21210
  return asErrorTextResponse("Could not parse active tab URL");
21140
21211
  }
21141
21212
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -21229,7 +21300,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
21229
21300
  try {
21230
21301
  hostname = new URL(tab.state.url).hostname;
21231
21302
  } catch (err) {
21232
- logger$7.warn("Failed to parse active tab URL for vault_totp:", err);
21303
+ logger$8.warn("Failed to parse active tab URL for vault_totp:", err);
21233
21304
  return asErrorTextResponse("Could not parse active tab URL");
21234
21305
  }
21235
21306
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -21401,7 +21472,7 @@ function startMcpServer(tabManager, runtime2, port) {
21401
21472
  await mcpServer.connect(transport);
21402
21473
  await transport.handleRequest(req, res);
21403
21474
  } catch (error) {
21404
- logger$7.error("Error handling request:", error);
21475
+ logger$8.error("Error handling request:", error);
21405
21476
  if (!res.headersSent) {
21406
21477
  res.writeHead(500, { "Content-Type": "application/json" });
21407
21478
  res.end(
@@ -21420,7 +21491,7 @@ function startMcpServer(tabManager, runtime2, port) {
21420
21491
  };
21421
21492
  server.once("error", (error) => {
21422
21493
  const message = error.code === "EADDRINUSE" ? `Port ${port} is already in use. MCP server not started.` : error.message;
21423
- logger$7.error("Server error:", error);
21494
+ logger$8.error("Server error:", error);
21424
21495
  clearMcpAuthFile();
21425
21496
  setMcpHealth({
21426
21497
  configuredPort: port,
@@ -21452,7 +21523,7 @@ function startMcpServer(tabManager, runtime2, port) {
21452
21523
  message: `MCP server listening on ${endpoint}.`
21453
21524
  });
21454
21525
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
21455
- logger$7.info(`Server listening on ${endpoint} (auth enabled)`);
21526
+ logger$8.info(`Server listening on ${endpoint} (auth enabled)`);
21456
21527
  }
21457
21528
  if (mcpAuthToken) {
21458
21529
  writeMcpAuthFile(endpoint, mcpAuthToken);
@@ -21491,7 +21562,7 @@ function stopMcpServer() {
21491
21562
  message: "MCP server is stopped."
21492
21563
  });
21493
21564
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
21494
- logger$7.info("Server stopped");
21565
+ logger$8.info("Server stopped");
21495
21566
  }
21496
21567
  resolve();
21497
21568
  });
@@ -21512,7 +21583,7 @@ const KIT_ID_UNSAFE_CHAR_PATTERN = /[\/\\\0]/;
21512
21583
  function isSafeAutomationKitId(id) {
21513
21584
  return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
21514
21585
  }
21515
- const logger$6 = createLogger("KitRegistry");
21586
+ const logger$7 = createLogger("KitRegistry");
21516
21587
  function getUserKitsDir() {
21517
21588
  return path$1.join(electron.app.getPath("userData"), "kits");
21518
21589
  }
@@ -21550,10 +21621,10 @@ function getInstalledKits() {
21550
21621
  if (isValidKit(parsed)) {
21551
21622
  kits.push(parsed);
21552
21623
  } else {
21553
- logger$6.warn(`Skipping invalid kit file: ${file}`);
21624
+ logger$7.warn(`Skipping invalid kit file: ${file}`);
21554
21625
  }
21555
21626
  } catch (err) {
21556
- logger$6.warn(`Failed to read kit file: ${file}`, err);
21627
+ logger$7.warn(`Failed to read kit file: ${file}`, err);
21557
21628
  }
21558
21629
  }
21559
21630
  return kits;
@@ -21625,7 +21696,7 @@ function uninstallKit(id, scheduledKitIds) {
21625
21696
  return errorResult("Failed to remove the kit file.");
21626
21697
  }
21627
21698
  }
21628
- const logger$5 = createLogger("Scheduler");
21699
+ const logger$6 = createLogger("Scheduler");
21629
21700
  let jobs = [];
21630
21701
  let removeIdleListener = null;
21631
21702
  let broadcastFn = null;
@@ -21650,7 +21721,7 @@ function saveJobs() {
21650
21721
  try {
21651
21722
  fs$1.writeFileSync(getJobsPath(), JSON.stringify(jobs, null, 2), "utf-8");
21652
21723
  } catch (err) {
21653
- logger$5.warn("Failed to save jobs:", err);
21724
+ logger$6.warn("Failed to save jobs:", err);
21654
21725
  }
21655
21726
  }
21656
21727
  function normalizeJob(job, now = /* @__PURE__ */ new Date()) {
@@ -21772,7 +21843,7 @@ async function fireJob(job, windowState, runtime2) {
21772
21843
  };
21773
21844
  startActivity();
21774
21845
  if (!settings2.chatProvider) {
21775
- logger$5.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
21846
+ logger$6.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
21776
21847
  appendActivity(
21777
21848
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
21778
21849
  );
@@ -21780,7 +21851,7 @@ async function fireJob(job, windowState, runtime2) {
21780
21851
  return;
21781
21852
  }
21782
21853
  if (process.env.VESSEL_DEBUG_SCHEDULER === "1" || process.env.VESSEL_DEBUG_SCHEDULER === "true") {
21783
- logger$5.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
21854
+ logger$6.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
21784
21855
  }
21785
21856
  try {
21786
21857
  const provider = createProvider(settings2.chatProvider);
@@ -21833,7 +21904,7 @@ function tick(windowState, runtime2) {
21833
21904
  saveJobs();
21834
21905
  broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
21835
21906
  void fireJob(job, windowState, runtime2).catch((err) => {
21836
- logger$5.warn("Unexpected error firing job:", err);
21907
+ logger$6.warn("Unexpected error firing job:", err);
21837
21908
  }).finally(fireNext);
21838
21909
  };
21839
21910
  fireNext();
@@ -22403,93 +22474,540 @@ function registerWindowControlHandlers(mainWindow) {
22403
22474
  mainWindow.close();
22404
22475
  });
22405
22476
  }
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
- );
22477
+ const BLOCKED_RESOURCE_TYPES = /* @__PURE__ */ new Set([
22478
+ "script",
22479
+ "image",
22480
+ "xhr",
22481
+ "fetch",
22482
+ "subFrame",
22483
+ "media",
22484
+ "ping",
22485
+ "webSocket"
22486
+ ]);
22487
+ const BLOCKED_HOST_SUFFIXES = [
22488
+ "doubleclick.net",
22489
+ "googlesyndication.com",
22490
+ "googleadservices.com",
22491
+ "adservice.google.com",
22492
+ "adnxs.com",
22493
+ "adsrvr.org",
22494
+ "taboola.com",
22495
+ "outbrain.com",
22496
+ "criteo.com",
22497
+ "criteo.net",
22498
+ "pubmatic.com",
22499
+ "rubiconproject.com",
22500
+ "openx.net",
22501
+ "casalemedia.com",
22502
+ "advertising.com",
22503
+ "amazon-adsystem.com",
22504
+ "adsymptotic.com",
22505
+ "moatads.com",
22506
+ "quantserve.com",
22507
+ "scorecardresearch.com"
22508
+ ];
22509
+ const THIRD_PARTY_PATH_PATTERNS = [
22510
+ /\/ads?[/?._-]/i,
22511
+ /\/adservice/i,
22512
+ /\/advert/i,
22513
+ /\/prebid/i,
22514
+ /\/banner/i,
22515
+ /\/sponsor/i,
22516
+ /\/promotions?\//i,
22517
+ /\/trk\//i,
22518
+ /\/track(ing)?\//i,
22519
+ /\/beacon/i,
22520
+ /\/pixel/i
22521
+ ];
22522
+ let installed = false;
22523
+ function normalizeHostname(value) {
22524
+ return value.trim().toLowerCase().replace(/\.$/, "");
22525
+ }
22526
+ function hostnameMatches(hostname, suffix) {
22527
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
22528
+ }
22529
+ function parseHostname(url) {
22530
+ try {
22531
+ const parsed = new URL(url);
22532
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
22533
+ return null;
22441
22534
  }
22442
- if (!sidebarView.webContents.isDestroyed()) {
22443
- sidebarView.webContents.send(
22444
- Channels.AGENT_RUNTIME_UPDATE,
22445
- pendingRuntimeState
22446
- );
22535
+ return normalizeHostname(parsed.hostname);
22536
+ } catch {
22537
+ return null;
22538
+ }
22539
+ }
22540
+ function isThirdParty(url, firstPartyHost) {
22541
+ if (!firstPartyHost) return true;
22542
+ const target = normalizeHostname(url.hostname);
22543
+ return !(target === firstPartyHost || target.endsWith(`.${firstPartyHost}`));
22544
+ }
22545
+ function shouldBlockRequest(details) {
22546
+ if (!BLOCKED_RESOURCE_TYPES.has(details.resourceType)) return false;
22547
+ let parsed;
22548
+ try {
22549
+ parsed = new URL(details.url);
22550
+ } catch {
22551
+ return false;
22552
+ }
22553
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
22554
+ return false;
22555
+ }
22556
+ const hostname = normalizeHostname(parsed.hostname);
22557
+ if (BLOCKED_HOST_SUFFIXES.some((suffix) => hostnameMatches(hostname, suffix))) {
22558
+ return true;
22559
+ }
22560
+ const firstPartyHost = parseHostname(details.referrer) || parseHostname(details.initiator || "");
22561
+ if (!isThirdParty(parsed, firstPartyHost)) return false;
22562
+ const candidate = `${hostname}${parsed.pathname}${parsed.search}`;
22563
+ return THIRD_PARTY_PATH_PATTERNS.some((pattern) => pattern.test(candidate));
22564
+ }
22565
+ function installAdBlocking(tabManager) {
22566
+ if (installed) return;
22567
+ installed = true;
22568
+ electron.session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
22569
+ const webContentsId = typeof details.webContentsId === "number" ? details.webContentsId : null;
22570
+ if (webContentsId == null) {
22571
+ callback({});
22572
+ return;
22447
22573
  }
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;
22574
+ if (!tabManager.isAdBlockingEnabledForWebContents(webContentsId)) {
22575
+ callback({});
22576
+ return;
22461
22577
  }
22462
- flushRuntimeUpdate();
22578
+ callback({ cancel: shouldBlockRequest(details) });
22463
22579
  });
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);
22580
+ }
22581
+ function installAdBlockingForSession(ses, tabManager) {
22582
+ ses.webRequest.onBeforeRequest((details, callback) => {
22583
+ const webContentsId = typeof details.webContentsId === "number" ? details.webContentsId : null;
22584
+ if (webContentsId == null) {
22585
+ callback({});
22586
+ return;
22587
+ }
22588
+ if (!tabManager.isAdBlockingEnabledForWebContents(webContentsId)) {
22589
+ callback({});
22590
+ return;
22591
+ }
22592
+ callback({ cancel: shouldBlockRequest(details) });
22593
+ });
22594
+ }
22595
+ function resolveDownloadPath(downloadDir, filename) {
22596
+ fs$1.mkdirSync(downloadDir, { recursive: true });
22597
+ const parsed = path.parse(filename);
22598
+ let attempt = 0;
22599
+ while (true) {
22600
+ const candidateName = attempt === 0 ? filename : `${parsed.name} (${attempt})${parsed.ext}`;
22601
+ const candidatePath = path.join(downloadDir, candidateName);
22602
+ if (!fs$1.existsSync(candidatePath)) {
22603
+ return candidatePath;
22604
+ }
22605
+ attempt += 1;
22606
+ }
22607
+ }
22608
+ function installDownloadHandler(chromeView) {
22609
+ installDownloadHandlerForSession(electron.session.defaultSession, chromeView);
22610
+ }
22611
+ function installDownloadHandlerForSession(targetSession, chromeView) {
22612
+ targetSession.on("will-download", (_event, item) => {
22613
+ const settings2 = loadSettings();
22614
+ const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
22615
+ const filename = item.getFilename();
22616
+ const savePath = resolveDownloadPath(downloadDir, filename);
22617
+ item.setSavePath(savePath);
22618
+ const info = {
22619
+ filename,
22620
+ savePath,
22621
+ totalBytes: item.getTotalBytes(),
22622
+ receivedBytes: 0,
22623
+ state: "progressing"
22478
22624
  };
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();
22625
+ if (!chromeView.webContents.isDestroyed()) {
22626
+ chromeView.webContents.send(Channels.DOWNLOAD_STARTED, info);
22627
+ }
22628
+ item.on("updated", (_event2, state2) => {
22629
+ info.receivedBytes = item.getReceivedBytes();
22630
+ info.totalBytes = item.getTotalBytes();
22631
+ info.state = state2 === "progressing" ? "progressing" : "interrupted";
22632
+ if (!chromeView.webContents.isDestroyed()) {
22633
+ chromeView.webContents.send(Channels.DOWNLOAD_PROGRESS, info);
22634
+ }
22635
+ });
22636
+ item.once("done", (_event2, state2) => {
22637
+ info.receivedBytes = item.getReceivedBytes();
22638
+ info.state = state2 === "completed" ? "completed" : "cancelled";
22639
+ if (!chromeView.webContents.isDestroyed()) {
22640
+ chromeView.webContents.send(Channels.DOWNLOAD_DONE, info);
22641
+ }
22642
+ });
22643
+ });
22644
+ }
22645
+ const logger$5 = createLogger("PrivateWindow");
22646
+ const CHROME_HEIGHT = 110;
22647
+ const privateWindows = /* @__PURE__ */ new Set();
22648
+ function resolveRendererFile$1() {
22649
+ const candidates = [
22650
+ path.join(__dirname, "../../out/renderer/index.html"),
22651
+ path.join(__dirname, "../../../out/renderer/index.html")
22652
+ ];
22653
+ for (const c of candidates) {
22654
+ try {
22655
+ fs.accessSync(c);
22656
+ return c;
22657
+ } catch {
22658
+ }
22659
+ }
22660
+ return path.join(__dirname, "../../out/renderer/index.html");
22661
+ }
22662
+ function layoutPrivateViews(state2) {
22663
+ const { window: win, chromeView, tabManager } = state2;
22664
+ const [width, height] = win.getContentSize();
22665
+ chromeView.setBounds({ x: 0, y: 0, width, height: CHROME_HEIGHT });
22666
+ win.contentView.removeChildView(chromeView);
22667
+ win.contentView.addChildView(chromeView);
22668
+ const activeTab = tabManager.getActiveTab();
22669
+ if (activeTab) {
22670
+ activeTab.view.setBounds({
22671
+ x: 0,
22672
+ y: CHROME_HEIGHT,
22673
+ width,
22674
+ height: height - CHROME_HEIGHT
22675
+ });
22676
+ }
22677
+ }
22678
+ function loadPrivateRenderer(chromeView) {
22679
+ const devUrl = process.env.ELECTRON_RENDERER_URL;
22680
+ if (devUrl) {
22681
+ const url = new URL(devUrl);
22682
+ url.searchParams.set("view", "chrome");
22683
+ url.searchParams.set("private", "1");
22684
+ chromeView.webContents.loadURL(url.toString());
22685
+ } else {
22686
+ chromeView.webContents.loadFile(resolveRendererFile$1(), {
22687
+ query: { view: "chrome", private: "1" }
22688
+ });
22689
+ }
22690
+ }
22691
+ function registerPrivateIpcHandlers(state2) {
22692
+ const { chromeView, tabManager } = state2;
22693
+ const ipc = chromeView.webContents.ipc;
22694
+ let findResultListener = null;
22695
+ let findWiredWcId = null;
22696
+ const wireFindEvents = (wc) => {
22697
+ if (findWiredWcId === wc.id && findResultListener) return;
22698
+ if (findWiredWcId && findResultListener) {
22699
+ const previous = tabManager.findTabByWebContentsId(findWiredWcId);
22700
+ previous?.view.webContents.removeListener(
22701
+ "found-in-page",
22702
+ findResultListener
22703
+ );
22704
+ }
22705
+ findWiredWcId = wc.id;
22706
+ if (wc.isDestroyed()) return;
22707
+ const listener = (_event, result) => {
22708
+ if (!chromeView.webContents.isDestroyed()) {
22709
+ chromeView.webContents.send(Channels.FIND_IN_PAGE_RESULT, result);
22710
+ }
22711
+ };
22712
+ findResultListener = listener;
22713
+ wc.on("found-in-page", listener);
22714
+ const capturedWcId = wc.id;
22715
+ wc.once("destroyed", () => {
22716
+ if (findWiredWcId === capturedWcId) {
22717
+ findWiredWcId = null;
22718
+ findResultListener = null;
22719
+ }
22720
+ });
22721
+ };
22722
+ ipc.handle(Channels.TAB_CREATE, (_e, url) => {
22723
+ return tabManager.createTab(url);
22724
+ });
22725
+ ipc.handle(Channels.TAB_CLOSE, (_e, id) => {
22726
+ tabManager.closeTab(id);
22727
+ layoutPrivateViews(state2);
22728
+ });
22729
+ ipc.handle(Channels.TAB_SWITCH, (_e, id) => {
22730
+ tabManager.switchTab(id);
22731
+ layoutPrivateViews(state2);
22732
+ });
22733
+ ipc.handle(Channels.TAB_NAVIGATE, (_e, id, url) => {
22734
+ return tabManager.navigateTab(id, url);
22735
+ });
22736
+ ipc.handle(Channels.TAB_BACK, (_e, id) => {
22737
+ tabManager.goBack(id);
22738
+ });
22739
+ ipc.handle(Channels.TAB_FORWARD, (_e, id) => {
22740
+ tabManager.goForward(id);
22741
+ });
22742
+ ipc.handle(Channels.TAB_RELOAD, (_e, id) => {
22743
+ tabManager.reloadTab(id);
22744
+ });
22745
+ ipc.handle(Channels.TAB_TOGGLE_AD_BLOCK, (_e, id) => {
22746
+ const tab = tabManager.getTab(id);
22747
+ if (!tab) return null;
22748
+ const newState = !tab.state.adBlockingEnabled;
22749
+ tab.setAdBlockingEnabled(newState);
22750
+ return newState;
22751
+ });
22752
+ ipc.handle(Channels.TAB_ZOOM_IN, (_e, id) => {
22753
+ tabManager.zoomIn(id);
22754
+ });
22755
+ ipc.handle(Channels.TAB_ZOOM_OUT, (_e, id) => {
22756
+ tabManager.zoomOut(id);
22757
+ });
22758
+ ipc.handle(Channels.TAB_ZOOM_RESET, (_e, id) => {
22759
+ tabManager.zoomReset(id);
22760
+ });
22761
+ ipc.handle(Channels.TAB_STATE_GET, () => ({
22762
+ tabs: tabManager.getAllStates(),
22763
+ activeId: tabManager.getActiveTabId() || ""
22764
+ }));
22765
+ ipc.handle(Channels.TAB_REOPEN_CLOSED, () => {
22766
+ const id = tabManager.reopenClosedTab();
22767
+ if (id) layoutPrivateViews(state2);
22768
+ return id;
22769
+ });
22770
+ ipc.handle(Channels.TAB_DUPLICATE, (_e, id) => {
22771
+ const newId = tabManager.duplicateTab(id);
22772
+ if (newId) layoutPrivateViews(state2);
22773
+ return newId;
22774
+ });
22775
+ ipc.on(Channels.TAB_CONTEXT_MENU, (_e, id) => {
22776
+ const { Menu, MenuItem } = require("electron");
22777
+ const menu = new Menu();
22778
+ menu.append(
22779
+ new MenuItem({
22780
+ label: "Duplicate Tab",
22781
+ click: () => {
22782
+ const newId = tabManager.duplicateTab(id);
22783
+ if (newId) layoutPrivateViews(state2);
22784
+ }
22785
+ })
22786
+ );
22787
+ menu.append(
22788
+ new MenuItem({
22789
+ label: "Close Tab",
22790
+ click: () => {
22791
+ tabManager.closeTab(id);
22792
+ layoutPrivateViews(state2);
22793
+ }
22794
+ })
22795
+ );
22796
+ menu.popup({ window: state2.window });
22797
+ });
22798
+ ipc.handle(Channels.IS_PRIVATE_MODE, () => true);
22799
+ ipc.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
22800
+ createPrivateWindow();
22801
+ });
22802
+ ipc.handle(Channels.WINDOW_MINIMIZE, () => {
22803
+ state2.window.minimize();
22804
+ });
22805
+ ipc.handle(Channels.WINDOW_MAXIMIZE, () => {
22806
+ if (state2.window.isMaximized()) {
22807
+ state2.window.unmaximize();
22808
+ } else {
22809
+ state2.window.maximize();
22810
+ }
22811
+ });
22812
+ ipc.handle(Channels.WINDOW_CLOSE, () => {
22813
+ state2.window.close();
22814
+ });
22815
+ ipc.handle(Channels.SETTINGS_VISIBILITY, () => {
22816
+ return false;
22817
+ });
22818
+ ipc.handle(Channels.FOCUS_MODE_TOGGLE, () => false);
22819
+ ipc.handle(Channels.SIDEBAR_TOGGLE, () => ({ open: false, width: 0 }));
22820
+ ipc.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => ({ open: false }));
22821
+ ipc.handle(
22822
+ Channels.FIND_IN_PAGE_START,
22823
+ (_e, text, options) => {
22824
+ const tab = tabManager.getActiveTab();
22825
+ if (!tab) return null;
22826
+ const wc = tab.view.webContents;
22827
+ if (wc.isDestroyed()) return null;
22828
+ wireFindEvents(wc);
22829
+ return wc.findInPage(text, {
22830
+ forward: options?.forward ?? true,
22831
+ findNext: options?.findNext ?? false
22832
+ });
22833
+ }
22834
+ );
22835
+ ipc.handle(Channels.FIND_IN_PAGE_NEXT, (_e, forward) => {
22836
+ const tab = tabManager.getActiveTab();
22837
+ if (!tab) return null;
22838
+ const wc = tab.view.webContents;
22839
+ if (wc.isDestroyed()) return null;
22840
+ wireFindEvents(wc);
22841
+ return wc.findInPage("", { forward: forward ?? true, findNext: true });
22842
+ });
22843
+ ipc.handle(
22844
+ Channels.FIND_IN_PAGE_STOP,
22845
+ (_e, action) => {
22846
+ const tab = tabManager.getActiveTab();
22847
+ if (!tab) return;
22848
+ const wc = tab.view.webContents;
22849
+ if (wc.isDestroyed()) return;
22850
+ wc.stopFindInPage(action ?? "clearSelection");
22851
+ }
22852
+ );
22853
+ }
22854
+ function createPrivateWindow() {
22855
+ const privateSessionPartition = `private-${crypto$1.randomUUID()}`;
22856
+ const privateSession = electron.session.fromPartition(privateSessionPartition);
22857
+ privateSession.setUserAgent(electron.session.defaultSession.getUserAgent());
22858
+ const win = new electron.BaseWindow({
22859
+ width: 1280,
22860
+ height: 800,
22861
+ minWidth: 800,
22862
+ minHeight: 600,
22863
+ frame: false,
22864
+ show: false,
22865
+ backgroundColor: "#1e1a2e",
22866
+ title: "Vessel - Private Browsing"
22867
+ });
22868
+ const chromeView = new electron.WebContentsView({
22869
+ webPreferences: {
22870
+ preload: path.join(__dirname, "../preload/index.js"),
22871
+ sandbox: true,
22872
+ contextIsolation: true,
22873
+ nodeIntegration: false
22874
+ }
22875
+ });
22876
+ chromeView.setBackgroundColor("#00000000");
22877
+ win.contentView.addChildView(chromeView);
22878
+ const tabManager = new TabManager(
22879
+ win,
22880
+ (tabs, activeId) => {
22881
+ if (!chromeView.webContents.isDestroyed()) {
22882
+ chromeView.webContents.send(Channels.TAB_STATE_UPDATE, tabs, activeId);
22883
+ }
22884
+ layoutPrivateViews(state2);
22885
+ },
22886
+ { isPrivate: true, sessionPartition: privateSessionPartition }
22887
+ );
22888
+ const state2 = {
22889
+ window: win,
22890
+ chromeView,
22891
+ tabManager,
22892
+ session: privateSession,
22893
+ sessionPartition: privateSessionPartition
22894
+ };
22895
+ installAdBlockingForSession(privateSession, tabManager);
22896
+ installDownloadHandlerForSession(privateSession, chromeView);
22897
+ registerPrivateIpcHandlers(state2);
22898
+ win.on("resize", () => layoutPrivateViews(state2));
22899
+ win.on("show", () => layoutPrivateViews(state2));
22900
+ win.on("closed", () => {
22901
+ privateWindows.delete(state2);
22902
+ tabManager.destroyAllTabs();
22903
+ void Promise.all([
22904
+ privateSession.clearStorageData(),
22905
+ privateSession.clearCache()
22906
+ ]).catch((error) => {
22907
+ logger$5.warn("Failed to clear private browsing session:", error);
22908
+ });
22909
+ });
22910
+ privateWindows.add(state2);
22911
+ chromeView.webContents.once("dom-ready", () => {
22912
+ tabManager.createTab("about:blank");
22913
+ layoutPrivateViews(state2);
22914
+ });
22915
+ loadPrivateRenderer(chromeView);
22916
+ win.show();
22917
+ logger$5.info("Private browsing window opened");
22918
+ return state2;
22919
+ }
22920
+ let activeChatProvider = null;
22921
+ const logger$4 = createLogger("IPC");
22922
+ const VALID_APPROVAL_MODES = ["auto", "confirm-dangerous", "manual"];
22923
+ function registerIpcHandlers(windowState, runtime2) {
22924
+ const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
22925
+ electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
22926
+ createPrivateWindow();
22927
+ });
22928
+ electron.ipcMain.handle(Channels.IS_PRIVATE_MODE, () => false);
22929
+ let sidebarResizeRecoveryTimer = null;
22930
+ let sidebarResizeActive = false;
22931
+ let runtimeUpdateTimer = null;
22932
+ let pendingRuntimeState = null;
22933
+ const premiumApiOrigin = process.env.VESSEL_PREMIUM_API ? new URL(process.env.VESSEL_PREMIUM_API).origin : "https://vesselpremium.quantaintellect.com";
22934
+ const clearSidebarResizeRecoveryTimer = () => {
22935
+ if (!sidebarResizeRecoveryTimer) return;
22936
+ clearTimeout(sidebarResizeRecoveryTimer);
22937
+ sidebarResizeRecoveryTimer = null;
22938
+ };
22939
+ const restoreSidebarLayoutAfterResize = () => {
22940
+ clearSidebarResizeRecoveryTimer();
22941
+ if (!sidebarResizeActive) return;
22942
+ sidebarResizeActive = false;
22943
+ layoutViews(windowState);
22944
+ };
22945
+ const scheduleSidebarResizeRecovery = () => {
22946
+ clearSidebarResizeRecoveryTimer();
22947
+ sidebarResizeRecoveryTimer = setTimeout(() => {
22948
+ restoreSidebarLayoutAfterResize();
22949
+ }, 1200);
22950
+ };
22951
+ const flushRuntimeUpdate = () => {
22952
+ runtimeUpdateTimer = null;
22953
+ if (!pendingRuntimeState) return;
22954
+ if (!chromeView.webContents.isDestroyed()) {
22955
+ chromeView.webContents.send(
22956
+ Channels.AGENT_RUNTIME_UPDATE,
22957
+ pendingRuntimeState
22958
+ );
22959
+ }
22960
+ if (!sidebarView.webContents.isDestroyed()) {
22961
+ sidebarView.webContents.send(
22962
+ Channels.AGENT_RUNTIME_UPDATE,
22963
+ pendingRuntimeState
22964
+ );
22965
+ }
22966
+ pendingRuntimeState = null;
22967
+ };
22968
+ const scheduleRuntimeUpdate = (state2) => {
22969
+ pendingRuntimeState = state2;
22970
+ if (runtimeUpdateTimer) return;
22971
+ runtimeUpdateTimer = setTimeout(() => {
22972
+ flushRuntimeUpdate();
22973
+ }, 32);
22974
+ };
22975
+ electron.app.on("before-quit", () => {
22976
+ if (runtimeUpdateTimer) {
22977
+ clearTimeout(runtimeUpdateTimer);
22978
+ runtimeUpdateTimer = null;
22979
+ }
22980
+ flushRuntimeUpdate();
22981
+ });
22982
+ const sendToRendererViews = (channel, ...args) => {
22983
+ chromeView.webContents.send(channel, ...args);
22984
+ sidebarView.webContents.send(channel, ...args);
22985
+ devtoolsPanelView.webContents.send(channel, ...args);
22986
+ };
22987
+ const watchPremiumCheckoutTab = (tabId) => {
22988
+ const tab = tabManager.getTab(tabId);
22989
+ const wc = tab?.view.webContents;
22990
+ if (!wc) return;
22991
+ let completed = false;
22992
+ const cleanup = () => {
22993
+ wc.removeListener("did-navigate", onNavigate);
22994
+ wc.removeListener("did-navigate-in-page", onNavigateInPage);
22995
+ wc.removeListener("destroyed", cleanup);
22996
+ };
22997
+ const handleUrl = async (rawUrl) => {
22998
+ if (completed) return;
22999
+ let parsed;
23000
+ try {
23001
+ parsed = new URL(rawUrl);
23002
+ } catch (err) {
23003
+ logger$4.warn("Failed to parse premium checkout URL while watching checkout tab:", err);
23004
+ return;
23005
+ }
23006
+ if (parsed.origin !== premiumApiOrigin) return;
23007
+ if (parsed.pathname === "/canceled") {
23008
+ completed = true;
23009
+ trackPremiumFunnel("checkout_canceled");
23010
+ cleanup();
22493
23011
  return;
22494
23012
  }
22495
23013
  if (parsed.pathname !== "/success") return;
@@ -22595,6 +23113,52 @@ function registerIpcHandlers(windowState, runtime2) {
22595
23113
  tab.setAdBlockingEnabled(newState);
22596
23114
  return newState;
22597
23115
  });
23116
+ electron.ipcMain.handle(Channels.TAB_ZOOM_IN, (_, id) => {
23117
+ assertString(id, "id");
23118
+ tabManager.zoomIn(id);
23119
+ });
23120
+ electron.ipcMain.handle(Channels.TAB_ZOOM_OUT, (_, id) => {
23121
+ assertString(id, "id");
23122
+ tabManager.zoomOut(id);
23123
+ });
23124
+ electron.ipcMain.handle(Channels.TAB_ZOOM_RESET, (_, id) => {
23125
+ assertString(id, "id");
23126
+ tabManager.zoomReset(id);
23127
+ });
23128
+ electron.ipcMain.handle(Channels.TAB_REOPEN_CLOSED, () => {
23129
+ const id = tabManager.reopenClosedTab();
23130
+ if (id) layoutViews(windowState);
23131
+ return id;
23132
+ });
23133
+ electron.ipcMain.handle(Channels.TAB_DUPLICATE, (_, id) => {
23134
+ assertString(id, "id");
23135
+ const newId = tabManager.duplicateTab(id);
23136
+ if (newId) layoutViews(windowState);
23137
+ return newId;
23138
+ });
23139
+ electron.ipcMain.on(Channels.TAB_CONTEXT_MENU, (_event, id) => {
23140
+ assertString(id, "id");
23141
+ const menu = new electron.Menu();
23142
+ menu.append(
23143
+ new electron.MenuItem({
23144
+ label: "Duplicate Tab",
23145
+ click: () => {
23146
+ const newId = tabManager.duplicateTab(id);
23147
+ if (newId) layoutViews(windowState);
23148
+ }
23149
+ })
23150
+ );
23151
+ menu.append(
23152
+ new electron.MenuItem({
23153
+ label: "Close Tab",
23154
+ click: () => {
23155
+ tabManager.closeTab(id);
23156
+ layoutViews(windowState);
23157
+ }
23158
+ })
23159
+ );
23160
+ menu.popup({ window: mainWindow });
23161
+ });
22598
23162
  electron.ipcMain.handle(Channels.TAB_STATE_GET, () => ({
22599
23163
  tabs: tabManager.getAllStates(),
22600
23164
  activeId: tabManager.getActiveTabId() || ""
@@ -24102,157 +24666,6 @@ ${progress}
24102
24666
  });
24103
24667
  }
24104
24668
  }
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
24669
  const logger$2 = createLogger("Shortcuts");
24257
24670
  function registerHighlightShortcut(mainWindow, tabManager) {
24258
24671
  const register = () => {
@@ -24273,8 +24686,18 @@ function registerHighlightShortcut(mainWindow, tabManager) {
24273
24686
  mainWindow.removeListener("focus", register);
24274
24687
  };
24275
24688
  }
24276
- function setupAppMenu() {
24689
+ function setupAppMenu(handlers) {
24277
24690
  const appMenu = electron.Menu.buildFromTemplate([
24691
+ {
24692
+ label: "File",
24693
+ submenu: [
24694
+ {
24695
+ label: "Reopen Closed Tab",
24696
+ accelerator: "CommandOrControl+Shift+T",
24697
+ click: handlers.reopenClosedTab
24698
+ }
24699
+ ]
24700
+ },
24278
24701
  {
24279
24702
  label: "Edit",
24280
24703
  submenu: [
@@ -24286,6 +24709,26 @@ function setupAppMenu() {
24286
24709
  { role: "paste" },
24287
24710
  { role: "selectAll" }
24288
24711
  ]
24712
+ },
24713
+ {
24714
+ label: "View",
24715
+ submenu: [
24716
+ {
24717
+ label: "Zoom In",
24718
+ accelerator: "CommandOrControl+Plus",
24719
+ click: handlers.zoomIn
24720
+ },
24721
+ {
24722
+ label: "Zoom Out",
24723
+ accelerator: "CommandOrControl+-",
24724
+ click: handlers.zoomOut
24725
+ },
24726
+ {
24727
+ label: "Actual Size",
24728
+ accelerator: "CommandOrControl+0",
24729
+ click: handlers.zoomReset
24730
+ }
24731
+ ]
24289
24732
  }
24290
24733
  ]);
24291
24734
  electron.Menu.setApplicationMenu(appMenu);
@@ -24644,7 +25087,24 @@ async function bootstrap() {
24644
25087
  });
24645
25088
  registerIpcHandlers(windowState, runtime);
24646
25089
  registerHighlightShortcut(windowState.mainWindow, tabManager);
24647
- setupAppMenu();
25090
+ setupAppMenu({
25091
+ reopenClosedTab: () => {
25092
+ const id = tabManager.reopenClosedTab();
25093
+ if (id) layoutViews(windowState);
25094
+ },
25095
+ zoomIn: () => {
25096
+ const id = tabManager.getActiveTabId();
25097
+ if (id) tabManager.zoomIn(id);
25098
+ },
25099
+ zoomOut: () => {
25100
+ const id = tabManager.getActiveTabId();
25101
+ if (id) tabManager.zoomOut(id);
25102
+ },
25103
+ zoomReset: () => {
25104
+ const id = tabManager.getActiveTabId();
25105
+ if (id) tabManager.zoomReset(id);
25106
+ }
25107
+ });
24648
25108
  subscribe((state2) => {
24649
25109
  chromeView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
24650
25110
  sidebarView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);