@quanta-intellect/vessel-browser 0.1.68 → 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
@@ -65,6 +65,7 @@ const defaults = {
65
65
  domainPolicy: { allowedDomains: [], blockedDomains: [] },
66
66
  downloadPath: "",
67
67
  telemetryEnabled: true,
68
+ defaultSearchEngine: "duckduckgo",
68
69
  premium: {
69
70
  status: "free",
70
71
  customerId: "",
@@ -76,7 +77,7 @@ const defaults = {
76
77
  };
77
78
  const SAVE_DEBOUNCE_MS$6 = 150;
78
79
  const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
79
- const logger$j = createLogger("Settings");
80
+ const logger$k = createLogger("Settings");
80
81
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
81
82
  let settings = null;
82
83
  let settingsIssues = [];
@@ -222,7 +223,7 @@ function persistNow() {
222
223
  getSettingsPath(),
223
224
  JSON.stringify(buildPersistedSettings(settings), null, 2)
224
225
  )
225
- ).catch((err) => logger$j.error("Failed to save settings:", err));
226
+ ).catch((err) => logger$k.error("Failed to save settings:", err));
226
227
  }
227
228
  function saveSettings() {
228
229
  saveDirty = true;
@@ -326,7 +327,7 @@ function assertPermittedNavigationURL(url) {
326
327
  }
327
328
  const MAX_CUSTOM_HISTORY = 50;
328
329
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
329
- const logger$i = createLogger("Tab");
330
+ const logger$j = createLogger("Tab");
330
331
  class Tab {
331
332
  id;
332
333
  view;
@@ -367,7 +368,7 @@ class Tab {
367
368
  guardedLoadURL(url, options) {
368
369
  const blockReason = this.getNavigationBlockReason(url);
369
370
  if (blockReason) {
370
- logger$i.warn(blockReason);
371
+ logger$j.warn(blockReason);
371
372
  return blockReason;
372
373
  }
373
374
  void this.view.webContents.loadURL(url, options);
@@ -382,14 +383,16 @@ class Tab {
382
383
  this.onHighlightSelection = options?.onHighlightSelection;
383
384
  this.onHighlightRemove = options?.onHighlightRemove;
384
385
  this.onHighlightRecolor = options?.onHighlightRecolor;
385
- this.view = new electron.WebContentsView({
386
- webPreferences: {
387
- preload: path.join(__dirname, "../preload/content-script.js"),
388
- sandbox: true,
389
- contextIsolation: true,
390
- nodeIntegration: false
391
- }
392
- });
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 });
393
396
  const initialUrl = url || "about:blank";
394
397
  this._state = {
395
398
  id,
@@ -403,11 +406,26 @@ class Tab {
403
406
  adBlockingEnabled: options?.adBlockingEnabled ?? true,
404
407
  role: options?.role
405
408
  };
406
- this.view.webContents.on("before-input-event", (_event, input) => {
409
+ this.view.webContents.on("before-input-event", (event, input) => {
407
410
  if (!input.control && !input.meta) return;
408
411
  if (input.type !== "keyDown") return;
409
412
  const key = input.key.toLowerCase();
410
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
+ }
411
429
  if (key === "c") wc.copy();
412
430
  else if (key === "v") wc.paste();
413
431
  else if (key === "x") wc.cut();
@@ -428,7 +446,7 @@ class Tab {
428
446
  wc.setWindowOpenHandler(({ url, disposition }) => {
429
447
  const error = this.getNavigationBlockReason(url);
430
448
  if (error) {
431
- logger$i.warn(error);
449
+ logger$j.warn(error);
432
450
  return { action: "deny" };
433
451
  }
434
452
  this.onOpenUrl?.({
@@ -442,7 +460,7 @@ class Tab {
442
460
  const error = this.getNavigationBlockReason(url);
443
461
  if (!error) return;
444
462
  event.preventDefault();
445
- logger$i.warn(`${context}: ${error}`);
463
+ logger$j.warn(`${context}: ${error}`);
446
464
  };
447
465
  wc.on("will-navigate", (event, url) => {
448
466
  blockNavigation(event, url, "Blocked top-level navigation");
@@ -506,7 +524,7 @@ class Tab {
506
524
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 999px; }
507
525
  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
508
526
  ::-webkit-scrollbar-corner { background: transparent; }
509
- `).catch((err) => logger$i.warn("Failed to inject scrollbar CSS:", err));
527
+ `).catch((err) => logger$j.warn("Failed to inject scrollbar CSS:", err));
510
528
  });
511
529
  wc.on("page-favicon-updated", (_, favicons) => {
512
530
  this._state.favicon = favicons[0] || "";
@@ -529,7 +547,7 @@ class Tab {
529
547
  ).then((highlightedText) => {
530
548
  this.buildContextMenu(wc, params, highlightedText.trim());
531
549
  }).catch((err) => {
532
- 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);
533
551
  this.buildContextMenu(wc, params, "");
534
552
  });
535
553
  });
@@ -675,6 +693,19 @@ class Tab {
675
693
  reload() {
676
694
  this.view.webContents.reload();
677
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
+ }
678
709
  setAdBlockingEnabled(enabled) {
679
710
  if (this._state.adBlockingEnabled === enabled) return false;
680
711
  this._state.adBlockingEnabled = enabled;
@@ -760,7 +791,7 @@ class Tab {
760
791
  document.addEventListener('mouseup', window.__vesselHighlightHandler);
761
792
  }
762
793
  })()
763
- `).catch((err) => logger$i.warn("Failed to inject highlight listener:", err));
794
+ `).catch((err) => logger$j.warn("Failed to inject highlight listener:", err));
764
795
  } else {
765
796
  void wc.executeJavaScript(`
766
797
  (function() {
@@ -771,7 +802,7 @@ class Tab {
771
802
  delete window.__vesselHighlightHandler;
772
803
  }
773
804
  })()
774
- `).catch((err) => logger$i.warn("Failed to remove highlight listener:", err));
805
+ `).catch((err) => logger$j.warn("Failed to remove highlight listener:", err));
775
806
  }
776
807
  }
777
808
  get webContentsId() {
@@ -782,7 +813,7 @@ class Tab {
782
813
  this.view.webContents.close();
783
814
  }
784
815
  }
785
- const logger$h = createLogger("JsonPersistence");
816
+ const logger$i = createLogger("JsonPersistence");
786
817
  function canUseSafeStorage() {
787
818
  try {
788
819
  return electron.safeStorage.isEncryptionAvailable();
@@ -847,7 +878,7 @@ function createDebouncedJsonPersistence({
847
878
  data,
848
879
  typeof data === "string" ? { encoding: "utf-8", mode: 384 } : { mode: 384 }
849
880
  )
850
- ).catch((err) => logger$h.error(`Failed to save ${logLabel}:`, err));
881
+ ).catch((err) => logger$i.error(`Failed to save ${logLabel}:`, err));
851
882
  };
852
883
  const schedule = () => {
853
884
  saveDirty2 = true;
@@ -2335,13 +2366,7 @@ function destroySession(tabId) {
2335
2366
  sessions.delete(tabId);
2336
2367
  }
2337
2368
  }
2338
- function destroyAllSessions() {
2339
- for (const session of sessions.values()) {
2340
- session.destroy();
2341
- }
2342
- sessions.clear();
2343
- }
2344
- const logger$g = createLogger("TabManager");
2369
+ const logger$h = createLogger("TabManager");
2345
2370
  class TabManager {
2346
2371
  tabs = /* @__PURE__ */ new Map();
2347
2372
  order = [];
@@ -2350,9 +2375,15 @@ class TabManager {
2350
2375
  onStateChange;
2351
2376
  highlightCaptureCallback = null;
2352
2377
  pageLoadCallback = null;
2353
- constructor(window2, onStateChange) {
2378
+ closedTabs = [];
2379
+ MAX_CLOSED_TABS = 20;
2380
+ isPrivate;
2381
+ sessionPartition;
2382
+ constructor(window2, onStateChange, options) {
2354
2383
  this.window = window2;
2355
2384
  this.onStateChange = onStateChange;
2385
+ this.isPrivate = options?.isPrivate ?? false;
2386
+ this.sessionPartition = options?.sessionPartition ?? (this.isPrivate ? "private-mode" : void 0);
2356
2387
  }
2357
2388
  onPageLoad(cb) {
2358
2389
  this.pageLoadCallback = cb;
@@ -2363,12 +2394,15 @@ class TabManager {
2363
2394
  const tab = new Tab(id, url, () => this.broadcastState(), {
2364
2395
  adBlockingEnabled: options?.adBlockingEnabled,
2365
2396
  parentWindow: this.window,
2397
+ sessionPartition: this.sessionPartition,
2366
2398
  onOpenUrl: ({ url: requestedUrl, background: background2, adBlockingEnabled }) => {
2367
2399
  this.createTab(requestedUrl, { background: background2, adBlockingEnabled });
2368
2400
  },
2369
2401
  onPageLoad: (pageUrl, wc) => {
2370
2402
  this.reapplyHighlights(pageUrl, wc);
2371
- addEntry$1(pageUrl, wc.getTitle());
2403
+ if (!this.isPrivate) {
2404
+ addEntry$1(pageUrl, wc.getTitle());
2405
+ }
2372
2406
  this.pageLoadCallback?.(pageUrl, wc);
2373
2407
  },
2374
2408
  onHighlightSelection: (wc) => this.captureHighlightFromPage(wc),
@@ -2400,6 +2434,14 @@ class TabManager {
2400
2434
  closeTab(id) {
2401
2435
  const tab = this.tabs.get(id);
2402
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
+ }
2403
2445
  const wcId = tab.webContentsId;
2404
2446
  if (wcId !== void 0) {
2405
2447
  this.lastReapply.delete(wcId);
@@ -2433,6 +2475,25 @@ class TabManager {
2433
2475
  reloadTab(id) {
2434
2476
  this.tabs.get(id)?.reload();
2435
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
+ }
2436
2497
  getActiveTab() {
2437
2498
  return this.activeTabId ? this.tabs.get(this.activeTabId) : void 0;
2438
2499
  }
@@ -2506,10 +2567,10 @@ class TabManager {
2506
2567
  return ids;
2507
2568
  }
2508
2569
  destroyAllTabs() {
2509
- destroyAllSessions();
2510
- for (const id of this.order) {
2570
+ for (const id of [...this.order]) {
2511
2571
  const tab = this.tabs.get(id);
2512
2572
  if (!tab) continue;
2573
+ destroySession(id);
2513
2574
  this.window.contentView.removeChildView(tab.view);
2514
2575
  tab.destroy();
2515
2576
  }
@@ -2535,7 +2596,7 @@ class TabManager {
2535
2596
  }));
2536
2597
  if (entries.length > 0) {
2537
2598
  void highlightBatchOnPage(wc, entries).catch(
2538
- (err) => logger$g.warn("Failed to batch highlight:", err)
2599
+ (err) => logger$h.warn("Failed to batch highlight:", err)
2539
2600
  );
2540
2601
  }
2541
2602
  }
@@ -2557,12 +2618,12 @@ class TabManager {
2557
2618
  const result = await captureSelectionHighlight(wc);
2558
2619
  if (result.success && result.text) {
2559
2620
  await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(
2560
- (err) => logger$g.warn("Failed to capture highlight:", err)
2621
+ (err) => logger$h.warn("Failed to capture highlight:", err)
2561
2622
  );
2562
2623
  }
2563
2624
  this.highlightCaptureCallback?.(result);
2564
2625
  } catch (err) {
2565
- logger$g.warn("Failed to capture highlight from page:", err);
2626
+ logger$h.warn("Failed to capture highlight from page:", err);
2566
2627
  this.highlightCaptureCallback?.({
2567
2628
  success: false,
2568
2629
  message: "Could not capture selection"
@@ -2587,7 +2648,7 @@ class TabManager {
2587
2648
  void this.removeHighlightMarksForText(wc, text);
2588
2649
  }
2589
2650
  } catch (err) {
2590
- logger$g.warn("Failed to remove highlight from matching tab:", err);
2651
+ logger$h.warn("Failed to remove highlight from matching tab:", err);
2591
2652
  }
2592
2653
  }
2593
2654
  this.highlightCaptureCallback?.({
@@ -2618,12 +2679,12 @@ class TabManager {
2618
2679
  void 0,
2619
2680
  color
2620
2681
  ).catch(
2621
- (err) => logger$g.warn("Failed to update highlight color:", err)
2682
+ (err) => logger$h.warn("Failed to update highlight color:", err)
2622
2683
  );
2623
2684
  });
2624
2685
  }
2625
2686
  } catch (err) {
2626
- logger$g.warn("Failed to iterate highlights for color change:", err);
2687
+ logger$h.warn("Failed to iterate highlights for color change:", err);
2627
2688
  }
2628
2689
  }
2629
2690
  this.highlightCaptureCallback?.({
@@ -2645,7 +2706,7 @@ class TabManager {
2645
2706
  });
2646
2707
  })()`
2647
2708
  ).catch(
2648
- (err) => logger$g.warn("Failed to remove highlight marks:", err)
2709
+ (err) => logger$h.warn("Failed to remove highlight marks:", err)
2649
2710
  );
2650
2711
  }
2651
2712
  broadcastState() {
@@ -2732,6 +2793,17 @@ const Channels = {
2732
2793
  DEVTOOLS_PANEL_RESIZE: "devtools-panel:resize",
2733
2794
  // Ad blocking
2734
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",
2735
2807
  // Find in page
2736
2808
  FIND_IN_PAGE_START: "find:start",
2737
2809
  FIND_IN_PAGE_NEXT: "find:next",
@@ -3749,11 +3821,29 @@ function errorResult(error, value) {
3749
3821
  function getErrorMessage(error, fallback = "Unknown error") {
3750
3822
  return error instanceof Error && error.message ? error.message : fallback;
3751
3823
  }
3752
- const logger$f = createLogger("Premium");
3824
+ const logger$g = createLogger("Premium");
3753
3825
  const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
3754
3826
  const FREE_TOOL_ITERATION_LIMIT = 50;
3755
3827
  const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3756
3828
  const OFFLINE_GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
3829
+ const MAX_API_ERROR_LOG_LENGTH = 300;
3830
+ async function readApiErrorDetail(res) {
3831
+ try {
3832
+ const text = (await res.text()).trim();
3833
+ if (!text) return "";
3834
+ try {
3835
+ const data = JSON.parse(text);
3836
+ const detail = data.error || data.message;
3837
+ if (typeof detail === "string" && detail.trim()) {
3838
+ return detail.trim().slice(0, MAX_API_ERROR_LOG_LENGTH);
3839
+ }
3840
+ } catch {
3841
+ }
3842
+ return text.slice(0, MAX_API_ERROR_LOG_LENGTH);
3843
+ } catch {
3844
+ return "";
3845
+ }
3846
+ }
3757
3847
  const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
3758
3848
  "screenshot",
3759
3849
  "save_session",
@@ -3843,7 +3933,12 @@ async function verifySubscription(identifier) {
3843
3933
  body: JSON.stringify({ identifier: verificationIdentifier })
3844
3934
  });
3845
3935
  if (!res.ok) {
3846
- logger$f.warn("Verification API returned a non-OK status:", res.status);
3936
+ const detail = await readApiErrorDetail(res);
3937
+ logger$g.warn(
3938
+ "Verification API returned a non-OK status:",
3939
+ res.status,
3940
+ detail
3941
+ );
3847
3942
  return current;
3848
3943
  }
3849
3944
  const data = await res.json();
@@ -3858,7 +3953,7 @@ async function verifySubscription(identifier) {
3858
3953
  setSetting("premium", updated);
3859
3954
  return updated;
3860
3955
  } catch (err) {
3861
- logger$f.warn("Verification failed:", err);
3956
+ logger$g.warn("Verification failed:", err);
3862
3957
  return current;
3863
3958
  }
3864
3959
  }
@@ -4431,7 +4526,7 @@ const EXTRACT_TIMEOUT_MAX_MS = 2e4;
4431
4526
  const MUTATION_CAPTURE_INTERVAL_MS = 5e3;
4432
4527
  const MUTATION_SETTLE_AFTER_MS = 1500;
4433
4528
  const AGENT_STREAM_IDLE_TIMEOUT_MS = 3e4;
4434
- const logger$e = createLogger("Extractor");
4529
+ const logger$f = createLogger("Extractor");
4435
4530
  const EMPTY_PAGE_CONTENT = {
4436
4531
  title: "",
4437
4532
  content: "",
@@ -5181,7 +5276,7 @@ async function executeScript(webContents, script) {
5181
5276
  })
5182
5277
  ]);
5183
5278
  } catch (err) {
5184
- logger$e.warn("Failed to execute page script:", err);
5279
+ logger$f.warn("Failed to execute page script:", err);
5185
5280
  return null;
5186
5281
  } finally {
5187
5282
  if (timer) {
@@ -5288,7 +5383,7 @@ async function estimateExtractionTimeout(webContents) {
5288
5383
  return EXTRACT_TIMEOUT_BASE_MS + extra;
5289
5384
  }
5290
5385
  } catch (err) {
5291
- 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);
5292
5387
  }
5293
5388
  return EXTRACT_TIMEOUT_BASE_MS;
5294
5389
  }
@@ -5599,7 +5694,7 @@ function enableClipboardShortcuts(view) {
5599
5694
  }
5600
5695
  });
5601
5696
  }
5602
- const CHROME_HEIGHT = 110;
5697
+ const CHROME_HEIGHT$1 = 110;
5603
5698
  const DEFAULT_DEVTOOLS_PANEL_HEIGHT = 250;
5604
5699
  const MIN_DEVTOOLS_PANEL = 120;
5605
5700
  const MAX_DEVTOOLS_PANEL = 600;
@@ -5814,7 +5909,7 @@ function layoutViews(state2) {
5814
5909
  uiState
5815
5910
  } = state2;
5816
5911
  const [width, height] = mainWindow.getContentSize();
5817
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
5912
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$1;
5818
5913
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5819
5914
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5820
5915
  const chromeNeedsFullHeight = uiState.settingsOpen;
@@ -5864,7 +5959,7 @@ function layoutViews(state2) {
5864
5959
  function resizeSidebarViews(state2) {
5865
5960
  const { mainWindow, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
5866
5961
  const [width, height] = mainWindow.getContentSize();
5867
- const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
5962
+ const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT$1;
5868
5963
  const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
5869
5964
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
5870
5965
  const resizeHandleOverlap = 6;
@@ -6326,7 +6421,7 @@ class AnthropicProvider {
6326
6421
  });
6327
6422
  continue;
6328
6423
  }
6329
- const argSummary = [tb.input.url, tb.input.text, tb.input.direction].map((v) => typeof v === "string" ? v : "").find((v) => v.length > 0) ?? "";
6424
+ const argSummary = [tb.input.url, tb.input.query, tb.input.text, tb.input.direction].map((v) => typeof v === "string" ? v : "").find((v) => v.length > 0) ?? "";
6330
6425
  onChunk(`
6331
6426
  <<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
6332
6427
  `);
@@ -6517,11 +6612,52 @@ const SAFE_TOOL_ALIASES = {
6517
6612
  scroll_up: "scroll",
6518
6613
  read: "read_page",
6519
6614
  read_current_page: "read_page",
6520
- scan_page: "read_page"
6615
+ scan_page: "read_page",
6616
+ save_bookmark: "save_bookmark",
6617
+ bookmark: "save_bookmark",
6618
+ bookmark_page: "save_bookmark",
6619
+ bookmark_url: "save_bookmark",
6620
+ add_bookmark: "save_bookmark",
6621
+ create_bookmark: "save_bookmark"
6521
6622
  };
6623
+ const CANONICAL_TOOL_NAMES = /* @__PURE__ */ new Set([
6624
+ "archive_bookmark",
6625
+ "click",
6626
+ "create_bookmark_folder",
6627
+ "current_tab",
6628
+ "go_back",
6629
+ "go_forward",
6630
+ "inspect_element",
6631
+ "list_bookmarks",
6632
+ "navigate",
6633
+ "open_bookmark",
6634
+ "organize_bookmark",
6635
+ "read_page",
6636
+ "save_bookmark",
6637
+ "scroll",
6638
+ "search",
6639
+ "type_text"
6640
+ ]);
6641
+ function repeatedTokenMatch(value, token) {
6642
+ if (value === token) return true;
6643
+ if (token.length === 0 || value.length <= token.length) return false;
6644
+ if (value.length % token.length !== 0) return false;
6645
+ return token.repeat(value.length / token.length) === value;
6646
+ }
6522
6647
  function normalizeToolAlias(name) {
6523
6648
  const normalized = name.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
6524
- return SAFE_TOOL_ALIASES[normalized] ?? name;
6649
+ const direct = SAFE_TOOL_ALIASES[normalized] ?? normalized;
6650
+ if (CANONICAL_TOOL_NAMES.has(direct)) return direct;
6651
+ const knownTokens = [
6652
+ ...Object.keys(SAFE_TOOL_ALIASES),
6653
+ ...CANONICAL_TOOL_NAMES
6654
+ ];
6655
+ for (const token of knownTokens) {
6656
+ if (repeatedTokenMatch(normalized, token)) {
6657
+ return SAFE_TOOL_ALIASES[token] ?? token;
6658
+ }
6659
+ }
6660
+ return name;
6525
6661
  }
6526
6662
  function parseModelSizeInBillions(model) {
6527
6663
  const match = model.toLowerCase().match(/(?:^|[:/_\-\s])(\d+(?:\.\d+)?)b(?:$|[:/_\-\s])/i);
@@ -6553,7 +6689,7 @@ const MAX_MCP_NAV_CONTENT_LENGTH = 3e4;
6553
6689
  const MAX_AGENT_DEBUG_CONTENT_LENGTH = 2e4;
6554
6690
  const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
6555
6691
  const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
6556
- const logger$d = createLogger("OpenAIProvider");
6692
+ const logger$e = createLogger("OpenAIProvider");
6557
6693
  function shouldDebugAgentLoop() {
6558
6694
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
6559
6695
  return value === "1" || value === "true";
@@ -6845,8 +6981,62 @@ function scalarArgsForTool(name, scalar) {
6845
6981
  const mode = trimmed.replace(/^["']|["']$/g, "").toLowerCase();
6846
6982
  if (mode) return { mode };
6847
6983
  }
6984
+ if (name === "save_bookmark") {
6985
+ const url = toLikelyUrl(trimmed);
6986
+ if (url) return { url };
6987
+ const lastSpace = trimmed.lastIndexOf(" ");
6988
+ if (lastSpace > 0) {
6989
+ const maybeUrl = toLikelyUrl(trimmed.slice(lastSpace + 1));
6990
+ if (maybeUrl) return { url: maybeUrl, title: trimmed.slice(0, lastSpace).replace(/^["']|["']$/g, "") };
6991
+ }
6992
+ }
6993
+ return null;
6994
+ }
6995
+ function firstStringArg(args, keys) {
6996
+ for (const key of keys) {
6997
+ const value = args[key];
6998
+ if (typeof value === "string" && value.trim()) {
6999
+ return value.trim();
7000
+ }
7001
+ }
6848
7002
  return null;
6849
7003
  }
7004
+ function normalizeElementTargetArgs(args) {
7005
+ const normalized = { ...args };
7006
+ if (typeof normalized.index === "string" && /^\d+$/.test(normalized.index.trim())) {
7007
+ normalized.index = Number(normalized.index.trim());
7008
+ }
7009
+ if (typeof normalized.selector !== "string" || !normalized.selector.trim()) {
7010
+ const selector = firstStringArg(normalized, [
7011
+ "cssSelector",
7012
+ "css_selector",
7013
+ "querySelector",
7014
+ "query_selector"
7015
+ ]);
7016
+ if (selector) normalized.selector = selector;
7017
+ }
7018
+ if (typeof normalized.text !== "string" || !normalized.text.trim()) {
7019
+ const text = firstStringArg(normalized, [
7020
+ "label",
7021
+ "title",
7022
+ "name",
7023
+ "target",
7024
+ "element",
7025
+ "linkText",
7026
+ "link_text",
7027
+ "ariaLabel",
7028
+ "aria_label"
7029
+ ]);
7030
+ if (text) normalized.text = text;
7031
+ }
7032
+ return normalized;
7033
+ }
7034
+ function hasElementTarget(args) {
7035
+ return typeof args.index === "number" || typeof args.selector === "string" && args.selector.trim().length > 0 || typeof args.text === "string" && args.text.trim().length > 0;
7036
+ }
7037
+ function isTargetlessClickArgs(args) {
7038
+ return !hasElementTarget(normalizeElementTargetArgs(args));
7039
+ }
6850
7040
  function tryParseJsonWithCommonRepairs(raw) {
6851
7041
  const trimmed = raw.trim();
6852
7042
  if (!trimmed) return {};
@@ -6900,7 +7090,10 @@ function parseToolArgsWithRepair(name, argsJson) {
6900
7090
  return scalarArgs ? { args: scalarArgs, repaired: true } : null;
6901
7091
  }
6902
7092
  function coerceToolArgsForExecution(name, args) {
6903
- const coerced = { ...args };
7093
+ let coerced = { ...args };
7094
+ if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
7095
+ coerced = normalizeElementTargetArgs(coerced);
7096
+ }
6904
7097
  if (name === "search") {
6905
7098
  if (typeof coerced.query !== "string" || !coerced.query.trim()) {
6906
7099
  if (typeof coerced.text === "string" && coerced.text.trim()) {
@@ -6921,6 +7114,25 @@ function coerceToolArgsForExecution(name, args) {
6921
7114
  }
6922
7115
  }
6923
7116
  }
7117
+ if (name === "save_bookmark") {
7118
+ if (typeof coerced.url !== "string" || !coerced.url.trim()) {
7119
+ if (typeof coerced.link === "string" && coerced.link.trim()) {
7120
+ coerced.url = coerced.link.trim();
7121
+ } else if (typeof coerced.href === "string" && coerced.href.trim()) {
7122
+ coerced.url = coerced.href.trim();
7123
+ }
7124
+ }
7125
+ if (typeof coerced.folderName !== "string" || !coerced.folderName.trim()) {
7126
+ if (typeof coerced.folder === "string" && coerced.folder.trim()) {
7127
+ coerced.folderName = coerced.folder.trim();
7128
+ } else if (typeof coerced.category === "string" && coerced.category.trim()) {
7129
+ coerced.folderName = coerced.category.trim();
7130
+ }
7131
+ }
7132
+ if (coerced.folderName && typeof coerced.createFolderIfMissing === "undefined") {
7133
+ coerced.createFolderIfMissing = true;
7134
+ }
7135
+ }
6924
7136
  return coerced;
6925
7137
  }
6926
7138
  function canonicalizeArgsForTool(name, args) {
@@ -6937,6 +7149,24 @@ function canonicalizeArgsForTool(name, args) {
6937
7149
  }
6938
7150
  return canonical;
6939
7151
  }
7152
+ function unsupportedToolHint(name) {
7153
+ const normalized = name.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
7154
+ const BOOKMARK_NAMES = [
7155
+ "organize_bookmark",
7156
+ "organize_bookmarks",
7157
+ "manage_bookmark",
7158
+ "manage_bookmarks",
7159
+ "add_to_bookmarks",
7160
+ "save_to_bookmarks",
7161
+ "bookmark_link",
7162
+ "save_link",
7163
+ "store_bookmark"
7164
+ ];
7165
+ if (BOOKMARK_NAMES.includes(normalized) || /bookmark|save.*link|organize/.test(normalized)) {
7166
+ return `Error: "${name}" is not a supported tool. Use save_bookmark to save a page as a bookmark, or create_bookmark_folder to create a folder. Example: save_bookmark with {"url": "...", "title": "...", "folderName": "..."}`;
7167
+ }
7168
+ return `Error: ${name} is not a supported tool. Choose one of the available browser tools instead.`;
7169
+ }
6940
7170
  function resolveToolCallName(rawName, args, availableToolNames) {
6941
7171
  const aliased = normalizeToolAlias(rawName);
6942
7172
  if (availableToolNames.has(aliased)) return aliased;
@@ -6960,9 +7190,9 @@ function resolveToolCallName(rawName, args, availableToolNames) {
6960
7190
  function logAgentLoopDebug(payload) {
6961
7191
  if (!shouldDebugAgentLoop()) return;
6962
7192
  try {
6963
- logger$d.info(`[agent-debug] ${JSON.stringify(payload)}`);
7193
+ logger$e.info(`[agent-debug] ${JSON.stringify(payload)}`);
6964
7194
  } catch (err) {
6965
- logger$d.warn("Failed to serialize debug payload:", err);
7195
+ logger$e.warn("Failed to serialize debug payload:", err);
6966
7196
  }
6967
7197
  }
6968
7198
  function recoverTextEncodedToolCalls(text, availableToolNames) {
@@ -7241,6 +7471,7 @@ class OpenAICompatProvider {
7241
7471
  );
7242
7472
  if (recoveredToolCalls.length > 0) {
7243
7473
  toolCalls = recoveredToolCalls;
7474
+ if (textAccum.trim()) onChunk("<<erase_prev>>");
7244
7475
  } else {
7245
7476
  const narratedToolCalls = recoverNarratedActionToolCalls(
7246
7477
  textAccum,
@@ -7248,6 +7479,7 @@ class OpenAICompatProvider {
7248
7479
  );
7249
7480
  if (narratedToolCalls.length > 0) {
7250
7481
  toolCalls = narratedToolCalls;
7482
+ if (textAccum.trim()) onChunk("<<erase_prev>>");
7251
7483
  }
7252
7484
  }
7253
7485
  }
@@ -7314,6 +7546,25 @@ class OpenAICompatProvider {
7314
7546
  compactRecoveryCount = 0;
7315
7547
  const iterationToolResultPreviews = [];
7316
7548
  for (const tc of toolCalls) {
7549
+ if (!availableToolNames.has(tc.name)) {
7550
+ const hint = unsupportedToolHint(tc.name);
7551
+ onChunk(`
7552
+ <<tool:${tc.name}:⚠ unsupported>>
7553
+ `);
7554
+ messages.push({
7555
+ role: "tool",
7556
+ tool_call_id: tc.id,
7557
+ content: hint
7558
+ });
7559
+ compactCorrectionCount += 1;
7560
+ if (compactCorrectionCount >= 2) {
7561
+ messages.push({
7562
+ role: "user",
7563
+ content: `[System] You are calling unsupported tools. Stop inventing tool names. Use the supported tools you were given and take the next concrete step.`
7564
+ });
7565
+ }
7566
+ continue;
7567
+ }
7317
7568
  if (malformedToolCalls.has(tc.id)) {
7318
7569
  onChunk(`
7319
7570
  <<tool:${tc.name}:⚠ invalid args>>
@@ -7340,25 +7591,23 @@ class OpenAICompatProvider {
7340
7591
  }
7341
7592
  args = repairedArgs.args;
7342
7593
  args = coerceToolArgsForExecution(tc.name, args);
7343
- if (!availableToolNames.has(tc.name)) {
7594
+ const toolSignature = stableToolSignature(tc.name, args);
7595
+ if (this.agentToolProfile === "compact" && tc.name === "click" && isTargetlessClickArgs(args)) {
7344
7596
  onChunk(`
7345
- <<tool:unsupported_tool:⚠ unsupported>>
7597
+ <<tool:${tc.name}:⚠ missing target>>
7346
7598
  `);
7347
7599
  messages.push({
7348
7600
  role: "tool",
7349
7601
  tool_call_id: tc.id,
7350
- content: `Error: ${tc.name} is not a supported tool. Choose one of the available browser tools instead.`
7602
+ content: `Error: click requires an element target. Use click with {"index": N} from the latest read_page result, or {"text": "exact visible link/button text"}. If you do not have a current result index, call read_page(mode="results_only") first and then click one listed result.`
7603
+ });
7604
+ messages.push({
7605
+ role: "user",
7606
+ content: `[System] Your last click had no target. Do not call click with empty arguments. Refresh the page state with read_page(mode="results_only") if needed, then click exactly one result by index or exact visible text.`
7351
7607
  });
7352
7608
  compactCorrectionCount += 1;
7353
- if (compactCorrectionCount >= 2) {
7354
- messages.push({
7355
- role: "user",
7356
- content: `[System] You are calling unsupported tools. Stop inventing tool names. Use the supported tools you were given and take the next concrete step.`
7357
- });
7358
- }
7359
7609
  continue;
7360
7610
  }
7361
- const toolSignature = stableToolSignature(tc.name, args);
7362
7611
  const neverSuppressDuplicate = [
7363
7612
  "read_page",
7364
7613
  "current_tab",
@@ -7577,7 +7826,7 @@ function createProvider(config) {
7577
7826
  return new OpenAICompatProvider(normalized);
7578
7827
  }
7579
7828
  const require$1 = node_module.createRequire(require("url").pathToFileURL(__filename).href);
7580
- const logger$c = createLogger("DevTrace");
7829
+ const logger$d = createLogger("DevTrace");
7581
7830
  let cachedFactory;
7582
7831
  function createNoopTraceSession() {
7583
7832
  return {
@@ -7610,7 +7859,7 @@ function loadLocalFactory() {
7610
7859
  return cachedFactory;
7611
7860
  }
7612
7861
  } catch (err) {
7613
- logger$c.warn("Failed to load local trace logger:", err);
7862
+ logger$d.warn("Failed to load local trace logger:", err);
7614
7863
  }
7615
7864
  }
7616
7865
  return cachedFactory;
@@ -10579,6 +10828,14 @@ function pruneToolsForContext(tools, pageType, query = "", options = {}) {
10579
10828
  return description !== tool.description ? { ...tool, description } : tool;
10580
10829
  });
10581
10830
  }
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
+ };
10582
10839
  function trimText(value) {
10583
10840
  return typeof value === "string" ? value.trim() : "";
10584
10841
  }
@@ -11417,7 +11674,7 @@ function formatDeadLinkMessage(label, result) {
11417
11674
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
11418
11675
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
11419
11676
  }
11420
- const logger$b = createLogger("Screenshot");
11677
+ const logger$c = createLogger("Screenshot");
11421
11678
  const SCREENSHOT_RETRY_COUNT = 3;
11422
11679
  const SCREENSHOT_RETRY_BASE_DELAY_MS = 120;
11423
11680
  async function captureScreenshot(wc) {
@@ -11439,7 +11696,7 @@ async function captureScreenshot(wc) {
11439
11696
  }
11440
11697
  }
11441
11698
  } catch (err) {
11442
- logger$b.debug(
11699
+ logger$c.debug(
11443
11700
  `capturePage attempt ${attempt + 1} failed; retrying if attempts remain.`,
11444
11701
  getErrorMessage(err)
11445
11702
  );
@@ -12313,7 +12570,7 @@ function buildHuggingFaceSearchShortcut(currentUrl, rawQuery) {
12313
12570
  appliedFilters
12314
12571
  };
12315
12572
  }
12316
- const logger$a = createLogger("PageActions");
12573
+ const logger$b = createLogger("PageActions");
12317
12574
  function getBookmarkMetadataFromArgs(args) {
12318
12575
  return normalizeBookmarkMetadata({
12319
12576
  intent: args.intent ?? args.intent,
@@ -12499,7 +12756,7 @@ async function executePageScript(wc, script, options) {
12499
12756
  return result;
12500
12757
  } catch (err) {
12501
12758
  const label = options?.label ? ` (${options.label})` : "";
12502
- logger$a.warn(`Failed to execute page script${label}:`, err);
12759
+ logger$b.warn(`Failed to execute page script${label}:`, err);
12503
12760
  return null;
12504
12761
  } finally {
12505
12762
  if (timer) {
@@ -12600,7 +12857,7 @@ Search results snapshot:
12600
12857
  ${truncated}`;
12601
12858
  }
12602
12859
  } catch (err) {
12603
- 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);
12604
12861
  }
12605
12862
  const fallback = await getPostNavSummary(wc);
12606
12863
  return fallback ? `${fallback}
@@ -12623,7 +12880,7 @@ Page snapshot after navigation:
12623
12880
  ${truncated}`;
12624
12881
  }
12625
12882
  } catch (err) {
12626
- logger$a.warn("Failed to build post-click navigation summary:", err);
12883
+ logger$b.warn("Failed to build post-click navigation summary:", err);
12627
12884
  }
12628
12885
  return "";
12629
12886
  }
@@ -13117,7 +13374,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
13117
13374
  }
13118
13375
  }
13119
13376
  } catch (err) {
13120
- 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);
13121
13378
  }
13122
13379
  if (snapshot.url && snapshot.url !== wc.getURL()) {
13123
13380
  try {
@@ -13126,7 +13383,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
13126
13383
  await waitForLoad(wc, 3e3);
13127
13384
  return;
13128
13385
  } catch (err) {
13129
- 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);
13130
13387
  }
13131
13388
  }
13132
13389
  if (snapshot.url) {
@@ -13134,7 +13391,7 @@ async function restoreLocaleSnapshot(wc, snapshot) {
13134
13391
  await wc.reload();
13135
13392
  await waitForLoad(wc, 3e3);
13136
13393
  } catch (err) {
13137
- logger$a.warn("Failed to restore locale via page reload:", err);
13394
+ logger$b.warn("Failed to restore locale via page reload:", err);
13138
13395
  }
13139
13396
  }
13140
13397
  }
@@ -13486,7 +13743,7 @@ ${postActivationOverlayHint}`;
13486
13743
  return `${clickText} -> ${hrefFallbackUrl} (recovered via href fallback)`;
13487
13744
  }
13488
13745
  } catch (err) {
13489
- 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);
13490
13747
  }
13491
13748
  }
13492
13749
  }
@@ -13531,7 +13788,7 @@ async function tryAutoDismissCartDialog(wc) {
13531
13788
  return result;
13532
13789
  }
13533
13790
  } catch (err) {
13534
- 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);
13535
13792
  }
13536
13793
  return null;
13537
13794
  }
@@ -14966,8 +15223,19 @@ function buildCommonSearchUrlShortcut(currentUrl, rawQuery) {
14966
15223
  appliedFilters: existingParam ? [`updated ${existingParam} query`] : []
14967
15224
  };
14968
15225
  }
14969
- function buildSearchShortcut(currentUrl, rawQuery) {
14970
- return buildHuggingFaceSearchShortcut(currentUrl, rawQuery) ?? buildCommonSearchUrlShortcut(currentUrl, rawQuery);
15226
+ function buildDefaultEngineShortcut(rawQuery) {
15227
+ const settings2 = loadSettings();
15228
+ const engineId = settings2.defaultSearchEngine ?? "duckduckgo";
15229
+ if (engineId === "none") return null;
15230
+ const preset = SEARCH_ENGINE_PRESETS[engineId];
15231
+ if (!preset) return null;
15232
+ const query = normalizeSearchQuery(rawQuery);
15233
+ if (!query) return null;
15234
+ return {
15235
+ url: preset.url + encodeURIComponent(query),
15236
+ source: "default search engine",
15237
+ appliedFilters: []
15238
+ };
14971
15239
  }
14972
15240
  async function locateSearchTarget(wc, explicitSelector) {
14973
15241
  if (explicitSelector) {
@@ -15020,7 +15288,7 @@ async function locateSearchTarget(wc, explicitSelector) {
15020
15288
  const seen = new Set();
15021
15289
  const ordered = [];
15022
15290
  const specific = document.querySelectorAll(
15023
- 'input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i]'
15291
+ 'input[type="search"], input[name="q"], input[name="query"], input[name="search"], input[role="searchbox"], input[aria-label*="search" i], input[placeholder*="search" i], textarea[name="q"], textarea[name="query"], textarea[name="search"], textarea[role="searchbox"], textarea[aria-label*="search" i], textarea[placeholder*="search" i]'
15024
15292
  );
15025
15293
  specific.forEach((el) => {
15026
15294
  if (!seen.has(el)) {
@@ -15028,7 +15296,7 @@ async function locateSearchTarget(wc, explicitSelector) {
15028
15296
  ordered.push(el);
15029
15297
  }
15030
15298
  });
15031
- document.querySelectorAll('input[type="text"], input:not([type])').forEach((el) => {
15299
+ document.querySelectorAll('input[type="text"], input:not([type]), textarea').forEach((el) => {
15032
15300
  if (seen.has(el)) return;
15033
15301
  const scope = nearestSearchScope(el);
15034
15302
  if (!scope) return;
@@ -15039,9 +15307,10 @@ async function locateSearchTarget(wc, explicitSelector) {
15039
15307
  }
15040
15308
 
15041
15309
  function scoreInput(el) {
15042
- if (!(el instanceof HTMLInputElement)) return -1;
15310
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return -1;
15043
15311
  if (isDisabled(el) || !isVisible(el)) return -1;
15044
- const type = normalize(el.getAttribute("type") || el.type);
15312
+ const isTextarea = el instanceof HTMLTextAreaElement;
15313
+ const type = isTextarea ? "text" : normalize(el.getAttribute("type") || el.type);
15045
15314
  if (type && !["search", "text", ""].includes(type)) return -1;
15046
15315
 
15047
15316
  let score = 0;
@@ -15188,16 +15457,19 @@ async function searchPage(wc, args) {
15188
15457
  if (looksLikeCurrentSiteNameQuery(query, wc.getURL(), wc.getTitle() || "")) {
15189
15458
  return `Error: "${query}" looks like the current site's name, not a product query. You are already on ${wc.getURL()}. Open a section like staff picks/new releases or search for actual book titles, authors, or genres instead.`;
15190
15459
  }
15460
+ const runShortcut = async (shortcut) => {
15461
+ const beforeUrl2 = wc.getURL();
15462
+ await loadPermittedUrl(wc, shortcut.url);
15463
+ await waitForPotentialNavigation(wc, beforeUrl2, 4e3);
15464
+ const afterUrl2 = wc.getURL();
15465
+ const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
15466
+ const destination = shortcut.section ? ` ${shortcut.section}` : "";
15467
+ return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}${await getPostSearchSummary(wc)}`;
15468
+ };
15191
15469
  if (typeof args.selector !== "string") {
15192
- const shortcut = buildSearchShortcut(wc.getURL(), query);
15470
+ const shortcut = buildHuggingFaceSearchShortcut(wc.getURL(), query) ?? buildCommonSearchUrlShortcut(wc.getURL(), query);
15193
15471
  if (shortcut) {
15194
- const beforeUrl2 = wc.getURL();
15195
- await loadPermittedUrl(wc, shortcut.url);
15196
- await waitForPotentialNavigation(wc, beforeUrl2, 4e3);
15197
- const afterUrl2 = wc.getURL();
15198
- const applied = shortcut.appliedFilters.length > 0 ? ` (${shortcut.appliedFilters.join(", ")})` : "";
15199
- const destination = shortcut.section ? ` ${shortcut.section}` : "";
15200
- return `Searched "${query}" via ${shortcut.source}${destination} shortcut${applied} → ${afterUrl2}${await getPostSearchSummary(wc)}`;
15472
+ return runShortcut(shortcut);
15201
15473
  }
15202
15474
  }
15203
15475
  const searchInfo = await locateSearchTarget(
@@ -15208,6 +15480,12 @@ async function searchPage(wc, args) {
15208
15480
  return pageBusyError("search");
15209
15481
  }
15210
15482
  if (!searchInfo?.selector) {
15483
+ if (typeof args.selector !== "string") {
15484
+ const fallback = buildDefaultEngineShortcut(query);
15485
+ if (fallback) {
15486
+ return runShortcut(fallback);
15487
+ }
15488
+ }
15211
15489
  return 'Error: Could not find a visible search input. Try read_page(mode="visible_only") or provide a selector.';
15212
15490
  }
15213
15491
  const fillResult = await setElementValue(wc, searchInfo.selector, query);
@@ -15780,7 +16058,7 @@ async function executeAction(name, args, ctx) {
15780
16058
  )
15781
16059
  ]);
15782
16060
  } catch (err) {
15783
- 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);
15784
16062
  content = null;
15785
16063
  }
15786
16064
  if (!content || content.content.length === 0) {
@@ -15797,12 +16075,12 @@ async function executeAction(name, args, ctx) {
15797
16075
  new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
15798
16076
  ]);
15799
16077
  } catch (err) {
15800
- 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);
15801
16079
  content = null;
15802
16080
  }
15803
16081
  }
15804
16082
  } catch (err) {
15805
- 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);
15806
16084
  }
15807
16085
  }
15808
16086
  if (content && content.content.length > 0) {
@@ -16215,7 +16493,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
16215
16493
  try {
16216
16494
  page = await extractContent(wc);
16217
16495
  } catch (err) {
16218
- logger$a.warn("Failed to extract content for suggest:", err);
16496
+ logger$b.warn("Failed to extract content for suggest:", err);
16219
16497
  return "Could not read page. Try navigate to a working URL.";
16220
16498
  }
16221
16499
  const suggestions = [];
@@ -17546,7 +17824,7 @@ const ALGORITHM = "aes-256-gcm";
17546
17824
  const IV_LENGTH = 12;
17547
17825
  const AUTH_TAG_LENGTH = 16;
17548
17826
  let cachedEntries = null;
17549
- const logger$9 = createLogger("Vault");
17827
+ const logger$a = createLogger("Vault");
17550
17828
  function getVaultDir() {
17551
17829
  return electron.app.getPath("userData");
17552
17830
  }
@@ -17613,7 +17891,7 @@ function loadVault() {
17613
17891
  cachedEntries = JSON.parse(json);
17614
17892
  return cachedEntries;
17615
17893
  } catch (err) {
17616
- logger$9.error("Failed to load vault:", err);
17894
+ logger$a.error("Failed to load vault:", err);
17617
17895
  throw new Error(
17618
17896
  "Could not unlock the Agent Credential Vault. Check that OS secret storage is available and that the stored vault key can be decrypted."
17619
17897
  );
@@ -17755,7 +18033,7 @@ async function requestConsent(request) {
17755
18033
  }
17756
18034
  const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
17757
18035
  const MAX_ENTRIES = 1e3;
17758
- const logger$8 = createLogger("VaultAudit");
18036
+ const logger$9 = createLogger("VaultAudit");
17759
18037
  function getAuditPath() {
17760
18038
  return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
17761
18039
  }
@@ -17765,7 +18043,7 @@ function appendAuditEntry(entry) {
17765
18043
  fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
17766
18044
  fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
17767
18045
  } catch (err) {
17768
- logger$8.error("Failed to write audit log:", err);
18046
+ logger$9.error("Failed to write audit log:", err);
17769
18047
  }
17770
18048
  }
17771
18049
  function readAuditLog(limit = 100) {
@@ -17775,13 +18053,13 @@ function readAuditLog(limit = 100) {
17775
18053
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
17776
18054
  return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
17777
18055
  } catch (err) {
17778
- logger$8.error("Failed to read audit log:", err);
18056
+ logger$9.error("Failed to read audit log:", err);
17779
18057
  return [];
17780
18058
  }
17781
18059
  }
17782
18060
  let httpServer = null;
17783
18061
  let mcpAuthToken = null;
17784
- const logger$7 = createLogger("MCP");
18062
+ const logger$8 = createLogger("MCP");
17785
18063
  const MCP_AUTH_FILENAME = "mcp-auth.json";
17786
18064
  function getMcpAuthFilePath() {
17787
18065
  const configDir = process.env.VESSEL_CONFIG_DIR || path$1.join(
@@ -17817,7 +18095,7 @@ function writeMcpAuthFile(endpoint, token) {
17817
18095
  { mode: 384 }
17818
18096
  );
17819
18097
  } catch (err) {
17820
- logger$7.warn("Failed to write auth file:", err);
18098
+ logger$8.warn("Failed to write auth file:", err);
17821
18099
  }
17822
18100
  }
17823
18101
  function clearMcpAuthFile() {
@@ -17842,7 +18120,7 @@ function clearMcpAuthFile() {
17842
18120
  { mode: 384 }
17843
18121
  );
17844
18122
  } catch (err) {
17845
- logger$7.warn("Failed to clear auth file:", err);
18123
+ logger$8.warn("Failed to clear auth file:", err);
17846
18124
  }
17847
18125
  }
17848
18126
  function asTextResponse(text) {
@@ -17932,7 +18210,7 @@ async function getPostActionState(tabManager, name) {
17932
18210
  }
17933
18211
  }
17934
18212
  } catch (err) {
17935
- logger$7.warn("Failed to compute post-action state warning:", err);
18213
+ logger$8.warn("Failed to compute post-action state warning:", err);
17936
18214
  }
17937
18215
  return `${warning}
17938
18216
  [state: url=${wc.getURL()}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]`;
@@ -18034,7 +18312,7 @@ async function waitForConditionMcp(wc, text, selector, timeoutMs) {
18034
18312
  }
18035
18313
  })()
18036
18314
  `).catch((err) => {
18037
- logger$7.warn("Failed to gather wait_for timeout diagnostic:", err);
18315
+ logger$8.warn("Failed to gather wait_for timeout diagnostic:", err);
18038
18316
  return null;
18039
18317
  });
18040
18318
  if (typeof diagnostic === "string" && diagnostic.trim()) {
@@ -18121,7 +18399,7 @@ function registerTools(server, tabManager, runtime2) {
18121
18399
  const page = await extractContent(wc);
18122
18400
  pageType = detectPageType(page);
18123
18401
  } catch (err) {
18124
- 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);
18125
18403
  }
18126
18404
  }
18127
18405
  const scored = TOOL_DEFINITIONS.map((def) => {
@@ -19405,7 +19683,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
19405
19683
  void 0,
19406
19684
  h.color
19407
19685
  ).catch(
19408
- (err) => logger$7.warn("Failed to restore highlight after removal:", err)
19686
+ (err) => logger$8.warn("Failed to restore highlight after removal:", err)
19409
19687
  );
19410
19688
  }
19411
19689
  }
@@ -20253,7 +20531,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
20253
20531
  try {
20254
20532
  page = await extractContent(wc);
20255
20533
  } catch (err) {
20256
- logger$7.warn("Failed to extract page while generating suggestions:", err);
20534
+ logger$8.warn("Failed to extract page while generating suggestions:", err);
20257
20535
  return asTextResponse(
20258
20536
  "Could not read page. Try navigate to a working URL."
20259
20537
  );
@@ -20862,7 +21140,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
20862
21140
  try {
20863
21141
  targetDomain = new URL(tab.state.url).hostname;
20864
21142
  } catch (err) {
20865
- 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);
20866
21144
  return asErrorTextResponse("Could not parse active tab URL");
20867
21145
  }
20868
21146
  }
@@ -20928,7 +21206,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
20928
21206
  try {
20929
21207
  hostname = new URL(tab.state.url).hostname;
20930
21208
  } catch (err) {
20931
- 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);
20932
21210
  return asErrorTextResponse("Could not parse active tab URL");
20933
21211
  }
20934
21212
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -21022,7 +21300,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
21022
21300
  try {
21023
21301
  hostname = new URL(tab.state.url).hostname;
21024
21302
  } catch (err) {
21025
- 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);
21026
21304
  return asErrorTextResponse("Could not parse active tab URL");
21027
21305
  }
21028
21306
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -21194,7 +21472,7 @@ function startMcpServer(tabManager, runtime2, port) {
21194
21472
  await mcpServer.connect(transport);
21195
21473
  await transport.handleRequest(req, res);
21196
21474
  } catch (error) {
21197
- logger$7.error("Error handling request:", error);
21475
+ logger$8.error("Error handling request:", error);
21198
21476
  if (!res.headersSent) {
21199
21477
  res.writeHead(500, { "Content-Type": "application/json" });
21200
21478
  res.end(
@@ -21213,7 +21491,7 @@ function startMcpServer(tabManager, runtime2, port) {
21213
21491
  };
21214
21492
  server.once("error", (error) => {
21215
21493
  const message = error.code === "EADDRINUSE" ? `Port ${port} is already in use. MCP server not started.` : error.message;
21216
- logger$7.error("Server error:", error);
21494
+ logger$8.error("Server error:", error);
21217
21495
  clearMcpAuthFile();
21218
21496
  setMcpHealth({
21219
21497
  configuredPort: port,
@@ -21245,7 +21523,7 @@ function startMcpServer(tabManager, runtime2, port) {
21245
21523
  message: `MCP server listening on ${endpoint}.`
21246
21524
  });
21247
21525
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
21248
- logger$7.info(`Server listening on ${endpoint} (auth enabled)`);
21526
+ logger$8.info(`Server listening on ${endpoint} (auth enabled)`);
21249
21527
  }
21250
21528
  if (mcpAuthToken) {
21251
21529
  writeMcpAuthFile(endpoint, mcpAuthToken);
@@ -21284,7 +21562,7 @@ function stopMcpServer() {
21284
21562
  message: "MCP server is stopped."
21285
21563
  });
21286
21564
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
21287
- logger$7.info("Server stopped");
21565
+ logger$8.info("Server stopped");
21288
21566
  }
21289
21567
  resolve();
21290
21568
  });
@@ -21305,7 +21583,7 @@ const KIT_ID_UNSAFE_CHAR_PATTERN = /[\/\\\0]/;
21305
21583
  function isSafeAutomationKitId(id) {
21306
21584
  return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
21307
21585
  }
21308
- const logger$6 = createLogger("KitRegistry");
21586
+ const logger$7 = createLogger("KitRegistry");
21309
21587
  function getUserKitsDir() {
21310
21588
  return path$1.join(electron.app.getPath("userData"), "kits");
21311
21589
  }
@@ -21343,10 +21621,10 @@ function getInstalledKits() {
21343
21621
  if (isValidKit(parsed)) {
21344
21622
  kits.push(parsed);
21345
21623
  } else {
21346
- logger$6.warn(`Skipping invalid kit file: ${file}`);
21624
+ logger$7.warn(`Skipping invalid kit file: ${file}`);
21347
21625
  }
21348
21626
  } catch (err) {
21349
- logger$6.warn(`Failed to read kit file: ${file}`, err);
21627
+ logger$7.warn(`Failed to read kit file: ${file}`, err);
21350
21628
  }
21351
21629
  }
21352
21630
  return kits;
@@ -21418,7 +21696,7 @@ function uninstallKit(id, scheduledKitIds) {
21418
21696
  return errorResult("Failed to remove the kit file.");
21419
21697
  }
21420
21698
  }
21421
- const logger$5 = createLogger("Scheduler");
21699
+ const logger$6 = createLogger("Scheduler");
21422
21700
  let jobs = [];
21423
21701
  let removeIdleListener = null;
21424
21702
  let broadcastFn = null;
@@ -21443,7 +21721,7 @@ function saveJobs() {
21443
21721
  try {
21444
21722
  fs$1.writeFileSync(getJobsPath(), JSON.stringify(jobs, null, 2), "utf-8");
21445
21723
  } catch (err) {
21446
- logger$5.warn("Failed to save jobs:", err);
21724
+ logger$6.warn("Failed to save jobs:", err);
21447
21725
  }
21448
21726
  }
21449
21727
  function normalizeJob(job, now = /* @__PURE__ */ new Date()) {
@@ -21565,7 +21843,7 @@ async function fireJob(job, windowState, runtime2) {
21565
21843
  };
21566
21844
  startActivity();
21567
21845
  if (!settings2.chatProvider) {
21568
- logger$5.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
21846
+ logger$6.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
21569
21847
  appendActivity(
21570
21848
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
21571
21849
  );
@@ -21573,7 +21851,7 @@ async function fireJob(job, windowState, runtime2) {
21573
21851
  return;
21574
21852
  }
21575
21853
  if (process.env.VESSEL_DEBUG_SCHEDULER === "1" || process.env.VESSEL_DEBUG_SCHEDULER === "true") {
21576
- logger$5.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
21854
+ logger$6.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
21577
21855
  }
21578
21856
  try {
21579
21857
  const provider = createProvider(settings2.chatProvider);
@@ -21626,7 +21904,7 @@ function tick(windowState, runtime2) {
21626
21904
  saveJobs();
21627
21905
  broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
21628
21906
  void fireJob(job, windowState, runtime2).catch((err) => {
21629
- logger$5.warn("Unexpected error firing job:", err);
21907
+ logger$6.warn("Unexpected error firing job:", err);
21630
21908
  }).finally(fireNext);
21631
21909
  };
21632
21910
  fireNext();
@@ -22196,68 +22474,515 @@ function registerWindowControlHandlers(mainWindow) {
22196
22474
  mainWindow.close();
22197
22475
  });
22198
22476
  }
22199
- let activeChatProvider = null;
22200
- const logger$4 = createLogger("IPC");
22201
- const VALID_APPROVAL_MODES = ["auto", "confirm-dangerous", "manual"];
22202
- function registerIpcHandlers(windowState, runtime2) {
22203
- const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
22204
- let sidebarResizeRecoveryTimer = null;
22205
- let sidebarResizeActive = false;
22206
- let runtimeUpdateTimer = null;
22207
- let pendingRuntimeState = null;
22208
- const premiumApiOrigin = process.env.VESSEL_PREMIUM_API ? new URL(process.env.VESSEL_PREMIUM_API).origin : "https://vesselpremium.quantaintellect.com";
22209
- const clearSidebarResizeRecoveryTimer = () => {
22210
- if (!sidebarResizeRecoveryTimer) return;
22211
- clearTimeout(sidebarResizeRecoveryTimer);
22212
- sidebarResizeRecoveryTimer = null;
22213
- };
22214
- const restoreSidebarLayoutAfterResize = () => {
22215
- clearSidebarResizeRecoveryTimer();
22216
- if (!sidebarResizeActive) return;
22217
- sidebarResizeActive = false;
22218
- layoutViews(windowState);
22219
- };
22220
- const scheduleSidebarResizeRecovery = () => {
22221
- clearSidebarResizeRecoveryTimer();
22222
- sidebarResizeRecoveryTimer = setTimeout(() => {
22223
- restoreSidebarLayoutAfterResize();
22224
- }, 1200);
22225
- };
22226
- const flushRuntimeUpdate = () => {
22227
- runtimeUpdateTimer = null;
22228
- if (!pendingRuntimeState) return;
22229
- if (!chromeView.webContents.isDestroyed()) {
22230
- chromeView.webContents.send(
22231
- Channels.AGENT_RUNTIME_UPDATE,
22232
- pendingRuntimeState
22233
- );
22234
- }
22235
- if (!sidebarView.webContents.isDestroyed()) {
22236
- sidebarView.webContents.send(
22237
- Channels.AGENT_RUNTIME_UPDATE,
22238
- pendingRuntimeState
22239
- );
22240
- }
22241
- pendingRuntimeState = null;
22242
- };
22243
- const scheduleRuntimeUpdate = (state2) => {
22244
- pendingRuntimeState = state2;
22245
- if (runtimeUpdateTimer) return;
22246
- runtimeUpdateTimer = setTimeout(() => {
22247
- flushRuntimeUpdate();
22248
- }, 32);
22249
- };
22250
- electron.app.on("before-quit", () => {
22251
- if (runtimeUpdateTimer) {
22252
- clearTimeout(runtimeUpdateTimer);
22253
- runtimeUpdateTimer = null;
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;
22254
22534
  }
22255
- flushRuntimeUpdate();
22256
- });
22257
- const sendToRendererViews = (channel, ...args) => {
22258
- chromeView.webContents.send(channel, ...args);
22259
- sidebarView.webContents.send(channel, ...args);
22260
- devtoolsPanelView.webContents.send(channel, ...args);
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;
22573
+ }
22574
+ if (!tabManager.isAdBlockingEnabledForWebContents(webContentsId)) {
22575
+ callback({});
22576
+ return;
22577
+ }
22578
+ callback({ cancel: shouldBlockRequest(details) });
22579
+ });
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"
22624
+ };
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);
22261
22986
  };
22262
22987
  const watchPremiumCheckoutTab = (tabId) => {
22263
22988
  const tab = tabManager.getTab(tabId);
@@ -22388,6 +23113,52 @@ function registerIpcHandlers(windowState, runtime2) {
22388
23113
  tab.setAdBlockingEnabled(newState);
22389
23114
  return newState;
22390
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
+ });
22391
23162
  electron.ipcMain.handle(Channels.TAB_STATE_GET, () => ({
22392
23163
  tabs: tabManager.getAllStates(),
22393
23164
  activeId: tabManager.getActiveTabId() || ""
@@ -23387,6 +24158,21 @@ class AgentRuntime {
23387
24158
  }
23388
24159
  setApprovalMode(mode) {
23389
24160
  this.state.supervisor.approvalMode = mode;
24161
+ if (mode === "auto" && !this.state.supervisor.paused) {
24162
+ const approvals = this.state.supervisor.pendingApprovals;
24163
+ if (approvals.length > 0) {
24164
+ const actionIds = new Set(approvals.map((approval) => approval.actionId));
24165
+ this.state.supervisor.pendingApprovals = [];
24166
+ this.state.actions = this.state.actions.map(
24167
+ (action) => actionIds.has(action.id) ? { ...action, status: "running", error: void 0 } : action
24168
+ );
24169
+ for (const approval of approvals) {
24170
+ const resolve = this.pendingResolvers.get(approval.id);
24171
+ this.pendingResolvers.delete(approval.id);
24172
+ resolve?.(true);
24173
+ }
24174
+ }
24175
+ }
23390
24176
  this.emit();
23391
24177
  return this.getState();
23392
24178
  }
@@ -23880,157 +24666,6 @@ ${progress}
23880
24666
  });
23881
24667
  }
23882
24668
  }
23883
- const BLOCKED_RESOURCE_TYPES = /* @__PURE__ */ new Set([
23884
- "script",
23885
- "image",
23886
- "xhr",
23887
- "fetch",
23888
- "subFrame",
23889
- "media",
23890
- "ping",
23891
- "webSocket"
23892
- ]);
23893
- const BLOCKED_HOST_SUFFIXES = [
23894
- "doubleclick.net",
23895
- "googlesyndication.com",
23896
- "googleadservices.com",
23897
- "adservice.google.com",
23898
- "adnxs.com",
23899
- "adsrvr.org",
23900
- "taboola.com",
23901
- "outbrain.com",
23902
- "criteo.com",
23903
- "criteo.net",
23904
- "pubmatic.com",
23905
- "rubiconproject.com",
23906
- "openx.net",
23907
- "casalemedia.com",
23908
- "advertising.com",
23909
- "amazon-adsystem.com",
23910
- "adsymptotic.com",
23911
- "moatads.com",
23912
- "quantserve.com",
23913
- "scorecardresearch.com"
23914
- ];
23915
- const THIRD_PARTY_PATH_PATTERNS = [
23916
- /\/ads?[/?._-]/i,
23917
- /\/adservice/i,
23918
- /\/advert/i,
23919
- /\/prebid/i,
23920
- /\/banner/i,
23921
- /\/sponsor/i,
23922
- /\/promotions?\//i,
23923
- /\/trk\//i,
23924
- /\/track(ing)?\//i,
23925
- /\/beacon/i,
23926
- /\/pixel/i
23927
- ];
23928
- let installed = false;
23929
- function normalizeHostname(value) {
23930
- return value.trim().toLowerCase().replace(/\.$/, "");
23931
- }
23932
- function hostnameMatches(hostname, suffix) {
23933
- return hostname === suffix || hostname.endsWith(`.${suffix}`);
23934
- }
23935
- function parseHostname(url) {
23936
- try {
23937
- const parsed = new URL(url);
23938
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23939
- return null;
23940
- }
23941
- return normalizeHostname(parsed.hostname);
23942
- } catch {
23943
- return null;
23944
- }
23945
- }
23946
- function isThirdParty(url, firstPartyHost) {
23947
- if (!firstPartyHost) return true;
23948
- const target = normalizeHostname(url.hostname);
23949
- return !(target === firstPartyHost || target.endsWith(`.${firstPartyHost}`));
23950
- }
23951
- function shouldBlockRequest(details) {
23952
- if (!BLOCKED_RESOURCE_TYPES.has(details.resourceType)) return false;
23953
- let parsed;
23954
- try {
23955
- parsed = new URL(details.url);
23956
- } catch {
23957
- return false;
23958
- }
23959
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23960
- return false;
23961
- }
23962
- const hostname = normalizeHostname(parsed.hostname);
23963
- if (BLOCKED_HOST_SUFFIXES.some((suffix) => hostnameMatches(hostname, suffix))) {
23964
- return true;
23965
- }
23966
- const firstPartyHost = parseHostname(details.referrer) || parseHostname(details.initiator || "");
23967
- if (!isThirdParty(parsed, firstPartyHost)) return false;
23968
- const candidate = `${hostname}${parsed.pathname}${parsed.search}`;
23969
- return THIRD_PARTY_PATH_PATTERNS.some((pattern) => pattern.test(candidate));
23970
- }
23971
- function installAdBlocking(tabManager) {
23972
- if (installed) return;
23973
- installed = true;
23974
- electron.session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
23975
- const webContentsId = typeof details.webContentsId === "number" ? details.webContentsId : null;
23976
- if (webContentsId == null) {
23977
- callback({});
23978
- return;
23979
- }
23980
- if (!tabManager.isAdBlockingEnabledForWebContents(webContentsId)) {
23981
- callback({});
23982
- return;
23983
- }
23984
- callback({ cancel: shouldBlockRequest(details) });
23985
- });
23986
- }
23987
- function resolveDownloadPath(downloadDir, filename) {
23988
- fs$1.mkdirSync(downloadDir, { recursive: true });
23989
- const parsed = path.parse(filename);
23990
- let attempt = 0;
23991
- while (true) {
23992
- const candidateName = attempt === 0 ? filename : `${parsed.name} (${attempt})${parsed.ext}`;
23993
- const candidatePath = path.join(downloadDir, candidateName);
23994
- if (!fs$1.existsSync(candidatePath)) {
23995
- return candidatePath;
23996
- }
23997
- attempt += 1;
23998
- }
23999
- }
24000
- function installDownloadHandler(chromeView) {
24001
- electron.session.defaultSession.on("will-download", (_event, item) => {
24002
- const settings2 = loadSettings();
24003
- const downloadDir = settings2.downloadPath.trim() || electron.app.getPath("downloads");
24004
- const filename = item.getFilename();
24005
- const savePath = resolveDownloadPath(downloadDir, filename);
24006
- item.setSavePath(savePath);
24007
- const info = {
24008
- filename,
24009
- savePath,
24010
- totalBytes: item.getTotalBytes(),
24011
- receivedBytes: 0,
24012
- state: "progressing"
24013
- };
24014
- if (!chromeView.webContents.isDestroyed()) {
24015
- chromeView.webContents.send(Channels.DOWNLOAD_STARTED, info);
24016
- }
24017
- item.on("updated", (_event2, state2) => {
24018
- info.receivedBytes = item.getReceivedBytes();
24019
- info.totalBytes = item.getTotalBytes();
24020
- info.state = state2 === "progressing" ? "progressing" : "interrupted";
24021
- if (!chromeView.webContents.isDestroyed()) {
24022
- chromeView.webContents.send(Channels.DOWNLOAD_PROGRESS, info);
24023
- }
24024
- });
24025
- item.once("done", (_event2, state2) => {
24026
- info.receivedBytes = item.getReceivedBytes();
24027
- info.state = state2 === "completed" ? "completed" : "cancelled";
24028
- if (!chromeView.webContents.isDestroyed()) {
24029
- chromeView.webContents.send(Channels.DOWNLOAD_DONE, info);
24030
- }
24031
- });
24032
- });
24033
- }
24034
24669
  const logger$2 = createLogger("Shortcuts");
24035
24670
  function registerHighlightShortcut(mainWindow, tabManager) {
24036
24671
  const register = () => {
@@ -24051,8 +24686,18 @@ function registerHighlightShortcut(mainWindow, tabManager) {
24051
24686
  mainWindow.removeListener("focus", register);
24052
24687
  };
24053
24688
  }
24054
- function setupAppMenu() {
24689
+ function setupAppMenu(handlers) {
24055
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
+ },
24056
24701
  {
24057
24702
  label: "Edit",
24058
24703
  submenu: [
@@ -24064,6 +24709,26 @@ function setupAppMenu() {
24064
24709
  { role: "paste" },
24065
24710
  { role: "selectAll" }
24066
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
+ ]
24067
24732
  }
24068
24733
  ]);
24069
24734
  electron.Menu.setApplicationMenu(appMenu);
@@ -24285,6 +24950,11 @@ function closeSplash(splash, delayMs = 0) {
24285
24950
  }
24286
24951
  const logger = createLogger("Bootstrap");
24287
24952
  let runtime = null;
24953
+ function configureUserAgent() {
24954
+ const originalUA = electron.session.defaultSession.getUserAgent();
24955
+ const maskedUA = originalUA.replace(/ Electron\/[^\s]+/, "") + " Vessel/" + electron.app.getVersion();
24956
+ electron.session.defaultSession.setUserAgent(maskedUA);
24957
+ }
24288
24958
  function checkWritableUserData(userDataPath) {
24289
24959
  const issues = [];
24290
24960
  try {
@@ -24348,6 +25018,7 @@ Action: Open Settings (Ctrl+,) to choose a different port, then save to restart
24348
25018
  });
24349
25019
  }
24350
25020
  async function bootstrap() {
25021
+ configureUserAgent();
24351
25022
  const splash = createSplashWindow();
24352
25023
  const settings2 = loadSettings();
24353
25024
  const userDataPath = electron.app.getPath("userData");
@@ -24416,7 +25087,24 @@ async function bootstrap() {
24416
25087
  });
24417
25088
  registerIpcHandlers(windowState, runtime);
24418
25089
  registerHighlightShortcut(windowState.mainWindow, tabManager);
24419
- 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
+ });
24420
25108
  subscribe((state2) => {
24421
25109
  chromeView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);
24422
25110
  sidebarView.webContents.send(Channels.BOOKMARKS_UPDATE, state2);