@quanta-intellect/vessel-browser 0.1.120 → 0.1.123

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
@@ -52,10 +52,43 @@ function createLogger(scope) {
52
52
  error: (...args) => writeLog("error", scope, args)
53
53
  };
54
54
  }
55
+ const SIDEBAR_MIN_WIDTH = 240;
56
+ const SIDEBAR_MAX_WIDTH = 800;
57
+ const SIDEBAR_RESIZE_HANDLE_OVERLAP = 6;
58
+ const DETACHED_SIDEBAR_MIN_WIDTH = 360;
59
+ const DETACHED_SIDEBAR_MIN_HEIGHT = 480;
60
+ const DETACHED_SIDEBAR_DEFAULT_WIDTH = 420;
61
+ const DETACHED_SIDEBAR_DEFAULT_HEIGHT = 760;
62
+ function clampSidebarWidth(width) {
63
+ return Math.max(
64
+ SIDEBAR_MIN_WIDTH,
65
+ Math.min(SIDEBAR_MAX_WIDTH, Math.round(width))
66
+ );
67
+ }
68
+ function sanitizeSidebarPanelMode(value) {
69
+ return value === "closed" || value === "docked" || value === "detached" ? value : "docked";
70
+ }
71
+ function sanitizeSidebarDetachedBounds(value) {
72
+ if (!value || typeof value !== "object") return null;
73
+ const bounds = value;
74
+ const width = Number(bounds.width);
75
+ const height = Number(bounds.height);
76
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
77
+ const x = Number(bounds.x);
78
+ const y = Number(bounds.y);
79
+ return {
80
+ ...Number.isFinite(x) ? { x: Math.round(x) } : {},
81
+ ...Number.isFinite(y) ? { y: Math.round(y) } : {},
82
+ width: Math.max(DETACHED_SIDEBAR_MIN_WIDTH, Math.round(width)),
83
+ height: Math.max(DETACHED_SIDEBAR_MIN_HEIGHT, Math.round(height))
84
+ };
85
+ }
55
86
  const defaults = {
56
87
  defaultUrl: "https://start.duckduckgo.com",
57
88
  theme: "dark",
89
+ sidebarPanelMode: "docked",
58
90
  sidebarWidth: 400,
91
+ sidebarDetachedBounds: null,
59
92
  mcpPort: 3100,
60
93
  autoRestoreSession: true,
61
94
  clearBookmarksOnLaunch: false,
@@ -258,6 +291,10 @@ function loadSettings() {
258
291
  mergeChatProviderSecret(parsed.chatProvider ?? null)
259
292
  ),
260
293
  mcpPort: sanitizePort(parsed.mcpPort ?? defaults.mcpPort),
294
+ sidebarPanelMode: sanitizeSidebarPanelMode(parsed.sidebarPanelMode),
295
+ sidebarDetachedBounds: sanitizeSidebarDetachedBounds(
296
+ parsed.sidebarDetachedBounds
297
+ ),
261
298
  sourceDoNotAllowList: sanitizeStringList(
262
299
  parsed.sourceDoNotAllowList ?? defaults.sourceDoNotAllowList
263
300
  ),
@@ -305,6 +342,10 @@ function setSetting(key2, value) {
305
342
  loadSettings();
306
343
  if (key2 === "mcpPort") {
307
344
  settings.mcpPort = sanitizePort(value);
345
+ } else if (key2 === "sidebarPanelMode") {
346
+ settings.sidebarPanelMode = sanitizeSidebarPanelMode(value);
347
+ } else if (key2 === "sidebarDetachedBounds") {
348
+ settings.sidebarDetachedBounds = sanitizeSidebarDetachedBounds(value);
308
349
  } else if (key2 === "sourceDoNotAllowList") {
309
350
  settings.sourceDoNotAllowList = sanitizeStringList(value);
310
351
  } else if (key2 === "chatProvider") {
@@ -343,8 +384,43 @@ function setSetting(key2, value) {
343
384
  function flushPersist$5() {
344
385
  return saveDirty ? persistNow() : Promise.resolve();
345
386
  }
387
+ function isAirGapped() {
388
+ const hasAirGapSwitch = typeof electron.app?.commandLine?.hasSwitch === "function" && electron.app.commandLine.hasSwitch("air-gapped");
389
+ return hasAirGapSwitch || process.env.VESSEL_AIR_GAPPED === "1";
390
+ }
391
+ const LOCAL_PROVIDER_IDS = /* @__PURE__ */ new Set(["ollama", "llama_cpp"]);
392
+ function isLocalProvider(providerId) {
393
+ return LOCAL_PROVIDER_IDS.has(providerId);
394
+ }
395
+ function isLocalHostname(hostname) {
396
+ const normalized = hostname.toLowerCase();
397
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
398
+ }
399
+ function isLocalBaseUrl(baseUrl) {
400
+ if (!baseUrl) return false;
401
+ try {
402
+ return isLocalHostname(new URL(baseUrl).hostname);
403
+ } catch {
404
+ return false;
405
+ }
406
+ }
407
+ function getAirGapBlockReason(url) {
408
+ if (!isAirGapped()) return null;
409
+ let parsed;
410
+ try {
411
+ parsed = new URL(url);
412
+ } catch {
413
+ return null;
414
+ }
415
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
416
+ return null;
417
+ }
418
+ return isLocalHostname(parsed.hostname) ? null : `Air-gapped mode blocked network access to ${parsed.hostname}.`;
419
+ }
346
420
  function checkDomainPolicy(url) {
347
421
  if (!url || url.startsWith("about:")) return null;
422
+ const airGapBlockReason = getAirGapBlockReason(url);
423
+ if (airGapBlockReason) return airGapBlockReason;
348
424
  const settings2 = loadSettings();
349
425
  const policy = settings2.domainPolicy;
350
426
  if (policy.allowedDomains.length === 0 && policy.blockedDomains.length === 0) {
@@ -3441,6 +3517,9 @@ const Channels = {
3441
3517
  SIDEBAR_RESIZE: "ui:sidebar-resize",
3442
3518
  SIDEBAR_RESIZE_START: "ui:sidebar-resize-start",
3443
3519
  SIDEBAR_RESIZE_COMMIT: "ui:sidebar-resize-commit",
3520
+ SIDEBAR_POPOUT: "ui:sidebar-popout",
3521
+ SIDEBAR_DOCK: "ui:sidebar-dock",
3522
+ SIDEBAR_STATE_UPDATE: "ui:sidebar-state-update",
3444
3523
  SIDEBAR_CONTEXT_MENU: "ui:sidebar-context-menu",
3445
3524
  FOCUS_MODE_TOGGLE: "ui:focus-mode-toggle",
3446
3525
  SETTINGS_VISIBILITY: "ui:settings-visibility",
@@ -4691,6 +4770,9 @@ function assertFeatureUnlocked(featureName, featureLabel = featureName) {
4691
4770
  }
4692
4771
  }
4693
4772
  async function getCheckoutUrl(email) {
4773
+ if (isAirGapped()) {
4774
+ return errorResult("Checkout is unavailable in air-gapped mode.");
4775
+ }
4694
4776
  try {
4695
4777
  const params = new URLSearchParams();
4696
4778
  if (email) params.set("email", email);
@@ -4709,6 +4791,9 @@ async function getCheckoutUrl(email) {
4709
4791
  }
4710
4792
  }
4711
4793
  async function getPortalUrl() {
4794
+ if (isAirGapped()) {
4795
+ return errorResult("Billing portal is unavailable in air-gapped mode.");
4796
+ }
4712
4797
  const current = loadSettings().premium;
4713
4798
  const identifier = current.verificationToken;
4714
4799
  if (!identifier) {
@@ -4736,6 +4821,9 @@ async function getPortalUrl() {
4736
4821
  }
4737
4822
  }
4738
4823
  async function verifySubscription$1(identifier) {
4824
+ if (isAirGapped()) {
4825
+ return loadSettings().premium;
4826
+ }
4739
4827
  const current = loadSettings().premium;
4740
4828
  const verificationIdentifier = identifier || current.verificationToken || current.customerId;
4741
4829
  if (!verificationIdentifier) {
@@ -4773,6 +4861,9 @@ async function verifySubscription$1(identifier) {
4773
4861
  }
4774
4862
  }
4775
4863
  async function requestActivationCode(email) {
4864
+ if (isAirGapped()) {
4865
+ return errorResult("Activation codes are unavailable in air-gapped mode.");
4866
+ }
4776
4867
  const normalizedEmail = email.trim().toLowerCase();
4777
4868
  if (!normalizedEmail) {
4778
4869
  return errorResult("Email is required");
@@ -4796,6 +4887,11 @@ async function requestActivationCode(email) {
4796
4887
  }
4797
4888
  }
4798
4889
  async function verifyActivationCode(email, code, challengeToken) {
4890
+ if (isAirGapped()) {
4891
+ return errorResult("Activation codes are unavailable in air-gapped mode.", {
4892
+ state: getPremiumState()
4893
+ });
4894
+ }
4799
4895
  const normalizedEmail = email.trim().toLowerCase();
4800
4896
  const trimmedCode = code.trim();
4801
4897
  if (!normalizedEmail) {
@@ -4844,6 +4940,7 @@ async function verifyActivationCode(email, code, challengeToken) {
4844
4940
  let revalidationTimer = null;
4845
4941
  function startBackgroundRevalidation() {
4846
4942
  if (revalidationTimer) return;
4943
+ if (isAirGapped()) return;
4847
4944
  const { premium } = loadSettings();
4848
4945
  const identifier = premium.verificationToken || premium.customerId;
4849
4946
  if (identifier) {
@@ -4919,6 +5016,7 @@ let eventQueue = [];
4919
5016
  let flushTimer = null;
4920
5017
  let sessionStartedAt = null;
4921
5018
  function isEnabled() {
5019
+ if (isAirGapped()) return false;
4922
5020
  if (POSTHOG_API_KEY === "YOUR_POSTHOG_KEY_HERE") return false;
4923
5021
  if (process.env.VESSEL_DEV === "1") return false;
4924
5022
  return loadSettings().telemetryEnabled !== false;
@@ -6577,6 +6675,159 @@ function schedulePageSnapshotCapture(wc, sendToRendererViews, delayMs = 0, optio
6577
6675
  }
6578
6676
  scheduleTimerAt(wc, sendToRendererViews, nextDueAt, options);
6579
6677
  }
6678
+ function setSidebarPanelMode(state2, mode, reason = "user") {
6679
+ state2.uiState.sidebarPanelMode = mode;
6680
+ if (reason === "user") {
6681
+ setSetting("sidebarPanelMode", mode);
6682
+ }
6683
+ }
6684
+ function persistDetachedBounds(state2) {
6685
+ const sidebarWindow = state2.sidebarWindow;
6686
+ if (!sidebarWindow || sidebarWindow.isDestroyed()) return;
6687
+ const bounds = sidebarWindow.getBounds();
6688
+ state2.uiState.sidebarDetachedBounds = {
6689
+ x: bounds.x,
6690
+ y: bounds.y,
6691
+ width: bounds.width,
6692
+ height: bounds.height
6693
+ };
6694
+ setSetting("sidebarDetachedBounds", state2.uiState.sidebarDetachedBounds);
6695
+ }
6696
+ function closeDetachedSidebarWindow(state2) {
6697
+ const sidebarWindow = state2.sidebarWindow;
6698
+ if (!sidebarWindow) return false;
6699
+ state2.sidebarWindow = null;
6700
+ state2.sidebarWindowClosing = true;
6701
+ sidebarWindow.once("closed", () => {
6702
+ state2.sidebarWindowClosing = false;
6703
+ });
6704
+ sidebarWindow.close();
6705
+ return true;
6706
+ }
6707
+ function moveSidebarToMainWindow(state2) {
6708
+ state2.sidebarWindow?.contentView.removeChildView(state2.sidebarView);
6709
+ state2.mainWindow.contentView.addChildView(state2.sidebarView);
6710
+ }
6711
+ function getSidebarPanelState(state2) {
6712
+ return {
6713
+ open: state2.uiState.sidebarPanelMode !== "closed",
6714
+ width: state2.uiState.sidebarWidth,
6715
+ detached: state2.uiState.sidebarPanelMode === "detached"
6716
+ };
6717
+ }
6718
+ function emitSidebarPanelState(state2) {
6719
+ const panelState = getSidebarPanelState(state2);
6720
+ if (!state2.chromeView.webContents.isDestroyed()) {
6721
+ state2.chromeView.webContents.send(Channels.SIDEBAR_STATE_UPDATE, panelState);
6722
+ }
6723
+ if (!state2.sidebarView.webContents.isDestroyed()) {
6724
+ state2.sidebarView.webContents.send(
6725
+ Channels.SIDEBAR_STATE_UPDATE,
6726
+ panelState
6727
+ );
6728
+ }
6729
+ return panelState;
6730
+ }
6731
+ function isSidebarAttached(state2) {
6732
+ return state2.uiState.sidebarPanelMode === "docked";
6733
+ }
6734
+ function isSidebarDetached(state2) {
6735
+ return state2.uiState.sidebarPanelMode === "detached";
6736
+ }
6737
+ function closeSidebar(state2, relayout, reason = "user") {
6738
+ if (state2.sidebarWindow) {
6739
+ moveSidebarToMainWindow(state2);
6740
+ closeDetachedSidebarWindow(state2);
6741
+ }
6742
+ setSidebarPanelMode(state2, "closed", reason);
6743
+ relayout();
6744
+ return emitSidebarPanelState(state2);
6745
+ }
6746
+ function openDockedSidebar(state2, relayout) {
6747
+ setSidebarPanelMode(state2, "docked");
6748
+ relayout();
6749
+ return emitSidebarPanelState(state2);
6750
+ }
6751
+ function toggleDockedSidebar(state2, relayout) {
6752
+ if (isSidebarDetached(state2)) {
6753
+ state2.sidebarWindow?.focus();
6754
+ return getSidebarPanelState(state2);
6755
+ }
6756
+ setSidebarPanelMode(
6757
+ state2,
6758
+ state2.uiState.sidebarPanelMode === "docked" ? "closed" : "docked"
6759
+ );
6760
+ relayout();
6761
+ return emitSidebarPanelState(state2);
6762
+ }
6763
+ function layoutDetachedSidebar(state2) {
6764
+ if (!state2.sidebarWindow) return;
6765
+ const [width, height] = state2.sidebarWindow.getContentSize();
6766
+ state2.sidebarView.setBounds({ x: 0, y: 0, width, height });
6767
+ }
6768
+ function detachSidebar(state2, hooks) {
6769
+ if (state2.sidebarWindow) {
6770
+ state2.sidebarWindow.focus();
6771
+ return getSidebarPanelState(state2);
6772
+ }
6773
+ const detachedBounds = state2.uiState.sidebarDetachedBounds;
6774
+ const detachedWidth = detachedBounds?.width ?? Math.max(DETACHED_SIDEBAR_DEFAULT_WIDTH, state2.uiState.sidebarWidth);
6775
+ const detachedHeight = detachedBounds?.height ?? DETACHED_SIDEBAR_DEFAULT_HEIGHT;
6776
+ const sidebarWindow = new electron.BaseWindow({
6777
+ ...typeof detachedBounds?.x === "number" ? { x: detachedBounds.x } : {},
6778
+ ...typeof detachedBounds?.y === "number" ? { y: detachedBounds.y } : {},
6779
+ width: Math.max(DETACHED_SIDEBAR_MIN_WIDTH, Math.round(detachedWidth)),
6780
+ height: Math.max(DETACHED_SIDEBAR_MIN_HEIGHT, Math.round(detachedHeight)),
6781
+ minWidth: DETACHED_SIDEBAR_MIN_WIDTH,
6782
+ minHeight: DETACHED_SIDEBAR_MIN_HEIGHT,
6783
+ frame: true,
6784
+ show: false,
6785
+ backgroundColor: "#1a1a1e",
6786
+ title: "Vessel Agent",
6787
+ icon: hooks.getWindowIconPath()
6788
+ });
6789
+ state2.mainWindow.contentView.removeChildView(state2.sidebarView);
6790
+ sidebarWindow.contentView.addChildView(state2.sidebarView);
6791
+ state2.sidebarWindow = sidebarWindow;
6792
+ setSidebarPanelMode(state2, "detached");
6793
+ sidebarWindow.on("resize", () => {
6794
+ layoutDetachedSidebar(state2);
6795
+ persistDetachedBounds(state2);
6796
+ });
6797
+ sidebarWindow.on("move", () => persistDetachedBounds(state2));
6798
+ sidebarWindow.on("close", (event) => {
6799
+ if (state2.sidebarWindowClosing) return;
6800
+ event.preventDefault();
6801
+ dockSidebar(state2, hooks);
6802
+ });
6803
+ sidebarWindow.on("closed", () => {
6804
+ if (state2.sidebarWindow !== sidebarWindow) return;
6805
+ state2.sidebarWindow = null;
6806
+ setSidebarPanelMode(state2, "docked");
6807
+ state2.mainWindow.contentView.addChildView(state2.sidebarView);
6808
+ hooks.relayout();
6809
+ emitSidebarPanelState(state2);
6810
+ });
6811
+ hooks.relayout();
6812
+ layoutDetachedSidebar(state2);
6813
+ sidebarWindow.show();
6814
+ sidebarWindow.focus();
6815
+ return emitSidebarPanelState(state2);
6816
+ }
6817
+ function dockSidebar(state2, hooks) {
6818
+ const sidebarWindow = state2.sidebarWindow;
6819
+ if (!sidebarWindow) {
6820
+ setSidebarPanelMode(state2, "docked");
6821
+ hooks.relayout();
6822
+ return emitSidebarPanelState(state2);
6823
+ }
6824
+ setSidebarPanelMode(state2, "docked");
6825
+ moveSidebarToMainWindow(state2);
6826
+ hooks.relayout();
6827
+ closeDetachedSidebarWindow(state2);
6828
+ state2.mainWindow.focus();
6829
+ return emitSidebarPanelState(state2);
6830
+ }
6580
6831
  function enableClipboardShortcuts(view) {
6581
6832
  view.webContents.on("before-input-event", (event, input) => {
6582
6833
  if (!input.control && !input.meta) return;
@@ -6757,10 +7008,6 @@ function createMainWindow(onTabStateChange) {
6757
7008
  }
6758
7009
  });
6759
7010
  sidebarView.setBackgroundColor("#00000000");
6760
- sidebarView.webContents.on("context-menu", (event, params) => {
6761
- event.preventDefault();
6762
- void showSidebarContextMenu(mainWindow, sidebarView, params);
6763
- });
6764
7011
  mainWindow.contentView.addChildView(sidebarView);
6765
7012
  const devtoolsPanelView = new electron.WebContentsView({
6766
7013
  webPreferences: {
@@ -6778,8 +7025,9 @@ function createMainWindow(onTabStateChange) {
6778
7025
  enableClipboardShortcuts(devtoolsPanelView);
6779
7026
  const settings2 = loadSettings();
6780
7027
  const uiState = {
6781
- sidebarOpen: true,
7028
+ sidebarPanelMode: settings2.sidebarPanelMode === "detached" ? "docked" : settings2.sidebarPanelMode,
6782
7029
  sidebarWidth: settings2.sidebarWidth,
7030
+ sidebarDetachedBounds: settings2.sidebarDetachedBounds,
6783
7031
  focusMode: false,
6784
7032
  settingsOpen: false,
6785
7033
  devtoolsPanelOpen: false,
@@ -6797,6 +7045,8 @@ function createMainWindow(onTabStateChange) {
6797
7045
  });
6798
7046
  const state2 = {
6799
7047
  mainWindow,
7048
+ sidebarWindow: null,
7049
+ sidebarWindowClosing: false,
6800
7050
  chromeView,
6801
7051
  sidebarView,
6802
7052
  devtoolsPanelView,
@@ -6806,7 +7056,21 @@ function createMainWindow(onTabStateChange) {
6806
7056
  mainWindow.on("resize", () => layoutViews(state2));
6807
7057
  mainWindow.on("show", () => layoutViews(state2));
6808
7058
  mainWindow.on("focus", () => layoutViews(state2));
7059
+ mainWindow.on("closed", () => {
7060
+ closeDetachedSidebarWindow(state2);
7061
+ });
7062
+ sidebarView.webContents.on("context-menu", (event, params) => {
7063
+ event.preventDefault();
7064
+ void showSidebarContextMenu(
7065
+ state2.sidebarWindow ?? state2.mainWindow,
7066
+ sidebarView,
7067
+ params
7068
+ );
7069
+ });
6809
7070
  layoutViews(state2);
7071
+ if (settings2.sidebarPanelMode === "detached") {
7072
+ detachSidebar(state2, { relayout: () => layoutViews(state2), getWindowIconPath });
7073
+ }
6810
7074
  return state2;
6811
7075
  }
6812
7076
  function layoutViews(state2) {
@@ -6820,7 +7084,8 @@ function layoutViews(state2) {
6820
7084
  } = state2;
6821
7085
  const [width, height] = mainWindow.getContentSize();
6822
7086
  const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
6823
- const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
7087
+ const sidebarAttached = isSidebarAttached(state2);
7088
+ const sidebarWidth = sidebarAttached ? uiState.sidebarWidth : 0;
6824
7089
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
6825
7090
  const chromeNeedsFullHeight = uiState.settingsOpen;
6826
7091
  if (chromeNeedsFullHeight) {
@@ -6828,15 +7093,14 @@ function layoutViews(state2) {
6828
7093
  } else {
6829
7094
  chromeView.setBounds({ x: 0, y: 0, width, height: chromeHeight });
6830
7095
  }
6831
- const resizeHandleOverlap = 6;
6832
- if (uiState.sidebarOpen) {
7096
+ if (sidebarAttached) {
6833
7097
  sidebarView.setBounds({
6834
- x: width - sidebarWidth - resizeHandleOverlap,
7098
+ x: width - sidebarWidth - SIDEBAR_RESIZE_HANDLE_OVERLAP,
6835
7099
  y: chromeHeight,
6836
- width: sidebarWidth + resizeHandleOverlap,
7100
+ width: sidebarWidth + SIDEBAR_RESIZE_HANDLE_OVERLAP,
6837
7101
  height: height - chromeHeight
6838
7102
  });
6839
- } else {
7103
+ } else if (uiState.sidebarPanelMode === "closed") {
6840
7104
  sidebarView.setBounds({ x: width, y: 0, width: 0, height: 0 });
6841
7105
  }
6842
7106
  const contentWidth = width - sidebarWidth;
@@ -6852,8 +7116,10 @@ function layoutViews(state2) {
6852
7116
  }
6853
7117
  mainWindow.contentView.removeChildView(chromeView);
6854
7118
  mainWindow.contentView.addChildView(chromeView);
6855
- mainWindow.contentView.removeChildView(sidebarView);
6856
- mainWindow.contentView.addChildView(sidebarView);
7119
+ if (uiState.sidebarPanelMode !== "detached") {
7120
+ mainWindow.contentView.removeChildView(sidebarView);
7121
+ mainWindow.contentView.addChildView(sidebarView);
7122
+ }
6857
7123
  mainWindow.contentView.removeChildView(devtoolsPanelView);
6858
7124
  mainWindow.contentView.addChildView(devtoolsPanelView);
6859
7125
  const activeTab = tabManager.getActiveTab();
@@ -6870,16 +7136,17 @@ function resizeSidebarViews(state2) {
6870
7136
  const { mainWindow, sidebarView, devtoolsPanelView, tabManager, uiState } = state2;
6871
7137
  const [width, height] = mainWindow.getContentSize();
6872
7138
  const chromeHeight = uiState.focusMode ? 0 : CHROME_HEIGHT;
6873
- const sidebarWidth = uiState.sidebarOpen ? uiState.sidebarWidth : 0;
7139
+ const sidebarWidth = isSidebarAttached(state2) ? uiState.sidebarWidth : 0;
6874
7140
  const devtoolsHeight = uiState.devtoolsPanelOpen ? uiState.devtoolsPanelHeight : 0;
6875
- const resizeHandleOverlap = 6;
6876
7141
  const contentWidth = width - sidebarWidth;
6877
- sidebarView.setBounds({
6878
- x: width - sidebarWidth - resizeHandleOverlap,
6879
- y: chromeHeight,
6880
- width: sidebarWidth + resizeHandleOverlap,
6881
- height: height - chromeHeight
6882
- });
7142
+ if (uiState.sidebarPanelMode !== "detached") {
7143
+ sidebarView.setBounds({
7144
+ x: width - sidebarWidth - SIDEBAR_RESIZE_HANDLE_OVERLAP,
7145
+ y: chromeHeight,
7146
+ width: sidebarWidth + SIDEBAR_RESIZE_HANDLE_OVERLAP,
7147
+ height: height - chromeHeight
7148
+ });
7149
+ }
6883
7150
  if (uiState.devtoolsPanelOpen) {
6884
7151
  devtoolsPanelView.setBounds({
6885
7152
  x: 0,
@@ -7020,6 +7287,7 @@ const state$3 = {
7020
7287
  userDataPath: "",
7021
7288
  settingsPath: "",
7022
7289
  startupIssues: [],
7290
+ airGapped: isAirGapped(),
7023
7291
  mcp: {
7024
7292
  configuredPort: 3100,
7025
7293
  activePort: null,
@@ -7047,6 +7315,7 @@ function getRuntimeHealth() {
7047
7315
  userDataPath: state$3.userDataPath,
7048
7316
  settingsPath: state$3.settingsPath,
7049
7317
  startupIssues: state$3.startupIssues.map((issue) => ({ ...issue })),
7318
+ airGapped: state$3.airGapped,
7050
7319
  mcp: { ...state$3.mcp }
7051
7320
  };
7052
7321
  }
@@ -7577,8 +7846,8 @@ function isLoopbackBaseUrl(baseUrl) {
7577
7846
  }
7578
7847
  function resolveAgentToolProfile(config) {
7579
7848
  const providerId = config.id;
7580
- const isLocalProvider = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
7581
- if (!isLocalProvider) return "default";
7849
+ const isLocalProvider2 = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
7850
+ if (!isLocalProvider2) return "default";
7582
7851
  const sizeInBillions = parseModelSizeInBillions(config.model);
7583
7852
  if (sizeInBillions === null) {
7584
7853
  return "compact";
@@ -9379,6 +9648,15 @@ function validateProviderConnection(config, options = { requireModel: true }) {
9379
9648
  if (!meta) {
9380
9649
  return "Selected AI provider is not supported.";
9381
9650
  }
9651
+ if (isAirGapped()) {
9652
+ if (meta.id === "custom") {
9653
+ if (!isLocalBaseUrl(normalized.baseUrl)) {
9654
+ return "Air-gapped mode only allows local AI providers. Use a localhost base URL for custom providers.";
9655
+ }
9656
+ } else if (!isLocalProvider(meta.id)) {
9657
+ return `Air-gapped mode only allows local AI providers (Ollama, llama.cpp). ${meta.name} requires internet access.`;
9658
+ }
9659
+ }
9382
9660
  if (meta.type !== "codex_oauth" && meta.requiresApiKey && !normalized.apiKey) {
9383
9661
  return `${meta.name} requires an API key. Open settings (Ctrl+,) to add one.`;
9384
9662
  }
@@ -11820,7 +12098,7 @@ const TOOL_DEFINITIONS = [
11820
12098
  {
11821
12099
  name: "select_option",
11822
12100
  title: "Select Option",
11823
- description: "Select an option in a <select> dropdown by visible label or option value. Only works on <select> elements for checkboxes or radio buttons use click instead.",
12101
+ description: "Select an option in a <select> dropdown by visible label or option value. Only works on <select> elements. For checkboxes or radio buttons use click instead.",
11824
12102
  inputSchema: {
11825
12103
  index: zod.z.number().optional().describe("The select element index number"),
11826
12104
  selector: zod.z.string().optional().describe("CSS selector as fallback"),
@@ -11945,7 +12223,7 @@ const TOOL_DEFINITIONS = [
11945
12223
  "full",
11946
12224
  "debug"
11947
12225
  ]).optional().describe(
11948
- "Read mode: glance (fastest viewport snapshot, no JS extraction, ideal for heavy pages), visible_only/results_only/forms_only/summary/text_only for narrow reads, full/debug for the complete page dump"
12226
+ "Read mode: glance (fastest: viewport snapshot, no JS extraction, ideal for heavy pages), visible_only/results_only/forms_only/summary/text_only for narrow reads, full/debug for the complete page dump"
11949
12227
  )
11950
12228
  },
11951
12229
  tier: 0
@@ -11953,7 +12231,7 @@ const TOOL_DEFINITIONS = [
11953
12231
  {
11954
12232
  name: "screenshot",
11955
12233
  title: "Screenshot",
11956
- description: "Take a screenshot of the current page — see exactly what the user sees. Returns the image for visual analysis. Use when you need to verify visual layout, check what's actually rendered on screen, or when text extraction fails on heavy pages.",
12234
+ description: "Take a screenshot of the current page. Returns the image for visual analysis. Use when you need to verify visual layout, check what's actually rendered on screen, or when text extraction fails on heavy pages.",
11957
12235
  inputSchema: {},
11958
12236
  tier: 1
11959
12237
  },
@@ -12287,7 +12565,7 @@ const TOOL_DEFINITIONS = [
12287
12565
  inputSchema: {
12288
12566
  index: zod.z.number().optional().describe("Element index of the table to extract"),
12289
12567
  selector: zod.z.string().optional().describe(
12290
- "CSS selector for the table (auto-detected if omitted uses first table)"
12568
+ "CSS selector for the table (auto-detected if omitted; uses first table)"
12291
12569
  )
12292
12570
  },
12293
12571
  tier: 1,
@@ -26500,6 +26778,12 @@ function getAdBlockDecision(details) {
26500
26778
  }
26501
26779
  let installed = false;
26502
26780
  const defaultSessionTabManagers = /* @__PURE__ */ new Set();
26781
+ function getRequestFilterDecision(details, adBlockingEnabled) {
26782
+ if (getAirGapBlockReason(details.url)) {
26783
+ return { cancel: true };
26784
+ }
26785
+ return adBlockingEnabled ? getAdBlockDecision(details) : null;
26786
+ }
26503
26787
  function installAdBlocking(tabManager) {
26504
26788
  defaultSessionTabManagers.add(tabManager);
26505
26789
  if (installed) return;
@@ -26507,17 +26791,18 @@ function installAdBlocking(tabManager) {
26507
26791
  electron.session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
26508
26792
  const webContentsId = typeof details.webContentsId === "number" ? details.webContentsId : null;
26509
26793
  if (webContentsId == null) {
26510
- callback({});
26794
+ callback(getRequestFilterDecision(details, false) ?? {});
26511
26795
  return;
26512
26796
  }
26513
26797
  const manager = [...defaultSessionTabManagers].find(
26514
26798
  (candidate) => candidate.findTabByWebContentsId(webContentsId)
26515
26799
  );
26516
- if (!manager?.isAdBlockingEnabledForWebContents(webContentsId)) {
26517
- callback({});
26518
- return;
26519
- }
26520
- callback(getAdBlockDecision(details));
26800
+ callback(
26801
+ getRequestFilterDecision(
26802
+ details,
26803
+ manager?.isAdBlockingEnabledForWebContents(webContentsId) ?? false
26804
+ ) ?? {}
26805
+ );
26521
26806
  });
26522
26807
  }
26523
26808
  function unregisterAdBlockingTabManager(tabManager) {
@@ -26527,14 +26812,15 @@ function installAdBlockingForSession(ses, tabManager) {
26527
26812
  ses.webRequest.onBeforeRequest((details, callback) => {
26528
26813
  const webContentsId = typeof details.webContentsId === "number" ? details.webContentsId : null;
26529
26814
  if (webContentsId == null) {
26530
- callback({});
26531
- return;
26532
- }
26533
- if (!tabManager.isAdBlockingEnabledForWebContents(webContentsId)) {
26534
- callback({});
26815
+ callback(getRequestFilterDecision(details, false) ?? {});
26535
26816
  return;
26536
26817
  }
26537
- callback(getAdBlockDecision(details));
26818
+ callback(
26819
+ getRequestFilterDecision(
26820
+ details,
26821
+ tabManager.isAdBlockingEnabledForWebContents(webContentsId)
26822
+ ) ?? {}
26823
+ );
26538
26824
  });
26539
26825
  }
26540
26826
  const filePath$1 = () => path$1.join(electron.app.getPath("userData"), "vessel-downloads.json");
@@ -27865,6 +28151,12 @@ const logger$7 = createLogger("CodexIPC");
27865
28151
  function registerCodexHandlers() {
27866
28152
  electron.ipcMain.handle(Channels.CODEX_START_AUTH, async (event) => {
27867
28153
  assertTrustedIpcSender(event);
28154
+ if (isAirGapped()) {
28155
+ return {
28156
+ ok: false,
28157
+ error: "Codex authentication is unavailable in air-gapped mode."
28158
+ };
28159
+ }
27868
28160
  const wc = event.sender;
27869
28161
  if (!wc || wc.isDestroyed()) {
27870
28162
  return {
@@ -27975,6 +28267,12 @@ const logger$5 = createLogger("OpenRouterIPC");
27975
28267
  function registerOpenRouterHandlers(applySettingChange) {
27976
28268
  electron.ipcMain.handle(Channels.OPENROUTER_START_AUTH, async (event) => {
27977
28269
  assertTrustedIpcSender(event);
28270
+ if (isAirGapped()) {
28271
+ return {
28272
+ ok: false,
28273
+ error: "OpenRouter authentication is unavailable in air-gapped mode."
28274
+ };
28275
+ }
27978
28276
  const wc = event.sender;
27979
28277
  if (!wc || wc.isDestroyed()) {
27980
28278
  return {
@@ -28019,6 +28317,116 @@ function registerOpenRouterHandlers(applySettingChange) {
28019
28317
  return { ok: true };
28020
28318
  });
28021
28319
  }
28320
+ function registerSidebarHandlers(windowState, requireTrusted) {
28321
+ let sidebarResizeRecoveryTimer = null;
28322
+ let sidebarResizeActive = false;
28323
+ const relayout = () => layoutViews(windowState);
28324
+ const clearSidebarResizeRecoveryTimer = () => {
28325
+ if (!sidebarResizeRecoveryTimer) return;
28326
+ clearTimeout(sidebarResizeRecoveryTimer);
28327
+ sidebarResizeRecoveryTimer = null;
28328
+ };
28329
+ const stopSidebarResize = () => {
28330
+ sidebarResizeActive = false;
28331
+ clearSidebarResizeRecoveryTimer();
28332
+ };
28333
+ const restoreSidebarLayoutAfterResize = () => {
28334
+ clearSidebarResizeRecoveryTimer();
28335
+ if (!sidebarResizeActive) return;
28336
+ sidebarResizeActive = false;
28337
+ relayout();
28338
+ };
28339
+ const scheduleSidebarResizeRecovery = () => {
28340
+ clearSidebarResizeRecoveryTimer();
28341
+ sidebarResizeRecoveryTimer = setTimeout(() => {
28342
+ restoreSidebarLayoutAfterResize();
28343
+ }, 1200);
28344
+ };
28345
+ windowState.mainWindow.once("closed", stopSidebarResize);
28346
+ electron.ipcMain.handle(Channels.SIDEBAR_TOGGLE, (event) => {
28347
+ requireTrusted(event);
28348
+ return toggleDockedSidebar(windowState, relayout);
28349
+ });
28350
+ electron.ipcMain.handle(Channels.SIDEBAR_NAVIGATE, (event, tab) => {
28351
+ requireTrusted(event);
28352
+ assertString(tab, "tab");
28353
+ if (windowState.uiState.sidebarPanelMode === "closed") {
28354
+ openDockedSidebar(windowState, relayout);
28355
+ }
28356
+ if (!windowState.sidebarView.webContents.isDestroyed()) {
28357
+ windowState.sidebarView.webContents.send(Channels.SIDEBAR_NAVIGATE, tab);
28358
+ }
28359
+ windowState.sidebarWindow?.focus();
28360
+ return emitSidebarPanelState(windowState);
28361
+ });
28362
+ electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_START, (event) => {
28363
+ requireTrusted(event);
28364
+ if (isSidebarDetached(windowState)) return;
28365
+ sidebarResizeActive = true;
28366
+ clearSidebarResizeRecoveryTimer();
28367
+ const [width, height] = windowState.mainWindow.getContentSize();
28368
+ const chromeHeight = windowState.uiState.focusMode ? 0 : CHROME_HEIGHT;
28369
+ const sidebarWidth = windowState.uiState.sidebarWidth;
28370
+ windowState.sidebarView.setBounds({
28371
+ x: width - sidebarWidth - SIDEBAR_RESIZE_HANDLE_OVERLAP,
28372
+ y: chromeHeight,
28373
+ width: sidebarWidth + SIDEBAR_RESIZE_HANDLE_OVERLAP,
28374
+ height: height - chromeHeight
28375
+ });
28376
+ scheduleSidebarResizeRecovery();
28377
+ });
28378
+ electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (event, width) => {
28379
+ requireTrusted(event);
28380
+ assertNumber(width, "width");
28381
+ if (isSidebarDetached(windowState)) {
28382
+ return windowState.uiState.sidebarWidth;
28383
+ }
28384
+ const clamped = clampSidebarWidth(width);
28385
+ windowState.uiState.sidebarWidth = clamped;
28386
+ resizeSidebarViews(windowState);
28387
+ emitSidebarPanelState(windowState);
28388
+ return clamped;
28389
+ });
28390
+ electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_COMMIT, (event) => {
28391
+ requireTrusted(event);
28392
+ if (isSidebarDetached(windowState)) return;
28393
+ stopSidebarResize();
28394
+ setSetting("sidebarWidth", windowState.uiState.sidebarWidth);
28395
+ relayout();
28396
+ });
28397
+ electron.ipcMain.handle(Channels.SIDEBAR_POPOUT, (event) => {
28398
+ requireTrusted(event);
28399
+ stopSidebarResize();
28400
+ return detachSidebar(windowState, {
28401
+ relayout,
28402
+ getWindowIconPath
28403
+ });
28404
+ });
28405
+ electron.ipcMain.handle(Channels.SIDEBAR_DOCK, (event) => {
28406
+ requireTrusted(event);
28407
+ stopSidebarResize();
28408
+ return dockSidebar(windowState, { relayout });
28409
+ });
28410
+ electron.ipcMain.on(
28411
+ Channels.RENDERER_VIEW_READY,
28412
+ (event, view) => {
28413
+ requireTrusted(event);
28414
+ if (view !== "sidebar") return;
28415
+ emitSidebarPanelState(windowState);
28416
+ }
28417
+ );
28418
+ electron.ipcMain.handle(Channels.SETTINGS_VISIBILITY, (event, open) => {
28419
+ requireTrusted(event);
28420
+ windowState.uiState.settingsOpen = open;
28421
+ if (open) {
28422
+ closeSidebar(windowState, relayout, "temporary");
28423
+ } else {
28424
+ relayout();
28425
+ emitSidebarPanelState(windowState);
28426
+ }
28427
+ return windowState.uiState.settingsOpen;
28428
+ });
28429
+ }
28022
28430
  const SUPPORT_API = process.env.VESSEL_SUPPORT_API || process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
28023
28431
  const MAX_FEEDBACK_MESSAGE_LENGTH = 5e3;
28024
28432
  const FEEDBACK_REQUEST_TIMEOUT_MS = 15e3;
@@ -28026,6 +28434,9 @@ function isValidEmail(email) {
28026
28434
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
28027
28435
  }
28028
28436
  async function submitFeedback(payload) {
28437
+ if (isAirGapped()) {
28438
+ return errorResult("Feedback submission is disabled in air-gapped mode.");
28439
+ }
28029
28440
  const email = payload.email.trim().toLowerCase();
28030
28441
  const message = payload.message.trim();
28031
28442
  if (!isValidEmail(email)) {
@@ -28222,6 +28633,15 @@ function compareVersions(a, b) {
28222
28633
  async function checkForUpdates() {
28223
28634
  const currentVersion = electron.app.getVersion();
28224
28635
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
28636
+ if (isAirGapped()) {
28637
+ return {
28638
+ currentVersion,
28639
+ latestVersion: null,
28640
+ updateAvailable: false,
28641
+ checkedAt,
28642
+ error: "Update checks are disabled in air-gapped mode."
28643
+ };
28644
+ }
28225
28645
  try {
28226
28646
  const response = await fetch(GITHUB_LATEST_RELEASE_API_URL, {
28227
28647
  headers: { accept: "application/vnd.github+json", "user-agent": `Vessel/${currentVersion}` }
@@ -28252,6 +28672,9 @@ async function checkForUpdates() {
28252
28672
  }
28253
28673
  }
28254
28674
  async function openUpdateDownload() {
28675
+ if (isAirGapped()) {
28676
+ throw new Error("Update downloads are unavailable in air-gapped mode.");
28677
+ }
28255
28678
  await openExternalAllowlisted(RELEASES_URL, { hosts: ["github.com"] });
28256
28679
  }
28257
28680
  let activeChatProvider = null;
@@ -28317,27 +28740,8 @@ function registerIpcHandlers(windowState, runtime2) {
28317
28740
  requireTrusted(event);
28318
28741
  return false;
28319
28742
  });
28320
- let sidebarResizeRecoveryTimer = null;
28321
- let sidebarResizeActive = false;
28322
28743
  let runtimeUpdateTimer = null;
28323
28744
  let pendingRuntimeState = null;
28324
- const clearSidebarResizeRecoveryTimer = () => {
28325
- if (!sidebarResizeRecoveryTimer) return;
28326
- clearTimeout(sidebarResizeRecoveryTimer);
28327
- sidebarResizeRecoveryTimer = null;
28328
- };
28329
- const restoreSidebarLayoutAfterResize = () => {
28330
- clearSidebarResizeRecoveryTimer();
28331
- if (!sidebarResizeActive) return;
28332
- sidebarResizeActive = false;
28333
- layoutViews(windowState);
28334
- };
28335
- const scheduleSidebarResizeRecovery = () => {
28336
- clearSidebarResizeRecoveryTimer();
28337
- sidebarResizeRecoveryTimer = setTimeout(() => {
28338
- restoreSidebarLayoutAfterResize();
28339
- }, 1200);
28340
- };
28341
28745
  const flushRuntimeUpdate = () => {
28342
28746
  runtimeUpdateTimer = null;
28343
28747
  if (!pendingRuntimeState) return;
@@ -28652,87 +29056,12 @@ function registerIpcHandlers(windowState, runtime2) {
28652
29056
  );
28653
29057
  }
28654
29058
  });
28655
- electron.ipcMain.handle(Channels.SIDEBAR_TOGGLE, (event) => {
28656
- requireTrusted(event);
28657
- windowState.uiState.sidebarOpen = !windowState.uiState.sidebarOpen;
28658
- layoutViews(windowState);
28659
- return {
28660
- open: windowState.uiState.sidebarOpen,
28661
- width: windowState.uiState.sidebarWidth
28662
- };
28663
- });
28664
- electron.ipcMain.handle(Channels.SIDEBAR_NAVIGATE, (event, tab) => {
28665
- requireTrusted(event);
28666
- assertString(tab, "tab");
28667
- if (!windowState.uiState.sidebarOpen) {
28668
- windowState.uiState.sidebarOpen = true;
28669
- layoutViews(windowState);
28670
- }
28671
- if (!sidebarView.webContents.isDestroyed()) {
28672
- sidebarView.webContents.send(Channels.SIDEBAR_NAVIGATE, tab);
28673
- }
28674
- return {
28675
- open: windowState.uiState.sidebarOpen,
28676
- width: windowState.uiState.sidebarWidth
28677
- };
28678
- });
28679
- electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_START, (event) => {
28680
- requireTrusted(event);
28681
- sidebarResizeActive = true;
28682
- clearSidebarResizeRecoveryTimer();
28683
- const [width, height] = windowState.mainWindow.getContentSize();
28684
- const chromeHeight = windowState.uiState.focusMode ? 0 : CHROME_HEIGHT;
28685
- const sidebarWidth = windowState.uiState.sidebarWidth;
28686
- const resizeHandleOverlap = 6;
28687
- windowState.sidebarView.setBounds({
28688
- x: width - sidebarWidth - resizeHandleOverlap,
28689
- y: chromeHeight,
28690
- width: sidebarWidth + resizeHandleOverlap,
28691
- height: height - chromeHeight
28692
- });
28693
- scheduleSidebarResizeRecovery();
28694
- });
28695
- electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (event, width) => {
28696
- requireTrusted(event);
28697
- assertNumber(width, "width");
28698
- const clamped = Math.max(240, Math.min(800, Math.round(width)));
28699
- windowState.uiState.sidebarWidth = clamped;
28700
- resizeSidebarViews(windowState);
28701
- return clamped;
28702
- });
28703
- electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_COMMIT, (event) => {
28704
- requireTrusted(event);
28705
- sidebarResizeActive = false;
28706
- clearSidebarResizeRecoveryTimer();
28707
- setSetting("sidebarWidth", windowState.uiState.sidebarWidth);
28708
- layoutViews(windowState);
28709
- });
28710
- electron.ipcMain.on(
28711
- Channels.RENDERER_VIEW_READY,
28712
- (event, view) => {
28713
- requireTrusted(event);
28714
- if (view !== "sidebar") return;
28715
- if (!windowState.uiState.sidebarOpen) {
28716
- windowState.uiState.sidebarOpen = true;
28717
- layoutViews(windowState);
28718
- }
28719
- }
28720
- );
28721
29059
  electron.ipcMain.handle(Channels.FOCUS_MODE_TOGGLE, (event) => {
28722
29060
  requireTrusted(event);
28723
29061
  windowState.uiState.focusMode = !windowState.uiState.focusMode;
28724
29062
  layoutViews(windowState);
28725
29063
  return windowState.uiState.focusMode;
28726
29064
  });
28727
- electron.ipcMain.handle(Channels.SETTINGS_VISIBILITY, (event, open) => {
28728
- requireTrusted(event);
28729
- windowState.uiState.settingsOpen = open;
28730
- if (open) {
28731
- windowState.uiState.sidebarOpen = false;
28732
- }
28733
- layoutViews(windowState);
28734
- return windowState.uiState.settingsOpen;
28735
- });
28736
29065
  electron.ipcMain.handle(Channels.SETTINGS_GET, (event) => {
28737
29066
  requireTrusted(event);
28738
29067
  return getRendererSettings();
@@ -28948,6 +29277,7 @@ function registerIpcHandlers(windowState, runtime2) {
28948
29277
  registerWindowControlHandlers(mainWindow);
28949
29278
  registerCodexHandlers();
28950
29279
  registerOpenRouterHandlers(applySettingChange);
29280
+ registerSidebarHandlers(windowState, requireTrusted);
28951
29281
  electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, (event) => {
28952
29282
  requireTrusted(event);
28953
29283
  return getInstalledKits();