@quanta-intellect/vessel-browser 0.1.104 → 0.1.115

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
@@ -81,7 +81,7 @@ const defaults = {
81
81
  const SAVE_DEBOUNCE_MS$6 = 150;
82
82
  const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
83
83
  const CODEX_TOKENS_FILENAME = "vessel-codex-tokens";
84
- const logger$p = createLogger("Settings");
84
+ const logger$r = createLogger("Settings");
85
85
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
86
86
  let settings = null;
87
87
  let settingsIssues = [];
@@ -289,7 +289,7 @@ function persistNow() {
289
289
  JSON.stringify(buildPersistedSettings(settings), null, 2),
290
290
  { encoding: "utf-8", mode: 384 }
291
291
  )
292
- ).then(() => fs.promises.chmod(getSettingsPath(), 384).catch(() => void 0)).catch((err) => logger$p.error("Failed to save settings:", err));
292
+ ).then(() => fs.promises.chmod(getSettingsPath(), 384).catch(() => void 0)).catch((err) => logger$r.error("Failed to save settings:", err));
293
293
  }
294
294
  function saveSettings() {
295
295
  saveDirty = true;
@@ -420,7 +420,7 @@ function loadTrustedAppURL(wc, url) {
420
420
  }
421
421
  const MAX_CUSTOM_HISTORY = 50;
422
422
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
423
- const logger$o = createLogger("Tab");
423
+ const logger$q = createLogger("Tab");
424
424
  const sessionCertExceptions = /* @__PURE__ */ new WeakMap();
425
425
  const sessionsWithVerifyProc = /* @__PURE__ */ new WeakSet();
426
426
  const CERT_VERIFY_TRUST = 0;
@@ -486,7 +486,7 @@ class Tab {
486
486
  guardedLoadURL(url, options) {
487
487
  const blockReason = this.getNavigationBlockReason(url);
488
488
  if (blockReason) {
489
- logger$o.warn(blockReason);
489
+ logger$q.warn(blockReason);
490
490
  return blockReason;
491
491
  }
492
492
  void this.view.webContents.loadURL(url, options);
@@ -570,7 +570,7 @@ class Tab {
570
570
  wc.setWindowOpenHandler(({ url, disposition }) => {
571
571
  const error = this.getNavigationBlockReason(url);
572
572
  if (error) {
573
- logger$o.warn(error);
573
+ logger$q.warn(error);
574
574
  return { action: "deny" };
575
575
  }
576
576
  this.onOpenUrl?.({
@@ -584,7 +584,7 @@ class Tab {
584
584
  const error = this.getNavigationBlockReason(url);
585
585
  if (!error) return;
586
586
  event.preventDefault();
587
- logger$o.warn(`${context}: ${error}`);
587
+ logger$q.warn(`${context}: ${error}`);
588
588
  };
589
589
  wc.on("will-navigate", (event, url) => {
590
590
  blockNavigation(event, url, "Blocked top-level navigation");
@@ -668,7 +668,7 @@ class Tab {
668
668
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 999px; }
669
669
  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
670
670
  ::-webkit-scrollbar-corner { background: transparent; }
671
- `).catch((err) => logger$o.warn("Failed to inject scrollbar CSS:", err));
671
+ `).catch((err) => logger$q.warn("Failed to inject scrollbar CSS:", err));
672
672
  });
673
673
  wc.on("page-favicon-updated", (_, favicons) => {
674
674
  this._state.favicon = favicons[0] || "";
@@ -704,7 +704,7 @@ class Tab {
704
704
  ).then((highlightedText) => {
705
705
  this.buildContextMenu(wc, params, highlightedText.trim());
706
706
  }).catch((err) => {
707
- logger$o.warn("Failed to inspect highlighted text for context menu:", err);
707
+ logger$q.warn("Failed to inspect highlighted text for context menu:", err);
708
708
  this.buildContextMenu(wc, params, "");
709
709
  });
710
710
  });
@@ -905,7 +905,7 @@ class Tab {
905
905
  "document.documentElement.outerHTML"
906
906
  );
907
907
  } catch (err) {
908
- logger$o.warn("Failed to retrieve page source:", err);
908
+ logger$q.warn("Failed to retrieve page source:", err);
909
909
  return;
910
910
  }
911
911
  const escaped = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1033,7 +1033,7 @@ class Tab {
1033
1033
  document.addEventListener('mouseup', window.__vesselHighlightHandler);
1034
1034
  }
1035
1035
  })()
1036
- `).catch((err) => logger$o.warn("Failed to inject highlight listener:", err));
1036
+ `).catch((err) => logger$q.warn("Failed to inject highlight listener:", err));
1037
1037
  } else {
1038
1038
  void wc.executeJavaScript(`
1039
1039
  (function() {
@@ -1044,7 +1044,7 @@ class Tab {
1044
1044
  delete window.__vesselHighlightHandler;
1045
1045
  }
1046
1046
  })()
1047
- `).catch((err) => logger$o.warn("Failed to remove highlight listener:", err));
1047
+ `).catch((err) => logger$q.warn("Failed to remove highlight listener:", err));
1048
1048
  }
1049
1049
  }
1050
1050
  get webContentsId() {
@@ -1081,7 +1081,7 @@ const SEARCH_ENGINE_PRESETS = {
1081
1081
  ecosia: { label: "Ecosia", url: "https://www.ecosia.org/search?q=" },
1082
1082
  kagi: { label: "Kagi", url: "https://kagi.com/search?q=" }
1083
1083
  };
1084
- const logger$n = createLogger("JsonPersistence");
1084
+ const logger$p = createLogger("JsonPersistence");
1085
1085
  function canUseSafeStorage() {
1086
1086
  try {
1087
1087
  return electron.safeStorage.isEncryptionAvailable();
@@ -1146,7 +1146,7 @@ function createDebouncedJsonPersistence({
1146
1146
  data,
1147
1147
  typeof data === "string" ? { encoding: "utf-8", mode: 384 } : { mode: 384 }
1148
1148
  )
1149
- ).then(() => fs.promises.chmod(filePath2, 384).catch(() => void 0)).catch((err) => logger$n.error(`Failed to save ${logLabel}:`, err));
1149
+ ).then(() => fs.promises.chmod(filePath2, 384).catch(() => void 0)).catch((err) => logger$p.error(`Failed to save ${logLabel}:`, err));
1150
1150
  };
1151
1151
  const schedule = () => {
1152
1152
  saveDirty2 = true;
@@ -1940,7 +1940,7 @@ function save$2() {
1940
1940
  }
1941
1941
  function emit$3() {
1942
1942
  if (!state$4) return;
1943
- const snapshot2 = { entries: [...state$4.entries] };
1943
+ const snapshot2 = listEntries$2();
1944
1944
  for (const listener of listeners$1) {
1945
1945
  listener(snapshot2);
1946
1946
  }
@@ -1949,6 +1949,17 @@ function getState$1() {
1949
1949
  load$3();
1950
1950
  return { entries: [...state$4.entries] };
1951
1951
  }
1952
+ function listEntries$2(offset = 0, limit = 200) {
1953
+ load$3();
1954
+ const safeOffset = Math.max(0, Math.floor(offset));
1955
+ const safeLimit = Math.max(1, Math.min(500, Math.floor(limit)));
1956
+ return {
1957
+ entries: state$4.entries.slice(safeOffset, safeOffset + safeLimit),
1958
+ offset: safeOffset,
1959
+ limit: safeLimit,
1960
+ total: state$4.entries.length
1961
+ };
1962
+ }
1952
1963
  function subscribe$1(listener) {
1953
1964
  listeners$1.add(listener);
1954
1965
  return () => {
@@ -2827,7 +2838,7 @@ function destroySession(tabId) {
2827
2838
  sessions.delete(tabId);
2828
2839
  }
2829
2840
  }
2830
- const logger$m = createLogger("TabManager");
2841
+ const logger$o = createLogger("TabManager");
2831
2842
  function sanitizePdfFilename(title) {
2832
2843
  const clean = title.replace(/[<>:"/\\|?*\x00-\x1f]/g, " ").replace(/\s+/g, " ").trim();
2833
2844
  const base = (clean || "Vessel Page").replace(/\.pdf$/i, "");
@@ -2851,6 +2862,7 @@ class TabManager {
2851
2862
  pageLoadCallback = null;
2852
2863
  securityStateCallback = null;
2853
2864
  closedTabs = [];
2865
+ lastSessionSignature = "";
2854
2866
  MAX_CLOSED_TABS = 20;
2855
2867
  isPrivate;
2856
2868
  sessionPartition;
@@ -2895,7 +2907,7 @@ class TabManager {
2895
2907
  this.window.contentView.addChildView(tab.view);
2896
2908
  if (background) {
2897
2909
  tab.view.setBounds({ x: 0, y: 0, width: 0, height: 0 });
2898
- this.broadcastState();
2910
+ this.broadcastState({ persistSession: true });
2899
2911
  } else {
2900
2912
  this.switchTab(id);
2901
2913
  }
@@ -2910,7 +2922,7 @@ class TabManager {
2910
2922
  }
2911
2923
  }
2912
2924
  this.activeTabId = id;
2913
- this.broadcastState();
2925
+ this.broadcastState({ persistSession: true });
2914
2926
  }
2915
2927
  closeTab(id) {
2916
2928
  const tab = this.tabs.get(id);
@@ -2942,7 +2954,7 @@ class TabManager {
2942
2954
  this.createTab();
2943
2955
  }
2944
2956
  } else {
2945
- this.broadcastState();
2957
+ this.broadcastState({ persistSession: true });
2946
2958
  }
2947
2959
  }
2948
2960
  navigateTab(id, url, postBody) {
@@ -2989,7 +3001,7 @@ class TabManager {
2989
3001
  } else {
2990
3002
  this.order.splice(firstNonPinned, 0, id);
2991
3003
  }
2992
- this.broadcastState();
3004
+ this.broadcastState({ persistSession: true });
2993
3005
  }
2994
3006
  unpinTab(id) {
2995
3007
  const tab = this.tabs.get(id);
@@ -3002,7 +3014,7 @@ class TabManager {
3002
3014
  } else {
3003
3015
  this.order.splice(firstNonPinned, 0, id);
3004
3016
  }
3005
- this.broadcastState();
3017
+ this.broadcastState({ persistSession: true });
3006
3018
  }
3007
3019
  createGroupFromTab(id, options) {
3008
3020
  const tab = this.tabs.get(id);
@@ -3026,7 +3038,7 @@ class TabManager {
3026
3038
  const previousGroupId = tab.state.groupId;
3027
3039
  tab.setGroup(groupId);
3028
3040
  this.removeGroupIfEmpty(previousGroupId);
3029
- this.broadcastState();
3041
+ this.broadcastState({ persistSession: true });
3030
3042
  }
3031
3043
  removeTabFromGroup(id) {
3032
3044
  const tab = this.tabs.get(id);
@@ -3034,20 +3046,20 @@ class TabManager {
3034
3046
  const groupId = tab.state.groupId;
3035
3047
  tab.setGroup(void 0);
3036
3048
  this.removeGroupIfEmpty(groupId);
3037
- this.broadcastState();
3049
+ this.broadcastState({ persistSession: true });
3038
3050
  }
3039
3051
  toggleGroupCollapsed(groupId) {
3040
3052
  const group = this.tabGroups.get(groupId);
3041
3053
  if (!group) return null;
3042
3054
  group.collapsed = !group.collapsed;
3043
- this.broadcastState();
3055
+ this.broadcastState({ persistSession: true });
3044
3056
  return group.collapsed;
3045
3057
  }
3046
3058
  setGroupColor(groupId, color) {
3047
3059
  const group = this.tabGroups.get(groupId);
3048
3060
  if (!group || !TAB_GROUP_COLORS.includes(color)) return;
3049
3061
  group.color = color;
3050
- this.broadcastState();
3062
+ this.broadcastState({ persistSession: true });
3051
3063
  }
3052
3064
  toggleMuted(id) {
3053
3065
  return this.tabs.get(id)?.toggleMuted() ?? null;
@@ -3207,7 +3219,7 @@ class TabManager {
3207
3219
  this.order = [];
3208
3220
  this.tabGroups.clear();
3209
3221
  this.activeTabId = null;
3210
- this.broadcastState();
3222
+ this.broadcastState({ persistSession: true });
3211
3223
  }
3212
3224
  lastReapply = /* @__PURE__ */ new Map();
3213
3225
  reapplyHighlights(url, wc) {
@@ -3226,7 +3238,7 @@ class TabManager {
3226
3238
  }));
3227
3239
  if (entries.length > 0) {
3228
3240
  void highlightBatchOnPage(wc, entries).catch(
3229
- (err) => logger$m.warn("Failed to batch highlight:", err)
3241
+ (err) => logger$o.warn("Failed to batch highlight:", err)
3230
3242
  );
3231
3243
  }
3232
3244
  }
@@ -3248,12 +3260,12 @@ class TabManager {
3248
3260
  const result = await captureSelectionHighlight(wc);
3249
3261
  if (result.success && result.text) {
3250
3262
  await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(
3251
- (err) => logger$m.warn("Failed to capture highlight:", err)
3263
+ (err) => logger$o.warn("Failed to capture highlight:", err)
3252
3264
  );
3253
3265
  }
3254
3266
  this.highlightCaptureCallback?.(result);
3255
3267
  } catch (err) {
3256
- logger$m.warn("Failed to capture highlight from page:", err);
3268
+ logger$o.warn("Failed to capture highlight from page:", err);
3257
3269
  this.highlightCaptureCallback?.({
3258
3270
  success: false,
3259
3271
  message: "Could not capture selection"
@@ -3278,7 +3290,7 @@ class TabManager {
3278
3290
  void this.removeHighlightMarksForText(wc, text);
3279
3291
  }
3280
3292
  } catch (err) {
3281
- logger$m.warn("Failed to remove highlight from matching tab:", err);
3293
+ logger$o.warn("Failed to remove highlight from matching tab:", err);
3282
3294
  }
3283
3295
  }
3284
3296
  this.highlightCaptureCallback?.({
@@ -3309,12 +3321,12 @@ class TabManager {
3309
3321
  void 0,
3310
3322
  color
3311
3323
  ).catch(
3312
- (err) => logger$m.warn("Failed to update highlight color:", err)
3324
+ (err) => logger$o.warn("Failed to update highlight color:", err)
3313
3325
  );
3314
3326
  });
3315
3327
  }
3316
3328
  } catch (err) {
3317
- logger$m.warn("Failed to iterate highlights for color change:", err);
3329
+ logger$o.warn("Failed to iterate highlights for color change:", err);
3318
3330
  }
3319
3331
  }
3320
3332
  this.highlightCaptureCallback?.({
@@ -3340,6 +3352,20 @@ class TabManager {
3340
3352
  }
3341
3353
  this.tabGroups.delete(groupId);
3342
3354
  }
3355
+ getSessionSignature(states) {
3356
+ return JSON.stringify({
3357
+ activeTabId: this.activeTabId,
3358
+ tabs: states.map((state2) => ({
3359
+ id: state2.id,
3360
+ url: state2.url || "about:blank",
3361
+ adBlockingEnabled: state2.adBlockingEnabled,
3362
+ isPinned: state2.isPinned,
3363
+ groupId: state2.groupId,
3364
+ groupName: state2.groupName,
3365
+ groupColor: state2.groupColor
3366
+ }))
3367
+ });
3368
+ }
3343
3369
  async removeHighlightMarksForText(wc, text) {
3344
3370
  await wc.executeJavaScript(
3345
3371
  `(function() {
@@ -3355,12 +3381,15 @@ class TabManager {
3355
3381
  });
3356
3382
  })()`
3357
3383
  ).catch(
3358
- (err) => logger$m.warn("Failed to remove highlight marks:", err)
3384
+ (err) => logger$o.warn("Failed to remove highlight marks:", err)
3359
3385
  );
3360
3386
  }
3361
- broadcastState() {
3387
+ broadcastState(meta = { persistSession: false }) {
3362
3388
  const states = this.getAllStates();
3363
- this.onStateChange(states, this.activeTabId || "");
3389
+ const sessionSignature = this.getSessionSignature(states);
3390
+ const persistSession = meta.persistSession || sessionSignature !== this.lastSessionSignature;
3391
+ this.lastSessionSignature = sessionSignature;
3392
+ this.onStateChange(states, this.activeTabId || "", { persistSession });
3364
3393
  const activeTab = this.getActiveTab();
3365
3394
  if (activeTab && this.activeTabId && this.securityStateCallback) {
3366
3395
  this.securityStateCallback(this.activeTabId, activeTab.securityState);
@@ -3422,6 +3451,8 @@ const Channels = {
3422
3451
  SETTINGS_HEALTH_GET: "settings:health:get",
3423
3452
  SETTINGS_HEALTH_UPDATE: "settings:health:update",
3424
3453
  MCP_REGENERATE_TOKEN: "mcp:regenerate-token",
3454
+ // Support
3455
+ SUPPORT_SUBMIT_FEEDBACK: "support:submit-feedback",
3425
3456
  // Bookmarks
3426
3457
  BOOKMARKS_GET: "bookmarks:get",
3427
3458
  BOOKMARKS_UPDATE: "bookmarks:update",
@@ -3493,6 +3524,7 @@ const Channels = {
3493
3524
  FIND_IN_PAGE_RESULT: "find:result",
3494
3525
  // Browsing history
3495
3526
  HISTORY_GET: "history:get",
3527
+ HISTORY_LIST: "history:list",
3496
3528
  HISTORY_SEARCH: "history:search",
3497
3529
  HISTORY_CLEAR: "history:clear",
3498
3530
  HISTORY_UPDATE: "history:update",
@@ -3582,6 +3614,10 @@ const Channels = {
3582
3614
  CODEX_CANCEL_AUTH: "codex:cancel-auth",
3583
3615
  CODEX_AUTH_STATUS: "codex:auth-status",
3584
3616
  CODEX_DISCONNECT: "codex:disconnect",
3617
+ // OpenRouter OAuth
3618
+ OPENROUTER_START_AUTH: "openrouter:start-auth",
3619
+ OPENROUTER_CANCEL_AUTH: "openrouter:cancel-auth",
3620
+ OPENROUTER_AUTH_STATUS: "openrouter:auth-status",
3585
3621
  // Updates
3586
3622
  UPDATES_CHECK: "updates:check",
3587
3623
  UPDATES_OPEN_DOWNLOAD: "updates:open-download",
@@ -3596,7 +3632,7 @@ const MAX_DIFF_BLOCKS = 500;
3596
3632
  function normalizeText$2(value) {
3597
3633
  return value.replace(/\s+/g, " ").trim();
3598
3634
  }
3599
- function truncateText(value, max = 180) {
3635
+ function truncateText$1(value, max = 180) {
3600
3636
  const normalized = normalizeText$2(value);
3601
3637
  if (normalized.length <= max) return normalized;
3602
3638
  return `${normalized.slice(0, max - 3)}...`;
@@ -3770,10 +3806,10 @@ function diffSnapshots(oldSnap, currentContent, currentTitle, currentHeadings) {
3770
3806
  addedBlocks.length,
3771
3807
  removedBlocks.length
3772
3808
  ),
3773
- before: changedPairs[0] ? truncateText(changedPairs[0].before) : removedBlocks[0] ? truncateText(removedBlocks[0]) : void 0,
3774
- after: changedPairs[0] ? truncateText(changedPairs[0].after) : addedBlocks[0] ? truncateText(addedBlocks[0]) : void 0,
3775
- addedItems: addedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText(item)),
3776
- removedItems: removedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText(item))
3809
+ before: changedPairs[0] ? truncateText$1(changedPairs[0].before) : removedBlocks[0] ? truncateText$1(removedBlocks[0]) : void 0,
3810
+ after: changedPairs[0] ? truncateText$1(changedPairs[0].after) : addedBlocks[0] ? truncateText$1(addedBlocks[0]) : void 0,
3811
+ addedItems: addedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText$1(item)),
3812
+ removedItems: removedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText$1(item))
3777
3813
  });
3778
3814
  }
3779
3815
  }
@@ -4546,7 +4582,7 @@ function errorResult(error, value) {
4546
4582
  function getErrorMessage(error, fallback = "Unknown error") {
4547
4583
  return error instanceof Error && error.message ? error.message : fallback;
4548
4584
  }
4549
- const logger$l = createLogger("Premium");
4585
+ const logger$n = createLogger("Premium");
4550
4586
  const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
4551
4587
  const FREE_TOOL_ITERATION_LIMIT = 50;
4552
4588
  const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
@@ -4591,6 +4627,15 @@ const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
4591
4627
  "research_approve_objectives",
4592
4628
  "research_export_report"
4593
4629
  ]);
4630
+ const PREMIUM_FEATURES = /* @__PURE__ */ new Set([
4631
+ "obsidian",
4632
+ "devtools",
4633
+ "unlimited_iterations",
4634
+ "vault",
4635
+ "human_vault",
4636
+ "automation_kits",
4637
+ "research"
4638
+ ]);
4594
4639
  function isPremium() {
4595
4640
  const { premium } = loadSettings();
4596
4641
  if (premium.status !== "active" && premium.status !== "trialing") {
@@ -4629,6 +4674,22 @@ function resetPremium() {
4629
4674
  function isToolGated(toolName) {
4630
4675
  return PREMIUM_TOOLS.has(toolName) && !isPremium();
4631
4676
  }
4677
+ function isFeatureGated(featureName) {
4678
+ return PREMIUM_FEATURES.has(featureName) && !isPremium();
4679
+ }
4680
+ function getPremiumToolGateMessage(toolName) {
4681
+ return `This tool (${toolName}) requires Vessel Premium. Upgrade at Settings > Premium to unlock screenshot, session management, workflow tracking, and more.`;
4682
+ }
4683
+ function assertToolUnlocked(toolName) {
4684
+ if (isToolGated(toolName)) {
4685
+ throw new Error(getPremiumToolGateMessage(toolName));
4686
+ }
4687
+ }
4688
+ function assertFeatureUnlocked(featureName, featureLabel = featureName) {
4689
+ if (isFeatureGated(featureName)) {
4690
+ throw new Error(`${featureLabel} requires Vessel Premium.`);
4691
+ }
4692
+ }
4632
4693
  async function getCheckoutUrl(email) {
4633
4694
  try {
4634
4695
  const params = new URLSearchParams();
@@ -4688,7 +4749,7 @@ async function verifySubscription$1(identifier) {
4688
4749
  });
4689
4750
  if (!res.ok) {
4690
4751
  const detail = await readApiErrorDetail(res);
4691
- logger$l.warn(
4752
+ logger$n.warn(
4692
4753
  "Verification API returned a non-OK status:",
4693
4754
  res.status,
4694
4755
  detail
@@ -4707,7 +4768,7 @@ async function verifySubscription$1(identifier) {
4707
4768
  setSetting("premium", updated);
4708
4769
  return updated;
4709
4770
  } catch (err) {
4710
- logger$l.warn("Verification failed:", err);
4771
+ logger$n.warn("Verification failed:", err);
4711
4772
  return current;
4712
4773
  }
4713
4774
  }
@@ -5319,7 +5380,18 @@ const EXTRACT_TIMEOUT_MAX_MS = 2e4;
5319
5380
  const MUTATION_CAPTURE_INTERVAL_MS = 5e3;
5320
5381
  const MUTATION_SETTLE_AFTER_MS = 1500;
5321
5382
  const AGENT_STREAM_IDLE_TIMEOUT_MS = 3e4;
5322
- const logger$k = createLogger("Extractor");
5383
+ const logger$m = createLogger("Extractor");
5384
+ const EXTRACTION_CACHE_TTL_MS = 1500;
5385
+ const MAX_EXTRACTION_CACHE_ENTRIES = 50;
5386
+ const extractionCache = /* @__PURE__ */ new Map();
5387
+ function invalidateExtractionCache(webContents) {
5388
+ const prefix = `${webContents.id}:`;
5389
+ for (const key2 of extractionCache.keys()) {
5390
+ if (key2.startsWith(prefix)) {
5391
+ extractionCache.delete(key2);
5392
+ }
5393
+ }
5394
+ }
5323
5395
  const EMPTY_PAGE_CONTENT = {
5324
5396
  title: "",
5325
5397
  content: "",
@@ -6075,9 +6147,9 @@ async function executeScript(webContents, script, options = {}) {
6075
6147
  const message = err instanceof Error ? err.message : String(err);
6076
6148
  const detail = `Failed to execute page script${label} on ${url}: ${message}`;
6077
6149
  if (options.warnOnFailure) {
6078
- logger$k.warn(detail);
6150
+ logger$m.warn(detail);
6079
6151
  } else {
6080
- logger$k.debug(detail);
6152
+ logger$m.debug(detail);
6081
6153
  }
6082
6154
  return null;
6083
6155
  } finally {
@@ -6186,7 +6258,7 @@ async function estimateExtractionTimeout(webContents) {
6186
6258
  return EXTRACT_TIMEOUT_BASE_MS + extra;
6187
6259
  }
6188
6260
  } catch (err) {
6189
- logger$k.warn("Failed to estimate extraction timeout, using base timeout:", err);
6261
+ logger$m.warn("Failed to estimate extraction timeout, using base timeout:", err);
6190
6262
  }
6191
6263
  return EXTRACT_TIMEOUT_BASE_MS;
6192
6264
  }
@@ -6206,9 +6278,14 @@ async function extractContentInner(webContents) {
6206
6278
  );
6207
6279
  }
6208
6280
  async function extractContent(webContents) {
6281
+ const cacheKey = `${webContents.id}:${webContents.getURL() || ""}`;
6282
+ const cached = extractionCache.get(cacheKey);
6283
+ if (cached && Date.now() - cached.capturedAt < EXTRACTION_CACHE_TTL_MS) {
6284
+ return structuredClone(cached.content);
6285
+ }
6209
6286
  try {
6210
6287
  const timeoutMs = await estimateExtractionTimeout(webContents);
6211
- return await Promise.race([
6288
+ const content = await Promise.race([
6212
6289
  extractContentInner(webContents),
6213
6290
  new Promise(
6214
6291
  (_, reject) => setTimeout(
@@ -6217,6 +6294,15 @@ async function extractContent(webContents) {
6217
6294
  )
6218
6295
  )
6219
6296
  ]);
6297
+ extractionCache.set(cacheKey, {
6298
+ capturedAt: Date.now(),
6299
+ content: structuredClone(content)
6300
+ });
6301
+ if (extractionCache.size > MAX_EXTRACTION_CACHE_ENTRIES) {
6302
+ const oldestKey = extractionCache.keys().next().value;
6303
+ if (oldestKey) extractionCache.delete(oldestKey);
6304
+ }
6305
+ return content;
6220
6306
  } catch (err) {
6221
6307
  const url = webContents.getURL() || "";
6222
6308
  let domain = "unknown";
@@ -6311,6 +6397,7 @@ function attachDestroyCleanup(wc) {
6311
6397
  const MAX_PERSISTED_DIFF_BURSTS = 50;
6312
6398
  const MAX_HISTORY_DAYS = 30;
6313
6399
  const SAVE_DEBOUNCE_MS$2 = 500;
6400
+ const BACKGROUND_DIFF_CAPTURE_DELAY_MS = 15e3;
6314
6401
  function getHistoryFilePath() {
6315
6402
  return path.join(electron.app.getPath("userData"), "vessel-page-diff-history.json");
6316
6403
  }
@@ -6443,7 +6530,7 @@ function computeNextSnapshotDueAt(wcId, now, delayMs) {
6443
6530
  const stableAfterActivityAt = lastActivityAt ? lastActivityAt + MUTATION_SETTLE_AFTER_MS : 0;
6444
6531
  return Math.max(now + delayMs, earliestAllowedAt, stableAfterActivityAt);
6445
6532
  }
6446
- function scheduleTimerAt(wc, sendToRendererViews, dueAt) {
6533
+ function scheduleTimerAt(wc, sendToRendererViews, dueAt, options = {}) {
6447
6534
  attachDestroyCleanup(wc);
6448
6535
  const wcId = wc.id;
6449
6536
  const existing = pendingPageSnapshotTimers.get(wcId);
@@ -6451,14 +6538,24 @@ function scheduleTimerAt(wc, sendToRendererViews, dueAt) {
6451
6538
  const timer = setTimeout(() => {
6452
6539
  cleanupTimersForWcId(wcId);
6453
6540
  if (wc.isDestroyed()) return;
6541
+ if (options.isActive && !options.isActive()) {
6542
+ scheduleTimerAt(
6543
+ wc,
6544
+ sendToRendererViews,
6545
+ Date.now() + BACKGROUND_DIFF_CAPTURE_DELAY_MS,
6546
+ options
6547
+ );
6548
+ return;
6549
+ }
6454
6550
  lastMutationSnapshotAt.set(wcId, Date.now());
6455
6551
  void capturePageSnapshot(wc.getURL(), wc, sendToRendererViews);
6456
6552
  }, Math.max(0, dueAt - Date.now()));
6457
6553
  pendingPageSnapshotTimers.set(wcId, timer);
6458
6554
  pendingPageSnapshotDueAt.set(wcId, dueAt);
6459
6555
  }
6460
- function notePageMutationActivity(wc, sendToRendererViews) {
6556
+ function notePageMutationActivity(wc, sendToRendererViews, options = {}) {
6461
6557
  if (wc.isDestroyed()) return;
6558
+ if (options.isActive && !options.isActive()) return;
6462
6559
  const wcId = wc.id;
6463
6560
  const now = Date.now();
6464
6561
  lastMutationActivityAt.set(wcId, now);
@@ -6466,18 +6563,19 @@ function notePageMutationActivity(wc, sendToRendererViews) {
6466
6563
  if (existingDueAt == null) return;
6467
6564
  const nextDueAt = computeNextSnapshotDueAt(wcId, now, 0);
6468
6565
  if (nextDueAt <= existingDueAt) return;
6469
- scheduleTimerAt(wc, sendToRendererViews, nextDueAt);
6566
+ scheduleTimerAt(wc, sendToRendererViews, nextDueAt, options);
6470
6567
  }
6471
- function schedulePageSnapshotCapture(wc, sendToRendererViews, delayMs = 0) {
6568
+ function schedulePageSnapshotCapture(wc, sendToRendererViews, delayMs = 0, options = {}) {
6472
6569
  if (wc.isDestroyed()) return;
6473
6570
  const wcId = wc.id;
6474
6571
  const now = Date.now();
6475
- const nextDueAt = computeNextSnapshotDueAt(wcId, now, delayMs);
6572
+ const effectiveDelayMs = options.isActive && !options.isActive() ? Math.max(delayMs, BACKGROUND_DIFF_CAPTURE_DELAY_MS) : delayMs;
6573
+ const nextDueAt = computeNextSnapshotDueAt(wcId, now, effectiveDelayMs);
6476
6574
  const existingDueAt = pendingPageSnapshotDueAt.get(wcId);
6477
6575
  if (existingDueAt != null && existingDueAt >= nextDueAt) {
6478
6576
  return;
6479
6577
  }
6480
- scheduleTimerAt(wc, sendToRendererViews, nextDueAt);
6578
+ scheduleTimerAt(wc, sendToRendererViews, nextDueAt, options);
6481
6579
  }
6482
6580
  function enableClipboardShortcuts(view) {
6483
6581
  view.webContents.on("before-input-event", (event, input) => {
@@ -6693,6 +6791,8 @@ function createMainWindow(onTabStateChange) {
6693
6791
  sidebarView.webContents.send(channel, ...args);
6694
6792
  };
6695
6793
  tabManager.onPageLoad((url, wc) => {
6794
+ const activeWc = tabManager.getActiveTab()?.view.webContents;
6795
+ if (activeWc?.id !== wc.id) return;
6696
6796
  void capturePageSnapshot(url, wc, sendToRendererViews);
6697
6797
  });
6698
6798
  const state2 = {
@@ -7385,8 +7485,9 @@ const PROVIDERS = {
7385
7485
  openrouter: {
7386
7486
  id: "openrouter",
7387
7487
  name: "OpenRouter",
7388
- defaultModel: "anthropic/claude-sonnet-4",
7488
+ defaultModel: "openrouter/free",
7389
7489
  models: [
7490
+ "openrouter/free",
7390
7491
  "anthropic/claude-sonnet-4",
7391
7492
  "anthropic/claude-haiku-4",
7392
7493
  "openai/gpt-4o",
@@ -7459,6 +7560,36 @@ const PROVIDERS = {
7459
7560
  apiKeyHint: "Optional — only if your endpoint requires authentication"
7460
7561
  }
7461
7562
  };
7563
+ function parseModelSizeInBillions(model) {
7564
+ const match = model.toLowerCase().match(/(?:^|[:/_\-\s])(\d+(?:\.\d+)?)b(?:$|[:/_\-\s])/i);
7565
+ if (!match) return null;
7566
+ const parsed = Number(match[1]);
7567
+ return Number.isFinite(parsed) ? parsed : null;
7568
+ }
7569
+ function isLoopbackBaseUrl(baseUrl) {
7570
+ if (!baseUrl) return false;
7571
+ try {
7572
+ const url = new URL(baseUrl);
7573
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
7574
+ } catch {
7575
+ return false;
7576
+ }
7577
+ }
7578
+ function resolveAgentToolProfile(config) {
7579
+ const providerId = config.id;
7580
+ const isLocalProvider = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
7581
+ if (!isLocalProvider) return "default";
7582
+ const sizeInBillions = parseModelSizeInBillions(config.model);
7583
+ if (sizeInBillions === null) {
7584
+ return "compact";
7585
+ }
7586
+ return sizeInBillions <= 14 ? "compact" : "default";
7587
+ }
7588
+ const MAX_CONTEXT_CONTENT_LENGTH = 6e4;
7589
+ const MAX_MCP_NAV_CONTENT_LENGTH = 3e4;
7590
+ const MAX_AGENT_DEBUG_CONTENT_LENGTH = 2e4;
7591
+ const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
7592
+ const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
7462
7593
  const SAFE_TOOL_ALIASES = {
7463
7594
  goto_url: "navigate",
7464
7595
  go_to_url: "navigate",
@@ -7523,682 +7654,657 @@ function normalizeToolAlias(name) {
7523
7654
  }
7524
7655
  return name;
7525
7656
  }
7526
- function parseModelSizeInBillions(model) {
7527
- const match = model.toLowerCase().match(/(?:^|[:/_\-\s])(\d+(?:\.\d+)?)b(?:$|[:/_\-\s])/i);
7528
- if (!match) return null;
7529
- const parsed = Number(match[1]);
7530
- return Number.isFinite(parsed) ? parsed : null;
7657
+ function stableToolSignature(name, args) {
7658
+ const canonicalArgs = canonicalizeArgsForTool(name, args);
7659
+ const sortedEntries = Object.entries(canonicalArgs).sort(
7660
+ ([left], [right]) => left.localeCompare(right)
7661
+ );
7662
+ return JSON.stringify([name, sortedEntries]);
7531
7663
  }
7532
- function isLoopbackBaseUrl(baseUrl) {
7533
- if (!baseUrl) return false;
7664
+ function normalizeToolToken(value) {
7665
+ return value.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
7666
+ }
7667
+ function canonicalizeUrlLike(value) {
7534
7668
  try {
7535
- const url = new URL(baseUrl);
7536
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
7669
+ const url = new URL(value.trim());
7670
+ if (url.protocol === "http:" || url.protocol === "https:") {
7671
+ url.hostname = url.hostname.replace(/^www\./, "");
7672
+ url.hash = "";
7673
+ if (url.pathname.endsWith("/") && url.pathname !== "/") {
7674
+ url.pathname = url.pathname.replace(/\/+$/, "");
7675
+ }
7676
+ return url.toString();
7677
+ }
7537
7678
  } catch {
7538
- return false;
7539
7679
  }
7680
+ return value.trim();
7540
7681
  }
7541
- function resolveAgentToolProfile(config) {
7542
- const providerId = config.id;
7543
- const isLocalProvider = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
7544
- if (!isLocalProvider) return "default";
7545
- const sizeInBillions = parseModelSizeInBillions(config.model);
7546
- if (sizeInBillions === null) {
7547
- return "compact";
7682
+ function toLikelyUrl(value) {
7683
+ const trimmed = value.trim().replace(/^["']|["']$/g, "");
7684
+ if (!trimmed) return null;
7685
+ if (/^https?:\/\//i.test(trimmed)) return trimmed;
7686
+ if (/^[a-z0-9-]+\.(com|org|net|io|dev|app|ai|co|edu|gov)(\/\S*)?$/i.test(trimmed)) {
7687
+ return `https://${trimmed}`;
7548
7688
  }
7549
- return sizeInBillions <= 14 ? "compact" : "default";
7550
- }
7551
- const MAX_CONTEXT_CONTENT_LENGTH = 6e4;
7552
- const MAX_MCP_NAV_CONTENT_LENGTH = 3e4;
7553
- const MAX_AGENT_DEBUG_CONTENT_LENGTH = 2e4;
7554
- const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
7555
- const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
7556
- const logger$j = createLogger("OpenAIProvider");
7557
- function shouldDebugAgentLoop() {
7558
- const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
7559
- return value === "1" || value === "true";
7560
- }
7561
- function previewDebugValue(value, maxLength = 800) {
7562
- const normalized = value.replace(/\s+/g, " ").trim();
7563
- if (normalized.length <= maxLength) return normalized;
7564
- return `${normalized.slice(0, maxLength)}…`;
7565
- }
7566
- function previewToolDebugContent(content) {
7567
- return previewDebugValue(content, 500);
7568
- }
7569
- function toOpenAITools(tools) {
7570
- return tools.map((t) => ({
7571
- type: "function",
7572
- function: {
7573
- name: t.name,
7574
- description: t.description ?? "",
7575
- parameters: t.input_schema
7576
- }
7577
- }));
7578
- }
7579
- function agentTemperatureForProfile(profile) {
7580
- return profile === "compact" ? 0.2 : void 0;
7581
- }
7582
- function modelLikelySupportsOpenAIReasoningEffort(model) {
7583
- return /^(?:o\d|o[1-9]|gpt-5|codex|computer-use)/i.test(model.trim());
7689
+ return null;
7584
7690
  }
7585
- function toOpenAIReasoningEffort(effort, providerId, model) {
7586
- const supportsReasoningParam = providerId === "openrouter" || providerId === "custom" || providerId === "openai" && modelLikelySupportsOpenAIReasoningEffort(model);
7587
- if (!supportsReasoningParam) return void 0;
7588
- switch (effort) {
7589
- case "off":
7590
- if (providerId === "openai" && !/^gpt-5\.1/i.test(model.trim())) {
7591
- return void 0;
7691
+ function scalarArgsForTool(name, scalar) {
7692
+ const trimmed = scalar.trim();
7693
+ if (!trimmed) return null;
7694
+ if (name === "navigate") {
7695
+ const url = toLikelyUrl(trimmed);
7696
+ return url ? { url } : null;
7697
+ }
7698
+ if (name === "search") {
7699
+ return { query: trimmed.replace(/^["']|["']$/g, "") };
7700
+ }
7701
+ if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
7702
+ return { text: trimmed.replace(/^["']|["']$/g, "") };
7703
+ }
7704
+ if (name === "read_page") {
7705
+ const mode = trimmed.replace(/^["']|["']$/g, "").toLowerCase();
7706
+ if (mode) return { mode };
7707
+ }
7708
+ if (name === "save_bookmark") {
7709
+ const url = toLikelyUrl(trimmed);
7710
+ if (url) return { url };
7711
+ const lastSpace = trimmed.lastIndexOf(" ");
7712
+ if (lastSpace > 0) {
7713
+ const maybeUrl = toLikelyUrl(trimmed.slice(lastSpace + 1));
7714
+ if (maybeUrl) {
7715
+ return {
7716
+ url: maybeUrl,
7717
+ title: trimmed.slice(0, lastSpace).replace(/^["']|["']$/g, "")
7718
+ };
7592
7719
  }
7593
- return "none";
7594
- case "low":
7595
- return "low";
7596
- case "medium":
7597
- return "medium";
7598
- case "high":
7599
- return "high";
7600
- case "max":
7601
- return "xhigh";
7602
- default:
7603
- return void 0;
7720
+ }
7604
7721
  }
7722
+ return null;
7605
7723
  }
7606
- function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
7607
- if (profile !== "compact") return null;
7608
- const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
7609
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
7610
- return {
7611
- role: "user",
7612
- content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
7613
- Do not ask the user what they want next unless the request is genuinely ambiguous or blocked. After navigation or page reads, keep executing the same task.` + (stateReminder ? `
7614
- ${stateReminder}` : "") + (phaseReminder ? `
7615
- ${phaseReminder}` : "")
7616
- };
7617
- }
7618
- function extractSingleGoalDomain(goal) {
7619
- const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
7620
- if (!matches || matches.length !== 1) return null;
7621
- return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
7724
+ function firstStringArg(args, keys) {
7725
+ for (const key2 of keys) {
7726
+ const value = args[key2];
7727
+ if (typeof value === "string" && value.trim()) {
7728
+ return value.trim();
7729
+ }
7730
+ }
7731
+ return null;
7622
7732
  }
7623
- function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
7624
- const phaseReminder = buildPhaseReminder(userMessage, assistantText);
7625
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
7626
- const goalDomain = extractSingleGoalDomain(userMessage);
7627
- const latest = (latestToolResultPreview || "").toLowerCase();
7628
- const assistant = assistantText.toLowerCase();
7629
- const alreadyOnGoalSite = !!goalDomain && (latest.includes(goalDomain) || assistant.includes(`https://${goalDomain}`) || assistant.includes(`https://www.${goalDomain}`));
7630
- const lines = [
7631
- `The task is still in progress: ${userMessage}`,
7632
- `Do not ask the user for permission to continue. Choose the next tool now unless the request is fully complete.`
7633
- ];
7634
- if (alreadyOnGoalSite) {
7635
- lines.push(
7636
- `You are already on the requested site (${goalDomain}). Do not navigate to the homepage again and do not restart discovery from scratch.`
7637
- );
7733
+ function normalizeElementTargetArgs(args) {
7734
+ const normalized = { ...args };
7735
+ if (typeof normalized.index === "string" && /^\d+$/.test(normalized.index.trim())) {
7736
+ normalized.index = Number(normalized.index.trim());
7638
7737
  }
7639
- if (stateReminder) {
7640
- lines.push(stateReminder);
7738
+ if (typeof normalized.selector !== "string" || !normalized.selector.trim()) {
7739
+ const selector = firstStringArg(normalized, [
7740
+ "cssSelector",
7741
+ "css_selector",
7742
+ "querySelector",
7743
+ "query_selector"
7744
+ ]);
7745
+ if (selector) normalized.selector = selector;
7641
7746
  }
7642
- if (phaseReminder) {
7643
- lines.push(phaseReminder);
7747
+ if (typeof normalized.text !== "string" || !normalized.text.trim()) {
7748
+ const text = firstStringArg(normalized, [
7749
+ "label",
7750
+ "title",
7751
+ "name",
7752
+ "target",
7753
+ "element",
7754
+ "linkText",
7755
+ "link_text",
7756
+ "ariaLabel",
7757
+ "aria_label"
7758
+ ]);
7759
+ if (text) normalized.text = text;
7644
7760
  }
7645
- return lines.join("\n");
7761
+ return normalized;
7646
7762
  }
7647
- function buildPhaseReminder(userMessage, assistantText) {
7648
- const goal = userMessage.toLowerCase();
7649
- const text = assistantText.toLowerCase();
7650
- if (!goal || !text) return "";
7651
- const wantsCart = /\b(cart|bag|basket|checkout)\b/.test(goal);
7652
- const wantsExplanation = /\b(explain|reason|why)\b/.test(goal);
7653
- const wantsBookRecommendations = /\b(book|books|recommend|recommended|interesting|novel|fiction|nonfiction)\b/.test(
7654
- goal
7655
- );
7656
- const hasFiveItemList = /(?:^|\n)\s*1\./.test(assistantText) && /(?:^|\n)\s*2\./.test(assistantText) && /(?:^|\n)\s*3\./.test(assistantText) && /(?:^|\n)\s*4\./.test(assistantText) && /(?:^|\n)\s*5\./.test(assistantText);
7657
- const selectedItems = hasFiveItemList || /i(?:'| a)?ve chosen/.test(text) || /i have chosen/.test(text) || /i selected/.test(text) || /here are the books/i.test(assistantText) || /here are the items/i.test(assistantText);
7658
- const intendsCart = /next[, ]+i will add/.test(text) || /i(?:'| a)?ll start with the first/.test(text) || /proceed systematically/.test(text) || /add (these|the chosen|the selected).*(cart|bag|basket)/.test(text);
7659
- const cartDone = /(added to cart|added them to the cart|cart confirmation|view cart|checkout)/.test(
7660
- text
7661
- );
7662
- const explanationDone = /here is why i chose/.test(text) || /here are my reasons/.test(text) || /reason:/.test(text) || /reasons:/.test(text) || /why i chose/.test(text);
7663
- const listingLoopSignals = /page contains a list of books|book listings|book cards|visible book|load more results|scroll further|scroll down|inspect the visible|focus on the book listings|targeting the book images|limited to interactive elements|identify the book cards|click one of the visible book/.test(
7664
- text
7665
- );
7666
- const missedResultsSignals = /visible_only mode did not return specific book titles|did not yield a book title link|did not yield specific book titles|navigation links rather than book titles|inspect elements did not yield|inspect the page to find a specific book title|inspect the page to locate a book title|book title link from the search results/.test(
7667
- text
7668
- );
7669
- const falseCartSuccessSignals = /added\s+to\s+the\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+to\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+["“”'a-z0-9 ,:&-]+\s+to the cart|added\s+["“”'a-z0-9 ,:&-]+\s+to cart|added\s+.*\s+by\s+.*\s+to the cart/.test(
7670
- text
7671
- ) && !/(cart confirmation|view cart|shopping cart|checkout|continue shopping)/.test(
7672
- text
7673
- );
7674
- const skippedSingleResultSignals = /did not yield a direct match|no direct match|no matches|unavailable on powell|out of stock or unavailable/.test(
7675
- text
7676
- ) && /proceed to (?:add|search for) the next book|move on to the next book|next book from my list/.test(
7677
- text
7678
- );
7679
- const selectedItemsRestartSignals = /navigate back to the search results page|search for ".*" directly in the search box|search for .* directly|page structure has shifted|refresh the page|restart search/.test(
7680
- text
7681
- );
7682
- const multiClickSelectionSignals = /i(?:'| a)?ll start by clicking on the following books|i will start by clicking on the following books|i will click on the following books|clicked on five different book titles|clicked on \d+ different book titles|clicking through the selected titles|click each of the selected titles/.test(
7683
- text
7684
- );
7685
- const staleSelectionSignals = /cannot locate the elements to click|page structure is not being reliably captured|specific titles failed|page may have changed|stale-index|no visible area|not visible/.test(
7686
- text
7687
- );
7688
- const intermediateCartDialogSignals = /(added to cart|has been added to the cart|cart confirmation)/.test(text) && /(continue shopping|search results page|return to the search results page|back button|go back)/.test(
7689
- text
7690
- ) && !/(all requested books are now in the cart|all 5 books are now in the cart|5 of 5 requested books are now in the cart)/.test(
7691
- text
7692
- );
7693
- if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && listingLoopSignals) {
7694
- return `Progress reminder: If product results or primary results are already visible, do not keep rereading or rescrolling the same listing page. Open one promising result now. On the detail page, add that item to the cart before returning for the next unseen result.`;
7695
- }
7696
- if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && missedResultsSignals) {
7697
- return `Progress reminder: On a results page, do not use visible_only or generic inspect_element to hunt product results. Call read_page(mode="results_only") once. If Primary Results are shown, click a listed result directly.`;
7763
+ function hasElementTarget(args) {
7764
+ return typeof args.index === "number" || typeof args.selector === "string" && args.selector.trim().length > 0 || typeof args.text === "string" && args.text.trim().length > 0;
7765
+ }
7766
+ function isTargetlessClickArgs(args) {
7767
+ return !hasElementTarget(normalizeElementTargetArgs(args));
7768
+ }
7769
+ function tryParseJsonWithCommonRepairs(raw) {
7770
+ const trimmed = raw.trim();
7771
+ if (!trimmed) return {};
7772
+ const candidates = /* @__PURE__ */ new Set([trimmed]);
7773
+ const objectMatch = trimmed.match(/\{[\s\S]*\}/);
7774
+ if (objectMatch?.[0]) candidates.add(objectMatch[0]);
7775
+ if (!trimmed.startsWith("{") && trimmed.includes(":")) {
7776
+ candidates.add(`{${trimmed}}`);
7698
7777
  }
7699
- if (wantsCart && falseCartSuccessSignals && !selectedItems && !explanationDone) {
7700
- return `Progress reminder: Do not assume an item was added just because its product page is open or you inspected it. Only treat the cart step as complete after a successful Add to Cart click followed by cart confirmation, View Cart, Continue Shopping, or the cart page itself.`;
7778
+ for (const candidate of candidates) {
7779
+ const normalized = candidate.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
7780
+ if (!normalized) continue;
7781
+ const repaired = normalized.replace(/([{,]\s*)([A-Za-z_][A-Za-z0-9_-]*)(\s*:)/g, '$1"$2"$3').replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3').replace(
7782
+ /:\s*'([^'\\]*(?:\\.[^'\\]*)*)'/g,
7783
+ (_match, value) => `: ${JSON.stringify(value)}`
7784
+ ).replace(/,\s*([}\]])/g, "$1");
7785
+ try {
7786
+ return JSON.parse(repaired);
7787
+ } catch {
7788
+ }
7701
7789
  }
7702
- if (wantsCart && skippedSingleResultSignals && !selectedItems) {
7703
- return `Progress reminder: Do not skip to a new query just because the match is not exact. If the results page shows even one plausible product result, inspect or click that result before concluding there is no match.`;
7790
+ throw new Error("invalid-json");
7791
+ }
7792
+ function parseToolArgsWithRepair(name, argsJson) {
7793
+ const trimmed = (argsJson || "").trim();
7794
+ if (!trimmed) return { args: {}, repaired: false };
7795
+ try {
7796
+ const parsed = JSON.parse(trimmed);
7797
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
7798
+ return { args: parsed, repaired: false };
7799
+ }
7800
+ if (typeof parsed === "string") {
7801
+ const scalarArgs2 = scalarArgsForTool(name, parsed);
7802
+ return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
7803
+ }
7804
+ return null;
7805
+ } catch {
7704
7806
  }
7705
- if (wantsCart && intermediateCartDialogSignals && !explanationDone) {
7706
- return `Progress reminder: After an Add to Cart success, prefer the cart-confirmation dialog action Continue Shopping while more items remain. Do not click View Cart or Go to Basket yet, and do not use the browser back button while the dialog is still open.`;
7807
+ try {
7808
+ const repaired = tryParseJsonWithCommonRepairs(trimmed);
7809
+ if (repaired && typeof repaired === "object" && !Array.isArray(repaired)) {
7810
+ return { args: repaired, repaired: true };
7811
+ }
7812
+ if (typeof repaired === "string") {
7813
+ const scalarArgs2 = scalarArgsForTool(name, repaired);
7814
+ return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
7815
+ }
7816
+ } catch {
7707
7817
  }
7708
- if (wantsCart && selectedItems && !cartDone && selectedItemsRestartSignals) {
7709
- return `Progress reminder: The chosen items are already decided. Do not restart search, refresh the results page, or navigate back to browse again unless a specific saved link fails. Use the current results page or the chosen result links you already have: open one chosen result, add it to the cart, confirm success, then continue to the next chosen result.`;
7818
+ const scalarArgs = scalarArgsForTool(name, trimmed);
7819
+ return scalarArgs ? { args: scalarArgs, repaired: true } : null;
7820
+ }
7821
+ function coerceToolArgsForExecution(name, args) {
7822
+ let coerced = { ...args };
7823
+ if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
7824
+ coerced = normalizeElementTargetArgs(coerced);
7710
7825
  }
7711
- if (wantsCart && wantsBookRecommendations && !cartDone && (multiClickSelectionSignals || staleSelectionSignals)) {
7712
- return `Progress reminder: Do not batch-click multiple results from a listing or category page. Open exactly one visible result, finish that item's Add to Cart flow, confirm success, then use Continue Shopping or go back once to choose the next unseen result. If a remembered label or index fails, trust the latest page state and refresh it with one read_page call before continuing.`;
7826
+ if (name === "search") {
7827
+ if (typeof coerced.query !== "string" || !coerced.query.trim()) {
7828
+ if (typeof coerced.text === "string" && coerced.text.trim()) {
7829
+ coerced.query = coerced.text.trim();
7830
+ } else if (typeof coerced.term === "string" && coerced.term.trim()) {
7831
+ coerced.query = coerced.term.trim();
7832
+ }
7833
+ }
7713
7834
  }
7714
- if (wantsCart && selectedItems && (intendsCart || !cartDone)) {
7715
- return `Progress reminder: You already selected the requested items. Do not restart browsing or searching unless a specific cart step fails. Continue adding the selected items to the cart one by one. Use the chosen result links you already have, add one selected item to the cart, confirm success, then continue to the next one. Do not click multiple chosen results in a row from the same listing page.`;
7835
+ if (name === "navigate") {
7836
+ if (typeof coerced.url !== "string" || !coerced.url.trim()) {
7837
+ if (typeof coerced.href === "string" && coerced.href.trim()) {
7838
+ coerced.url = coerced.href.trim();
7839
+ } else if (typeof coerced.link === "string" && coerced.link.trim()) {
7840
+ coerced.url = coerced.link.trim();
7841
+ } else if (typeof coerced.text === "string" && /^https?:\/\//i.test(coerced.text.trim())) {
7842
+ coerced.url = coerced.text.trim();
7843
+ }
7844
+ }
7716
7845
  }
7717
- if (wantsCart && wantsExplanation && cartDone && !explanationDone) {
7718
- return `Progress reminder: The cart step appears complete. Do not resume browsing. Finish by explaining why the chosen items were recommended.`;
7846
+ if (name === "save_bookmark") {
7847
+ if (typeof coerced.url !== "string" || !coerced.url.trim()) {
7848
+ if (typeof coerced.link === "string" && coerced.link.trim()) {
7849
+ coerced.url = coerced.link.trim();
7850
+ } else if (typeof coerced.href === "string" && coerced.href.trim()) {
7851
+ coerced.url = coerced.href.trim();
7852
+ }
7853
+ }
7854
+ if (typeof coerced.folderName !== "string" || !coerced.folderName.trim()) {
7855
+ if (typeof coerced.folder === "string" && coerced.folder.trim()) {
7856
+ coerced.folderName = coerced.folder.trim();
7857
+ } else if (typeof coerced.category === "string" && coerced.category.trim()) {
7858
+ coerced.folderName = coerced.category.trim();
7859
+ }
7860
+ }
7861
+ if (coerced.folderName && typeof coerced.createFolderIfMissing === "undefined") {
7862
+ coerced.createFolderIfMissing = true;
7863
+ }
7719
7864
  }
7720
- return "";
7865
+ return coerced;
7721
7866
  }
7722
- function buildLatestStateReminder(toolResultPreview) {
7723
- const text = toolResultPreview.trim();
7724
- if (!text) return "";
7725
- const stateMatch = text.match(
7726
- /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
7727
- );
7728
- if (stateMatch) {
7729
- const url = stateMatch[1]?.trim();
7730
- const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
7731
- if (url) {
7732
- return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
7733
- }
7867
+ function canonicalizeArgsForTool(name, args) {
7868
+ const canonical = coerceToolArgsForExecution(name, args);
7869
+ if (typeof canonical.url === "string") {
7870
+ canonical.url = canonicalizeUrlLike(canonical.url);
7734
7871
  }
7735
- const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
7736
- const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
7737
- if (structuredUrl) {
7738
- return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
7872
+ if (typeof canonical.query === "string") {
7873
+ canonical.query = canonical.query.trim().replace(/\s+/g, " ").toLowerCase();
7874
+ delete canonical.text;
7739
7875
  }
7740
- const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i)?.[1]?.trim();
7741
- const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
7742
- if (navigatedUrl) {
7743
- return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
7876
+ if (typeof canonical.text === "string") {
7877
+ canonical.text = canonical.text.trim().replace(/\s+/g, " ");
7744
7878
  }
7745
- return "";
7879
+ return canonical;
7746
7880
  }
7747
- function shouldRecoverCompactStall(text, userMessage) {
7748
- const trimmed = text.trim().toLowerCase();
7749
- if (!trimmed) return true;
7750
- if (trimmed.length <= 160 && trimmed.includes("?")) return true;
7751
- if (userMessage && buildPhaseReminder(userMessage, text)) {
7752
- return true;
7753
- }
7754
- const repetitivePlanningSignals = [
7755
- "next step:",
7756
- "i will now inspect",
7757
- "i will now read",
7758
- "i will now click",
7759
- "i'll use readpage",
7760
- "i'll use read_page",
7761
- "i'll start by clicking",
7762
- "i have clicked on five different book titles",
7763
- "clicked on five different book titles",
7764
- "i'll begin with",
7765
- "if the selection is unclear"
7881
+ function unsupportedToolHint(name) {
7882
+ const normalized = name.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
7883
+ const BOOKMARK_NAMES = [
7884
+ "organize_bookmark",
7885
+ "organize_bookmarks",
7886
+ "manage_bookmark",
7887
+ "manage_bookmarks",
7888
+ "add_to_bookmarks",
7889
+ "save_to_bookmarks",
7890
+ "bookmark_link",
7891
+ "save_link",
7892
+ "store_bookmark"
7766
7893
  ];
7767
- if (repetitivePlanningSignals.some((pattern) => trimmed.includes(pattern))) {
7768
- return true;
7894
+ if (BOOKMARK_NAMES.includes(normalized) || /bookmark|save.*link|organize/.test(normalized)) {
7895
+ 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": "..."}`;
7769
7896
  }
7770
- const falseCartSuccessWithoutConfirmation = /added\s+to\s+the\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+to\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+["“”'a-z0-9 ,:&-]+\s+to the cart|added\s+["“”'a-z0-9 ,:&-]+\s+to cart|added\s+.*\s+by\s+.*\s+to the cart/.test(
7771
- trimmed
7772
- ) && !/(cart confirmation|view cart|continue shopping|shopping cart|checkout|why i chose|here is why i chose|here are my reasons)/.test(
7773
- trimmed
7774
- );
7775
- if (falseCartSuccessWithoutConfirmation) {
7776
- return true;
7897
+ return `Error: ${name} is not a supported tool. Choose one of the available browser tools instead.`;
7898
+ }
7899
+ function resolveToolCallName(rawName, args, availableToolNames) {
7900
+ const aliased = normalizeToolAlias(rawName);
7901
+ if (availableToolNames.has(aliased)) return aliased;
7902
+ const normalized = normalizeToolToken(rawName);
7903
+ if (availableToolNames.has(normalized)) return normalized;
7904
+ const hasUrl = typeof args.url === "string" && args.url.trim().length > 0;
7905
+ if (availableToolNames.has("navigate") && (hasUrl || /goto|navigate|open|visit|browser|url|link/.test(normalized))) {
7906
+ return "navigate";
7777
7907
  }
7778
- const completionSignals = [
7779
- "i found",
7780
- "i chose",
7781
- "i selected",
7782
- "i added",
7783
- "here are",
7784
- "these are",
7785
- "recommendations",
7786
- "reasoning",
7787
- "why i chose",
7788
- "added them to the cart"
7789
- ];
7790
- if (completionSignals.some((pattern) => trimmed.includes(pattern))) {
7791
- return false;
7908
+ if (availableToolNames.has("search") && (/search|find|lookup|query/.test(normalized) || normalized === "google" || normalized.startsWith("google_"))) {
7909
+ return "search";
7792
7910
  }
7793
- return [
7794
- "what are you hoping",
7795
- "what would you like",
7796
- "how can i help",
7797
- "let me know",
7798
- "are you looking for",
7799
- "just browsing",
7800
- "i need to",
7801
- "i will",
7802
- "i'll",
7803
- "since i cannot see",
7804
- "since i can't see",
7805
- "cannot see the current page",
7806
- "scroll down to",
7807
- "load more results",
7808
- "as placeholders",
7809
- "would you like me to proceed",
7810
- "action:",
7811
- "one moment",
7812
- "i will now navigate",
7813
- "navigating to ",
7814
- "this will take me",
7815
- "i will use the browser"
7816
- ].some((pattern) => trimmed.includes(pattern));
7817
- }
7818
- function shouldRetryCompactToolLoop(profile, text, hasToolHistory, userMessage) {
7819
- return profile === "compact" && hasToolHistory && shouldRecoverCompactStall(text, userMessage);
7911
+ if (availableToolNames.has("scroll") && /scroll|page_?down|page_?up/.test(normalized)) {
7912
+ return "scroll";
7913
+ }
7914
+ if (availableToolNames.has("read_page") && /read|scan|inspect|analy[sz]e|summari[sz]e/.test(normalized)) {
7915
+ return "read_page";
7916
+ }
7917
+ return aliased;
7820
7918
  }
7821
- function stableToolSignature(name, args) {
7822
- const canonicalArgs = canonicalizeArgsForTool(name, args);
7823
- const sortedEntries = Object.entries(canonicalArgs).sort(
7824
- ([left], [right]) => left.localeCompare(right)
7919
+ function recoverTextEncodedToolCalls(text, availableToolNames) {
7920
+ const trimmed = text.trim();
7921
+ if (!trimmed) return [];
7922
+ const candidates = trimmed.match(
7923
+ /([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*?\})(?=\s*$|\n{2,}|[A-Za-z0-9._ -]+\s*\[ARGS\])/g
7825
7924
  );
7826
- return JSON.stringify([name, sortedEntries]);
7827
- }
7828
- function normalizeToolToken(value) {
7829
- return value.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
7830
- }
7831
- function canonicalizeUrlLike(value) {
7832
- try {
7833
- const url = new URL(value.trim());
7834
- if (url.protocol === "http:" || url.protocol === "https:") {
7835
- url.hostname = url.hostname.replace(/^www\./, "");
7836
- url.hash = "";
7837
- if (url.pathname.endsWith("/") && url.pathname !== "/") {
7838
- url.pathname = url.pathname.replace(/\/+$/, "");
7925
+ if (!candidates || candidates.length === 0) return [];
7926
+ const recovered = [];
7927
+ for (const candidate of candidates) {
7928
+ const match = candidate.match(
7929
+ /^\s*([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*\})\s*$/
7930
+ );
7931
+ if (!match) continue;
7932
+ const rawName = match[1] ?? "";
7933
+ const argsJson = match[2] ?? "{}";
7934
+ let parsedArgs = {};
7935
+ try {
7936
+ const raw = JSON.parse(argsJson);
7937
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
7938
+ parsedArgs = raw;
7839
7939
  }
7840
- return url.toString();
7940
+ } catch {
7941
+ continue;
7841
7942
  }
7842
- } catch {
7843
- }
7844
- return value.trim();
7845
- }
7846
- function toLikelyUrl(value) {
7847
- const trimmed = value.trim().replace(/^["']|["']$/g, "");
7848
- if (!trimmed) return null;
7849
- if (/^https?:\/\//i.test(trimmed)) return trimmed;
7850
- if (/^[a-z0-9-]+\.(com|org|net|io|dev|app|ai|co|edu|gov)(\/\S*)?$/i.test(trimmed)) {
7851
- return `https://${trimmed}`;
7943
+ const resolvedName = resolveToolCallName(
7944
+ rawName,
7945
+ parsedArgs,
7946
+ availableToolNames
7947
+ );
7948
+ recovered.push({
7949
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
7950
+ name: resolvedName,
7951
+ argsJson
7952
+ });
7852
7953
  }
7853
- return null;
7954
+ return recovered;
7854
7955
  }
7855
- function scalarArgsForTool(name, scalar) {
7856
- const trimmed = scalar.trim();
7857
- if (!trimmed) return null;
7858
- if (name === "navigate") {
7859
- const url = toLikelyUrl(trimmed);
7860
- return url ? { url } : null;
7861
- }
7862
- if (name === "search") {
7863
- return { query: trimmed.replace(/^["']|["']$/g, "") };
7864
- }
7865
- if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
7866
- return { text: trimmed.replace(/^["']|["']$/g, "") };
7867
- }
7868
- if (name === "read_page") {
7869
- const mode = trimmed.replace(/^["']|["']$/g, "").toLowerCase();
7870
- if (mode) return { mode };
7871
- }
7872
- if (name === "save_bookmark") {
7873
- const url = toLikelyUrl(trimmed);
7874
- if (url) return { url };
7875
- const lastSpace = trimmed.lastIndexOf(" ");
7876
- if (lastSpace > 0) {
7877
- const maybeUrl = toLikelyUrl(trimmed.slice(lastSpace + 1));
7878
- if (maybeUrl) return { url: maybeUrl, title: trimmed.slice(0, lastSpace).replace(/^["']|["']$/g, "") };
7956
+ function recoverNarratedActionToolCalls(text, availableToolNames) {
7957
+ const trimmed = text.trim();
7958
+ if (!trimmed) return [];
7959
+ const recovered = [];
7960
+ const actionLines = trimmed.match(/^action:\s+.+$/gim) ?? [];
7961
+ for (const rawLine of actionLines) {
7962
+ const line = rawLine.replace(/^action:\s*/i, "").trim();
7963
+ if (!line) continue;
7964
+ const quotedValue = line.match(/"([^"]+)"/)?.[1]?.trim() ?? line.match(/'([^']+)'/)?.[1]?.trim() ?? "";
7965
+ const navigateMatch = line.match(
7966
+ /\b(?:navigate|open|go)\b(?:\s+(?:to|the url))?\s+(https?:\/\/[^\s)]+)\.?/i
7967
+ );
7968
+ if (navigateMatch?.[1]) {
7969
+ const argsJson = JSON.stringify({ url: navigateMatch[1].replace(/\.$/, "") });
7970
+ recovered.push({
7971
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
7972
+ name: resolveToolCallName("navigate", { url: navigateMatch[1] }, availableToolNames),
7973
+ argsJson
7974
+ });
7975
+ continue;
7879
7976
  }
7880
- }
7881
- return null;
7882
- }
7883
- function firstStringArg(args, keys) {
7884
- for (const key2 of keys) {
7885
- const value = args[key2];
7886
- if (typeof value === "string" && value.trim()) {
7887
- return value.trim();
7977
+ const isSearchAction = /\bsearch\b/i.test(line) || /\btype\b/i.test(line) && /\bsearch box\b/i.test(line);
7978
+ if (isSearchAction && quotedValue && availableToolNames.has("search")) {
7979
+ recovered.push({
7980
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
7981
+ name: "search",
7982
+ argsJson: JSON.stringify({ query: quotedValue })
7983
+ });
7984
+ continue;
7985
+ }
7986
+ if (/\b(?:read|scan)\b.*\bpage\b/i.test(line) && availableToolNames.has("read_page")) {
7987
+ recovered.push({
7988
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
7989
+ name: "read_page",
7990
+ argsJson: JSON.stringify({ mode: "visible_only" })
7991
+ });
7992
+ continue;
7993
+ }
7994
+ const toolRefMatch = line.match(
7995
+ /\b(?:use|call)\s+([a-z_][a-z0-9_]*)(?:\s+tool)?\b/i
7996
+ );
7997
+ if (toolRefMatch?.[1]) {
7998
+ const toolName = resolveToolCallName(toolRefMatch[1], {}, availableToolNames);
7999
+ recovered.push({
8000
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8001
+ name: toolName,
8002
+ argsJson: "{}"
8003
+ });
7888
8004
  }
7889
8005
  }
7890
- return null;
7891
- }
7892
- function normalizeElementTargetArgs(args) {
7893
- const normalized = { ...args };
7894
- if (typeof normalized.index === "string" && /^\d+$/.test(normalized.index.trim())) {
7895
- normalized.index = Number(normalized.index.trim());
7896
- }
7897
- if (typeof normalized.selector !== "string" || !normalized.selector.trim()) {
7898
- const selector = firstStringArg(normalized, [
7899
- "cssSelector",
7900
- "css_selector",
7901
- "querySelector",
7902
- "query_selector"
7903
- ]);
7904
- if (selector) normalized.selector = selector;
8006
+ const inlineReadMatch = trimmed.match(
8007
+ /\bread_?page\b\s*\(\s*mode\s*=\s*["']?([a-z_]+)["']?\s*\)/i
8008
+ ) ?? trimmed.match(
8009
+ /\breadpage\b\s*\(\s*mode\s*=\s*["']?([a-z_]+)["']?\s*\)/i
8010
+ );
8011
+ if (inlineReadMatch && availableToolNames.has("read_page")) {
8012
+ const rawMode = (inlineReadMatch[1] || "").trim().toLowerCase();
8013
+ const normalizedMode = rawMode === "visibleonly" ? "visible_only" : rawMode === "resultsonly" ? "results_only" : rawMode;
8014
+ if (normalizedMode) {
8015
+ recovered.push({
8016
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8017
+ name: "read_page",
8018
+ argsJson: JSON.stringify({ mode: normalizedMode })
8019
+ });
8020
+ return recovered;
8021
+ }
7905
8022
  }
7906
- if (typeof normalized.text !== "string" || !normalized.text.trim()) {
7907
- const text = firstStringArg(normalized, [
7908
- "label",
7909
- "title",
7910
- "name",
7911
- "target",
7912
- "element",
7913
- "linkText",
7914
- "link_text",
7915
- "ariaLabel",
7916
- "aria_label"
7917
- ]);
7918
- if (text) normalized.text = text;
8023
+ const inlineInspectMatch = trimmed.match(
8024
+ /\binspect_?element\b(?:\s+tool)?\b/i
8025
+ ) ?? trimmed.match(/\binspectelement\b\b/i);
8026
+ if (inlineInspectMatch && availableToolNames.has("inspect_element")) {
8027
+ recovered.push({
8028
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8029
+ name: "inspect_element",
8030
+ argsJson: "{}"
8031
+ });
8032
+ return recovered;
7919
8033
  }
7920
- return normalized;
8034
+ return recovered;
7921
8035
  }
7922
- function hasElementTarget(args) {
7923
- return typeof args.index === "number" || typeof args.selector === "string" && args.selector.trim().length > 0 || typeof args.text === "string" && args.text.trim().length > 0;
8036
+ const logger$l = createLogger("OpenAIProvider");
8037
+ function shouldDebugAgentLoop() {
8038
+ const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
8039
+ return value === "1" || value === "true";
7924
8040
  }
7925
- function isTargetlessClickArgs(args) {
7926
- return !hasElementTarget(normalizeElementTargetArgs(args));
8041
+ function previewDebugValue(value, maxLength = 800) {
8042
+ const normalized = value.replace(/\s+/g, " ").trim();
8043
+ if (normalized.length <= maxLength) return normalized;
8044
+ return `${normalized.slice(0, maxLength)}…`;
7927
8045
  }
7928
- function tryParseJsonWithCommonRepairs(raw) {
7929
- const trimmed = raw.trim();
7930
- if (!trimmed) return {};
7931
- const candidates = /* @__PURE__ */ new Set([trimmed]);
7932
- const objectMatch = trimmed.match(/\{[\s\S]*\}/);
7933
- if (objectMatch?.[0]) candidates.add(objectMatch[0]);
7934
- if (!trimmed.startsWith("{") && trimmed.includes(":")) {
7935
- candidates.add(`{${trimmed}}`);
7936
- }
7937
- for (const candidate of candidates) {
7938
- const normalized = candidate.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
7939
- if (!normalized) continue;
7940
- const repaired = normalized.replace(/([{,]\s*)([A-Za-z_][A-Za-z0-9_-]*)(\s*:)/g, '$1"$2"$3').replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3').replace(
7941
- /:\s*'([^'\\]*(?:\\.[^'\\]*)*)'/g,
7942
- (_match, value) => `: ${JSON.stringify(value)}`
7943
- ).replace(/,\s*([}\]])/g, "$1");
7944
- try {
7945
- return JSON.parse(repaired);
7946
- } catch {
7947
- }
7948
- }
7949
- throw new Error("invalid-json");
8046
+ function previewToolDebugContent(content) {
8047
+ return previewDebugValue(content, 500);
7950
8048
  }
7951
- function parseToolArgsWithRepair(name, argsJson) {
7952
- const trimmed = (argsJson || "").trim();
7953
- if (!trimmed) return { args: {}, repaired: false };
7954
- try {
7955
- const parsed = JSON.parse(trimmed);
7956
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
7957
- return { args: parsed, repaired: false };
7958
- }
7959
- if (typeof parsed === "string") {
7960
- const scalarArgs2 = scalarArgsForTool(name, parsed);
7961
- return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
7962
- }
7963
- return null;
7964
- } catch {
7965
- }
7966
- try {
7967
- const repaired = tryParseJsonWithCommonRepairs(trimmed);
7968
- if (repaired && typeof repaired === "object" && !Array.isArray(repaired)) {
7969
- return { args: repaired, repaired: true };
7970
- }
7971
- if (typeof repaired === "string") {
7972
- const scalarArgs2 = scalarArgsForTool(name, repaired);
7973
- return scalarArgs2 ? { args: scalarArgs2, repaired: true } : null;
8049
+ function toOpenAITools(tools) {
8050
+ return tools.map((t) => ({
8051
+ type: "function",
8052
+ function: {
8053
+ name: t.name,
8054
+ description: t.description ?? "",
8055
+ parameters: t.input_schema
7974
8056
  }
7975
- } catch {
7976
- }
7977
- const scalarArgs = scalarArgsForTool(name, trimmed);
7978
- return scalarArgs ? { args: scalarArgs, repaired: true } : null;
8057
+ }));
7979
8058
  }
7980
- function coerceToolArgsForExecution(name, args) {
7981
- let coerced = { ...args };
7982
- if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
7983
- coerced = normalizeElementTargetArgs(coerced);
7984
- }
7985
- if (name === "search") {
7986
- if (typeof coerced.query !== "string" || !coerced.query.trim()) {
7987
- if (typeof coerced.text === "string" && coerced.text.trim()) {
7988
- coerced.query = coerced.text.trim();
7989
- } else if (typeof coerced.term === "string" && coerced.term.trim()) {
7990
- coerced.query = coerced.term.trim();
8059
+ function agentTemperatureForProfile(profile) {
8060
+ return profile === "compact" ? 0.2 : void 0;
8061
+ }
8062
+ function modelLikelySupportsOpenAIReasoningEffort(model) {
8063
+ return /^(?:o\d|o[1-9]|gpt-5|codex|computer-use)/i.test(model.trim());
8064
+ }
8065
+ function toOpenAIReasoningEffort(effort, providerId, model) {
8066
+ const supportsReasoningParam = providerId === "openrouter" || providerId === "custom" || providerId === "openai" && modelLikelySupportsOpenAIReasoningEffort(model);
8067
+ if (!supportsReasoningParam) return void 0;
8068
+ switch (effort) {
8069
+ case "off":
8070
+ if (providerId === "openai" && !/^gpt-5\.1/i.test(model.trim())) {
8071
+ return void 0;
7991
8072
  }
7992
- }
8073
+ return "none";
8074
+ case "low":
8075
+ return "low";
8076
+ case "medium":
8077
+ return "medium";
8078
+ case "high":
8079
+ return "high";
8080
+ case "max":
8081
+ return "xhigh";
8082
+ default:
8083
+ return void 0;
7993
8084
  }
7994
- if (name === "navigate") {
7995
- if (typeof coerced.url !== "string" || !coerced.url.trim()) {
7996
- if (typeof coerced.href === "string" && coerced.href.trim()) {
7997
- coerced.url = coerced.href.trim();
7998
- } else if (typeof coerced.link === "string" && coerced.link.trim()) {
7999
- coerced.url = coerced.link.trim();
8000
- } else if (typeof coerced.text === "string" && /^https?:\/\//i.test(coerced.text.trim())) {
8001
- coerced.url = coerced.text.trim();
8002
- }
8003
- }
8085
+ }
8086
+ function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
8087
+ if (profile !== "compact") return null;
8088
+ const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
8089
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
8090
+ return {
8091
+ role: "user",
8092
+ content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
8093
+ Do not ask the user what they want next unless the request is genuinely ambiguous or blocked. After navigation or page reads, keep executing the same task.` + (stateReminder ? `
8094
+ ${stateReminder}` : "") + (phaseReminder ? `
8095
+ ${phaseReminder}` : "")
8096
+ };
8097
+ }
8098
+ function extractSingleGoalDomain(goal) {
8099
+ const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
8100
+ if (!matches || matches.length !== 1) return null;
8101
+ return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
8102
+ }
8103
+ function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
8104
+ const phaseReminder = buildPhaseReminder(userMessage, assistantText);
8105
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
8106
+ const goalDomain = extractSingleGoalDomain(userMessage);
8107
+ const latest = (latestToolResultPreview || "").toLowerCase();
8108
+ const assistant = assistantText.toLowerCase();
8109
+ const alreadyOnGoalSite = !!goalDomain && (latest.includes(goalDomain) || assistant.includes(`https://${goalDomain}`) || assistant.includes(`https://www.${goalDomain}`));
8110
+ const lines = [
8111
+ `The task is still in progress: ${userMessage}`,
8112
+ `Do not ask the user for permission to continue. Choose the next tool now unless the request is fully complete.`
8113
+ ];
8114
+ if (alreadyOnGoalSite) {
8115
+ lines.push(
8116
+ `You are already on the requested site (${goalDomain}). Do not navigate to the homepage again and do not restart discovery from scratch.`
8117
+ );
8004
8118
  }
8005
- if (name === "save_bookmark") {
8006
- if (typeof coerced.url !== "string" || !coerced.url.trim()) {
8007
- if (typeof coerced.link === "string" && coerced.link.trim()) {
8008
- coerced.url = coerced.link.trim();
8009
- } else if (typeof coerced.href === "string" && coerced.href.trim()) {
8010
- coerced.url = coerced.href.trim();
8011
- }
8012
- }
8013
- if (typeof coerced.folderName !== "string" || !coerced.folderName.trim()) {
8014
- if (typeof coerced.folder === "string" && coerced.folder.trim()) {
8015
- coerced.folderName = coerced.folder.trim();
8016
- } else if (typeof coerced.category === "string" && coerced.category.trim()) {
8017
- coerced.folderName = coerced.category.trim();
8018
- }
8019
- }
8020
- if (coerced.folderName && typeof coerced.createFolderIfMissing === "undefined") {
8021
- coerced.createFolderIfMissing = true;
8022
- }
8119
+ if (stateReminder) {
8120
+ lines.push(stateReminder);
8023
8121
  }
8024
- return coerced;
8122
+ if (phaseReminder) {
8123
+ lines.push(phaseReminder);
8124
+ }
8125
+ return lines.join("\n");
8025
8126
  }
8026
- function canonicalizeArgsForTool(name, args) {
8027
- const canonical = coerceToolArgsForExecution(name, args);
8028
- if (typeof canonical.url === "string") {
8029
- canonical.url = canonicalizeUrlLike(canonical.url);
8127
+ function buildPhaseReminder(userMessage, assistantText) {
8128
+ const goal = userMessage.toLowerCase();
8129
+ const text = assistantText.toLowerCase();
8130
+ if (!goal || !text) return "";
8131
+ const wantsCart = /\b(cart|bag|basket|checkout)\b/.test(goal);
8132
+ const wantsExplanation = /\b(explain|reason|why)\b/.test(goal);
8133
+ const wantsBookRecommendations = /\b(book|books|recommend|recommended|interesting|novel|fiction|nonfiction)\b/.test(
8134
+ goal
8135
+ );
8136
+ const hasFiveItemList = /(?:^|\n)\s*1\./.test(assistantText) && /(?:^|\n)\s*2\./.test(assistantText) && /(?:^|\n)\s*3\./.test(assistantText) && /(?:^|\n)\s*4\./.test(assistantText) && /(?:^|\n)\s*5\./.test(assistantText);
8137
+ const selectedItems = hasFiveItemList || /i(?:'| a)?ve chosen/.test(text) || /i have chosen/.test(text) || /i selected/.test(text) || /here are the books/i.test(assistantText) || /here are the items/i.test(assistantText);
8138
+ const intendsCart = /next[, ]+i will add/.test(text) || /i(?:'| a)?ll start with the first/.test(text) || /proceed systematically/.test(text) || /add (these|the chosen|the selected).*(cart|bag|basket)/.test(text);
8139
+ const cartDone = /(added to cart|added them to the cart|cart confirmation|view cart|checkout)/.test(
8140
+ text
8141
+ );
8142
+ const explanationDone = /here is why i chose/.test(text) || /here are my reasons/.test(text) || /reason:/.test(text) || /reasons:/.test(text) || /why i chose/.test(text);
8143
+ const listingLoopSignals = /page contains a list of books|book listings|book cards|visible book|load more results|scroll further|scroll down|inspect the visible|focus on the book listings|targeting the book images|limited to interactive elements|identify the book cards|click one of the visible book/.test(
8144
+ text
8145
+ );
8146
+ const missedResultsSignals = /visible_only mode did not return specific book titles|did not yield a book title link|did not yield specific book titles|navigation links rather than book titles|inspect elements did not yield|inspect the page to find a specific book title|inspect the page to locate a book title|book title link from the search results/.test(
8147
+ text
8148
+ );
8149
+ const falseCartSuccessSignals = /added\s+to\s+the\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+to\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+["“”'a-z0-9 ,:&-]+\s+to the cart|added\s+["“”'a-z0-9 ,:&-]+\s+to cart|added\s+.*\s+by\s+.*\s+to the cart/.test(
8150
+ text
8151
+ ) && !/(cart confirmation|view cart|shopping cart|checkout|continue shopping)/.test(
8152
+ text
8153
+ );
8154
+ const skippedSingleResultSignals = /did not yield a direct match|no direct match|no matches|unavailable on powell|out of stock or unavailable/.test(
8155
+ text
8156
+ ) && /proceed to (?:add|search for) the next book|move on to the next book|next book from my list/.test(
8157
+ text
8158
+ );
8159
+ const selectedItemsRestartSignals = /navigate back to the search results page|search for ".*" directly in the search box|search for .* directly|page structure has shifted|refresh the page|restart search/.test(
8160
+ text
8161
+ );
8162
+ const multiClickSelectionSignals = /i(?:'| a)?ll start by clicking on the following books|i will start by clicking on the following books|i will click on the following books|clicked on five different book titles|clicked on \d+ different book titles|clicking through the selected titles|click each of the selected titles/.test(
8163
+ text
8164
+ );
8165
+ const staleSelectionSignals = /cannot locate the elements to click|page structure is not being reliably captured|specific titles failed|page may have changed|stale-index|no visible area|not visible/.test(
8166
+ text
8167
+ );
8168
+ const intermediateCartDialogSignals = /(added to cart|has been added to the cart|cart confirmation)/.test(text) && /(continue shopping|search results page|return to the search results page|back button|go back)/.test(
8169
+ text
8170
+ ) && !/(all requested books are now in the cart|all 5 books are now in the cart|5 of 5 requested books are now in the cart)/.test(
8171
+ text
8172
+ );
8173
+ if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && listingLoopSignals) {
8174
+ return `Progress reminder: If product results or primary results are already visible, do not keep rereading or rescrolling the same listing page. Open one promising result now. On the detail page, add that item to the cart before returning for the next unseen result.`;
8030
8175
  }
8031
- if (typeof canonical.query === "string") {
8032
- canonical.query = canonical.query.trim().replace(/\s+/g, " ").toLowerCase();
8033
- delete canonical.text;
8176
+ if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && missedResultsSignals) {
8177
+ return `Progress reminder: On a results page, do not use visible_only or generic inspect_element to hunt product results. Call read_page(mode="results_only") once. If Primary Results are shown, click a listed result directly.`;
8034
8178
  }
8035
- if (typeof canonical.text === "string") {
8036
- canonical.text = canonical.text.trim().replace(/\s+/g, " ");
8179
+ if (wantsCart && falseCartSuccessSignals && !selectedItems && !explanationDone) {
8180
+ return `Progress reminder: Do not assume an item was added just because its product page is open or you inspected it. Only treat the cart step as complete after a successful Add to Cart click followed by cart confirmation, View Cart, Continue Shopping, or the cart page itself.`;
8037
8181
  }
8038
- return canonical;
8039
- }
8040
- function unsupportedToolHint(name) {
8041
- const normalized = name.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
8042
- const BOOKMARK_NAMES = [
8043
- "organize_bookmark",
8044
- "organize_bookmarks",
8045
- "manage_bookmark",
8046
- "manage_bookmarks",
8047
- "add_to_bookmarks",
8048
- "save_to_bookmarks",
8049
- "bookmark_link",
8050
- "save_link",
8051
- "store_bookmark"
8052
- ];
8053
- if (BOOKMARK_NAMES.includes(normalized) || /bookmark|save.*link|organize/.test(normalized)) {
8054
- 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": "..."}`;
8182
+ if (wantsCart && skippedSingleResultSignals && !selectedItems) {
8183
+ return `Progress reminder: Do not skip to a new query just because the match is not exact. If the results page shows even one plausible product result, inspect or click that result before concluding there is no match.`;
8055
8184
  }
8056
- return `Error: ${name} is not a supported tool. Choose one of the available browser tools instead.`;
8057
- }
8058
- function resolveToolCallName(rawName, args, availableToolNames) {
8059
- const aliased = normalizeToolAlias(rawName);
8060
- if (availableToolNames.has(aliased)) return aliased;
8061
- const normalized = normalizeToolToken(rawName);
8062
- if (availableToolNames.has(normalized)) return normalized;
8063
- const hasUrl = typeof args.url === "string" && args.url.trim().length > 0;
8064
- if (availableToolNames.has("navigate") && (hasUrl || /goto|navigate|open|visit|browser|url|link/.test(normalized))) {
8065
- return "navigate";
8185
+ if (wantsCart && intermediateCartDialogSignals && !explanationDone) {
8186
+ return `Progress reminder: After an Add to Cart success, prefer the cart-confirmation dialog action Continue Shopping while more items remain. Do not click View Cart or Go to Basket yet, and do not use the browser back button while the dialog is still open.`;
8066
8187
  }
8067
- if (availableToolNames.has("search") && (/search|find|lookup|query/.test(normalized) || normalized === "google" || normalized.startsWith("google_"))) {
8068
- return "search";
8188
+ if (wantsCart && selectedItems && !cartDone && selectedItemsRestartSignals) {
8189
+ return `Progress reminder: The chosen items are already decided. Do not restart search, refresh the results page, or navigate back to browse again unless a specific saved link fails. Use the current results page or the chosen result links you already have: open one chosen result, add it to the cart, confirm success, then continue to the next chosen result.`;
8069
8190
  }
8070
- if (availableToolNames.has("scroll") && /scroll|page_?down|page_?up/.test(normalized)) {
8071
- return "scroll";
8191
+ if (wantsCart && wantsBookRecommendations && !cartDone && (multiClickSelectionSignals || staleSelectionSignals)) {
8192
+ return `Progress reminder: Do not batch-click multiple results from a listing or category page. Open exactly one visible result, finish that item's Add to Cart flow, confirm success, then use Continue Shopping or go back once to choose the next unseen result. If a remembered label or index fails, trust the latest page state and refresh it with one read_page call before continuing.`;
8072
8193
  }
8073
- if (availableToolNames.has("read_page") && /read|scan|inspect|analy[sz]e|summari[sz]e/.test(normalized)) {
8074
- return "read_page";
8194
+ if (wantsCart && selectedItems && (intendsCart || !cartDone)) {
8195
+ return `Progress reminder: You already selected the requested items. Do not restart browsing or searching unless a specific cart step fails. Continue adding the selected items to the cart one by one. Use the chosen result links you already have, add one selected item to the cart, confirm success, then continue to the next one. Do not click multiple chosen results in a row from the same listing page.`;
8075
8196
  }
8076
- return aliased;
8077
- }
8078
- function logAgentLoopDebug(payload) {
8079
- if (!shouldDebugAgentLoop()) return;
8080
- try {
8081
- logger$j.info(`[agent-debug] ${JSON.stringify(payload)}`);
8082
- } catch (err) {
8083
- logger$j.warn("Failed to serialize debug payload:", err);
8197
+ if (wantsCart && wantsExplanation && cartDone && !explanationDone) {
8198
+ return `Progress reminder: The cart step appears complete. Do not resume browsing. Finish by explaining why the chosen items were recommended.`;
8084
8199
  }
8200
+ return "";
8085
8201
  }
8086
- function recoverTextEncodedToolCalls(text, availableToolNames) {
8087
- const trimmed = text.trim();
8088
- if (!trimmed) return [];
8089
- const candidates = trimmed.match(
8090
- /([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*?\})(?=\s*$|\n{2,}|[A-Za-z0-9._ -]+\s*\[ARGS\])/g
8202
+ function buildLatestStateReminder(toolResultPreview) {
8203
+ const text = toolResultPreview.trim();
8204
+ if (!text) return "";
8205
+ const stateMatch = text.match(
8206
+ /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
8091
8207
  );
8092
- if (!candidates || candidates.length === 0) return [];
8093
- const recovered = [];
8094
- for (const candidate of candidates) {
8095
- const match = candidate.match(
8096
- /^\s*([A-Za-z0-9._ -]+)\s*\[ARGS\]\s*(\{[\s\S]*\})\s*$/
8097
- );
8098
- if (!match) continue;
8099
- const rawName = match[1] ?? "";
8100
- const argsJson = match[2] ?? "{}";
8101
- let parsedArgs = {};
8102
- try {
8103
- const raw = JSON.parse(argsJson);
8104
- if (raw && typeof raw === "object" && !Array.isArray(raw)) {
8105
- parsedArgs = raw;
8106
- }
8107
- } catch {
8108
- continue;
8208
+ if (stateMatch) {
8209
+ const url = stateMatch[1]?.trim();
8210
+ const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
8211
+ if (url) {
8212
+ return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
8109
8213
  }
8110
- const resolvedName = resolveToolCallName(
8111
- rawName,
8112
- parsedArgs,
8113
- availableToolNames
8114
- );
8115
- recovered.push({
8116
- id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8117
- name: resolvedName,
8118
- argsJson
8119
- });
8120
8214
  }
8121
- return recovered;
8215
+ const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8216
+ const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8217
+ if (structuredUrl) {
8218
+ return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8219
+ }
8220
+ const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i)?.[1]?.trim();
8221
+ const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
8222
+ if (navigatedUrl) {
8223
+ return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8224
+ }
8225
+ return "";
8122
8226
  }
8123
- function recoverNarratedActionToolCalls(text, availableToolNames) {
8124
- const trimmed = text.trim();
8125
- if (!trimmed) return [];
8126
- const recovered = [];
8127
- const actionLines = trimmed.match(/^action:\s+.+$/gim) ?? [];
8128
- for (const rawLine of actionLines) {
8129
- const line = rawLine.replace(/^action:\s*/i, "").trim();
8130
- if (!line) continue;
8131
- const quotedValue = line.match(/"([^"]+)"/)?.[1]?.trim() ?? line.match(/'([^']+)'/)?.[1]?.trim() ?? "";
8132
- const navigateMatch = line.match(
8133
- /\b(?:navigate|open|go)\b(?:\s+(?:to|the url))?\s+(https?:\/\/[^\s)]+)\.?/i
8134
- );
8135
- if (navigateMatch?.[1]) {
8136
- const argsJson = JSON.stringify({ url: navigateMatch[1].replace(/\.$/, "") });
8137
- recovered.push({
8138
- id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8139
- name: resolveToolCallName("navigate", { url: navigateMatch[1] }, availableToolNames),
8140
- argsJson
8141
- });
8142
- continue;
8143
- }
8144
- const isSearchAction = /\bsearch\b/i.test(line) || /\btype\b/i.test(line) && /\bsearch box\b/i.test(line);
8145
- if (isSearchAction && quotedValue && availableToolNames.has("search")) {
8146
- recovered.push({
8147
- id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8148
- name: "search",
8149
- argsJson: JSON.stringify({ query: quotedValue })
8150
- });
8151
- continue;
8152
- }
8153
- if (/\b(?:read|scan)\b.*\bpage\b/i.test(line) && availableToolNames.has("read_page")) {
8154
- recovered.push({
8155
- id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8156
- name: "read_page",
8157
- argsJson: JSON.stringify({ mode: "visible_only" })
8158
- });
8159
- continue;
8160
- }
8161
- const toolRefMatch = line.match(
8162
- /\b(?:use|call)\s+([a-z_][a-z0-9_]*)(?:\s+tool)?\b/i
8163
- );
8164
- if (toolRefMatch?.[1]) {
8165
- const toolName = resolveToolCallName(toolRefMatch[1], {}, availableToolNames);
8166
- recovered.push({
8167
- id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8168
- name: toolName,
8169
- argsJson: "{}"
8170
- });
8171
- }
8227
+ function shouldRecoverCompactStall(text, userMessage) {
8228
+ const trimmed = text.trim().toLowerCase();
8229
+ if (!trimmed) return true;
8230
+ if (trimmed.length <= 160 && trimmed.includes("?")) return true;
8231
+ if (userMessage && buildPhaseReminder(userMessage, text)) {
8232
+ return true;
8172
8233
  }
8173
- const inlineReadMatch = trimmed.match(
8174
- /\bread_?page\b\s*\(\s*mode\s*=\s*["']?([a-z_]+)["']?\s*\)/i
8175
- ) ?? trimmed.match(
8176
- /\breadpage\b\s*\(\s*mode\s*=\s*["']?([a-z_]+)["']?\s*\)/i
8234
+ const repetitivePlanningSignals = [
8235
+ "next step:",
8236
+ "i will now inspect",
8237
+ "i will now read",
8238
+ "i will now click",
8239
+ "i'll use readpage",
8240
+ "i'll use read_page",
8241
+ "i'll start by clicking",
8242
+ "i have clicked on five different book titles",
8243
+ "clicked on five different book titles",
8244
+ "i'll begin with",
8245
+ "if the selection is unclear"
8246
+ ];
8247
+ if (repetitivePlanningSignals.some((pattern) => trimmed.includes(pattern))) {
8248
+ return true;
8249
+ }
8250
+ const falseCartSuccessWithoutConfirmation = /added\s+to\s+the\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+to\s+cart\s*:\s*["“”'a-z0-9 ,:&-]+|added\s+["“”'a-z0-9 ,:&-]+\s+to the cart|added\s+["“”'a-z0-9 ,:&-]+\s+to cart|added\s+.*\s+by\s+.*\s+to the cart/.test(
8251
+ trimmed
8252
+ ) && !/(cart confirmation|view cart|continue shopping|shopping cart|checkout|why i chose|here is why i chose|here are my reasons)/.test(
8253
+ trimmed
8177
8254
  );
8178
- if (inlineReadMatch && availableToolNames.has("read_page")) {
8179
- const rawMode = (inlineReadMatch[1] || "").trim().toLowerCase();
8180
- const normalizedMode = rawMode === "visibleonly" ? "visible_only" : rawMode === "resultsonly" ? "results_only" : rawMode;
8181
- if (normalizedMode) {
8182
- recovered.push({
8183
- id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8184
- name: "read_page",
8185
- argsJson: JSON.stringify({ mode: normalizedMode })
8186
- });
8187
- return recovered;
8188
- }
8255
+ if (falseCartSuccessWithoutConfirmation) {
8256
+ return true;
8189
8257
  }
8190
- const inlineInspectMatch = trimmed.match(
8191
- /\binspect_?element\b(?:\s+tool)?\b/i
8192
- ) ?? trimmed.match(/\binspectelement\b\b/i);
8193
- if (inlineInspectMatch && availableToolNames.has("inspect_element")) {
8194
- recovered.push({
8195
- id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8196
- name: "inspect_element",
8197
- argsJson: "{}"
8198
- });
8199
- return recovered;
8258
+ const completionSignals = [
8259
+ "i found",
8260
+ "i chose",
8261
+ "i selected",
8262
+ "i added",
8263
+ "here are",
8264
+ "these are",
8265
+ "recommendations",
8266
+ "reasoning",
8267
+ "why i chose",
8268
+ "added them to the cart"
8269
+ ];
8270
+ if (completionSignals.some((pattern) => trimmed.includes(pattern))) {
8271
+ return false;
8272
+ }
8273
+ return [
8274
+ "what are you hoping",
8275
+ "what would you like",
8276
+ "how can i help",
8277
+ "let me know",
8278
+ "are you looking for",
8279
+ "just browsing",
8280
+ "i need to",
8281
+ "i will",
8282
+ "i'll",
8283
+ "since i cannot see",
8284
+ "since i can't see",
8285
+ "cannot see the current page",
8286
+ "scroll down to",
8287
+ "load more results",
8288
+ "as placeholders",
8289
+ "would you like me to proceed",
8290
+ "action:",
8291
+ "one moment",
8292
+ "i will now navigate",
8293
+ "navigating to ",
8294
+ "this will take me",
8295
+ "i will use the browser"
8296
+ ].some((pattern) => trimmed.includes(pattern));
8297
+ }
8298
+ function shouldRetryCompactToolLoop(profile, text, hasToolHistory, userMessage) {
8299
+ return profile === "compact" && hasToolHistory && shouldRecoverCompactStall(text, userMessage);
8300
+ }
8301
+ function logAgentLoopDebug(payload) {
8302
+ if (!shouldDebugAgentLoop()) return;
8303
+ try {
8304
+ logger$l.info(`[agent-debug] ${JSON.stringify(payload)}`);
8305
+ } catch (err) {
8306
+ logger$l.warn("Failed to serialize debug payload:", err);
8200
8307
  }
8201
- return recovered;
8202
8308
  }
8203
8309
  function formatOpenAICompatErrorMessage(providerId, message) {
8204
8310
  if (providerId === "llama_cpp" && /(available context size|context size exceeded|exceeds the available context size|try increasing it)/i.test(
@@ -8631,28 +8737,184 @@ async function openExternalAllowlisted(url, rule) {
8631
8737
  }
8632
8738
  await electron.shell.openExternal(parsed.toString());
8633
8739
  }
8634
- const logger$i = createLogger("CodexOAuth");
8635
- const ISSUER = "https://auth.openai.com";
8636
- const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
8637
- const SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
8638
- const AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
8639
- const PREFERRED_PORT = 1455;
8640
- const FALLBACK_PORT = 1457;
8641
- let activeFlow = null;
8642
8740
  function base64url(buffer) {
8643
8741
  return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
8644
8742
  }
8645
8743
  function generatePkce() {
8646
8744
  const codeVerifier = base64url(crypto$1.randomBytes(64));
8647
8745
  const hash = crypto$1.createHash("sha256").update(codeVerifier).digest();
8648
- const codeChallenge = base64url(hash);
8649
- return { codeVerifier, codeChallenge };
8746
+ return {
8747
+ codeVerifier,
8748
+ codeChallenge: base64url(hash)
8749
+ };
8650
8750
  }
8651
8751
  function generateState() {
8652
8752
  return base64url(crypto$1.randomBytes(32));
8653
8753
  }
8654
- function buildAuthorizeUrl(port, pkce, state2) {
8655
- const redirectUri = `http://localhost:${port}/auth/callback`;
8754
+ function buildCallbackUrl(port, path2) {
8755
+ return `http://localhost:${port}${path2}`;
8756
+ }
8757
+ async function bindServer(server, preferredPorts) {
8758
+ for (const port of preferredPorts) {
8759
+ try {
8760
+ await new Promise((resolve, reject) => {
8761
+ const onError = (err) => {
8762
+ server.off("listening", onListening);
8763
+ reject(err);
8764
+ };
8765
+ const onListening = () => {
8766
+ server.off("error", onError);
8767
+ resolve();
8768
+ };
8769
+ server.once("error", onError);
8770
+ server.once("listening", onListening);
8771
+ server.listen(port, "127.0.0.1");
8772
+ });
8773
+ return port;
8774
+ } catch (err) {
8775
+ if (err.code === "EADDRINUSE") {
8776
+ continue;
8777
+ }
8778
+ throw err;
8779
+ }
8780
+ }
8781
+ throw new Error(
8782
+ `Could not bind ${preferredPorts.join(", ")} callback ports`
8783
+ );
8784
+ }
8785
+ function createLocalPkceOAuthFlow(config) {
8786
+ let activeFlow = null;
8787
+ const cancel = () => {
8788
+ if (!activeFlow) return;
8789
+ activeFlow.server.close();
8790
+ clearTimeout(activeFlow.timeout);
8791
+ try {
8792
+ activeFlow.onStatus("idle");
8793
+ } catch {
8794
+ config.logger.warn(`${config.name} OAuth cancel status callback failed`);
8795
+ }
8796
+ activeFlow = null;
8797
+ };
8798
+ const start = (onStatus) => {
8799
+ if (activeFlow) {
8800
+ throw new Error(`${config.name} auth flow already in progress`);
8801
+ }
8802
+ const pkce = generatePkce();
8803
+ const state2 = generateState();
8804
+ const callbackPath = config.callbackPath(state2);
8805
+ return new Promise((resolve, reject) => {
8806
+ let settled = false;
8807
+ let boundPort = 0;
8808
+ const safeOnStatus = (status, error) => {
8809
+ try {
8810
+ onStatus(status, error);
8811
+ } catch {
8812
+ config.logger.warn(`${config.name} OAuth status callback failed`);
8813
+ }
8814
+ };
8815
+ const cleanup = () => {
8816
+ clearTimeout(activeFlow?.timeout);
8817
+ activeFlow?.server.close();
8818
+ activeFlow = null;
8819
+ };
8820
+ const wrappedResolve = (result) => {
8821
+ if (settled) return;
8822
+ settled = true;
8823
+ cleanup();
8824
+ safeOnStatus("connected");
8825
+ resolve(result);
8826
+ };
8827
+ const wrappedReject = (err) => {
8828
+ if (settled) return;
8829
+ settled = true;
8830
+ cleanup();
8831
+ safeOnStatus("error", err.message);
8832
+ reject(err);
8833
+ };
8834
+ const server = http.createServer(async (req, res) => {
8835
+ const url = new URL(req.url || "/", `http://localhost:${boundPort}`);
8836
+ if (url.pathname === callbackPath) {
8837
+ const authError = config.authErrorMessage?.(url) || url.searchParams.get("error");
8838
+ if (authError) {
8839
+ res.writeHead(400, { "Content-Type": "text/plain", Connection: "close" });
8840
+ res.end(`Authorization failed: ${authError}`);
8841
+ wrappedReject(new Error(authError));
8842
+ return;
8843
+ }
8844
+ if (config.readState(url) !== state2) {
8845
+ res.writeHead(400, { "Content-Type": "text/plain", Connection: "close" });
8846
+ res.end("State mismatch. Please try again.");
8847
+ wrappedReject(new Error("State mismatch"));
8848
+ return;
8849
+ }
8850
+ const code = url.searchParams.get("code");
8851
+ if (!code) {
8852
+ res.writeHead(400, { "Content-Type": "text/plain", Connection: "close" });
8853
+ res.end("Missing authorization code.");
8854
+ wrappedReject(new Error("Missing authorization code"));
8855
+ return;
8856
+ }
8857
+ try {
8858
+ safeOnStatus("exchanging");
8859
+ const result = await config.exchangeCode({
8860
+ code,
8861
+ codeVerifier: pkce.codeVerifier,
8862
+ callbackUrl: buildCallbackUrl(boundPort, callbackPath),
8863
+ port: boundPort
8864
+ });
8865
+ res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
8866
+ res.end(config.successHtml(result));
8867
+ wrappedResolve(result);
8868
+ } catch (err) {
8869
+ const message = err instanceof Error ? err.message : "Unknown error";
8870
+ res.writeHead(400, { "Content-Type": "text/plain", Connection: "close" });
8871
+ res.end(`${config.name} setup failed: ${message}`);
8872
+ wrappedReject(err instanceof Error ? err : new Error(`${config.name} setup failed`));
8873
+ }
8874
+ return;
8875
+ }
8876
+ res.writeHead(404, { Connection: "close" });
8877
+ res.end("Not found");
8878
+ });
8879
+ const timeout = setTimeout(() => {
8880
+ wrappedReject(new Error(`${config.name} setup timed out after 5 minutes`));
8881
+ }, config.timeoutMs);
8882
+ activeFlow = {
8883
+ server,
8884
+ timeout,
8885
+ onStatus
8886
+ };
8887
+ bindServer(server, config.preferredPorts).then((port) => {
8888
+ if (settled || !activeFlow) return;
8889
+ boundPort = port;
8890
+ const callbackUrl = buildCallbackUrl(port, callbackPath);
8891
+ const authUrl = config.buildAuthorizeUrl({
8892
+ port,
8893
+ pkce,
8894
+ state: state2,
8895
+ callbackUrl
8896
+ });
8897
+ safeOnStatus("waiting");
8898
+ openExternalAllowlisted(authUrl, { hosts: [...config.openHosts] }).catch((err) => {
8899
+ config.logger.warn(`Failed to open ${config.name} auth URL:`, err);
8900
+ });
8901
+ }).catch(wrappedReject);
8902
+ });
8903
+ };
8904
+ return {
8905
+ start,
8906
+ cancel,
8907
+ isInProgress: () => activeFlow !== null
8908
+ };
8909
+ }
8910
+ const logger$k = createLogger("CodexOAuth");
8911
+ const ISSUER = "https://auth.openai.com";
8912
+ const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
8913
+ const SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
8914
+ const AUTH_TIMEOUT_MS$1 = 5 * 60 * 1e3;
8915
+ const PREFERRED_PORT$1 = 1455;
8916
+ const FALLBACK_PORT$1 = 1457;
8917
+ function buildAuthorizeUrl(redirectUri, pkce, state2) {
8656
8918
  const params = new URLSearchParams({
8657
8919
  response_type: "code",
8658
8920
  client_id: CLIENT_ID,
@@ -8771,172 +9033,36 @@ async function refreshAccessToken(tokens) {
8771
9033
  };
8772
9034
  return refreshedTokens;
8773
9035
  }
8774
- function startServer(port, pkce, expectedState, resolve, reject) {
8775
- const server = http.createServer(async (req, res) => {
8776
- const url = new URL(req.url || "/", `http://localhost:${port}`);
8777
- if (url.pathname === "/auth/callback") {
8778
- const state2 = url.searchParams.get("state");
8779
- const code = url.searchParams.get("code");
8780
- const error = url.searchParams.get("error");
8781
- const errorDescription = url.searchParams.get("error_description");
8782
- if (error) {
8783
- res.writeHead(400, { "Content-Type": "text/plain", "Connection": "close" });
8784
- const msg = errorDescription || error;
8785
- res.end(`Authorization failed: ${msg}`);
8786
- reject(new Error(msg));
8787
- return;
8788
- }
8789
- if (state2 !== expectedState) {
8790
- res.writeHead(400, { "Content-Type": "text/plain", "Connection": "close" });
8791
- res.end("State mismatch. Please try again.");
8792
- reject(new Error("State mismatch"));
8793
- return;
8794
- }
8795
- if (!code) {
8796
- res.writeHead(400, { "Content-Type": "text/plain", "Connection": "close" });
8797
- res.end("Missing authorization code.");
8798
- reject(new Error("Missing authorization code"));
8799
- return;
8800
- }
8801
- try {
8802
- activeFlow?.onStatus("exchanging");
8803
- const redirectUri = `http://localhost:${activeFlow?.port ?? port}/auth/callback`;
8804
- const tokens = await exchangeCodeForTokens(code, redirectUri, pkce.codeVerifier);
8805
- res.writeHead(302, {
8806
- Location: `/success?email=${encodeURIComponent(tokens.accountEmail || tokens.accountId)}`,
8807
- Connection: "close"
8808
- });
8809
- res.end();
8810
- resolve(tokens);
8811
- } catch (err) {
8812
- res.writeHead(400, { "Content-Type": "text/plain", "Connection": "close" });
8813
- res.end(`Token exchange failed: ${err instanceof Error ? err.message : "Unknown error"}`);
8814
- reject(err instanceof Error ? err : new Error("Token exchange failed"));
8815
- }
8816
- return;
8817
- }
8818
- if (url.pathname === "/success") {
8819
- const email = url.searchParams.get("email") || "";
8820
- res.writeHead(200, { "Content-Type": "text/html", "Connection": "close" });
8821
- res.end(`<!DOCTYPE html>
8822
- <html><head><meta charset="utf-8"><title>Vessel — Signed In</title>
8823
- <style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#111;color:#eee}</style></head>
8824
- <body><div style="text-align:center"><h1>✓ Signed In</h1>
8825
- <p>Connected as ${escapeHtml(email)}</p><p>You can close this tab.</p></div></body></html>`);
8826
- return;
8827
- }
8828
- if (url.pathname === "/cancel") {
8829
- res.writeHead(200, { "Content-Type": "text/plain", "Connection": "close" });
8830
- res.end("Login cancelled");
8831
- reject(new Error("Login cancelled by user"));
8832
- return;
8833
- }
8834
- res.writeHead(404, { "Connection": "close" });
8835
- res.end("Not found");
8836
- });
8837
- return server;
8838
- }
8839
9036
  function escapeHtml(text) {
8840
9037
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
8841
9038
  }
8842
- async function bindServer(server) {
8843
- const allowedPorts = [PREFERRED_PORT, FALLBACK_PORT];
8844
- for (const port of allowedPorts) {
8845
- try {
8846
- await new Promise((resolve, reject) => {
8847
- const onError = (err) => {
8848
- server.off("listening", onListening);
8849
- reject(err);
8850
- };
8851
- const onListening = () => {
8852
- server.off("error", onError);
8853
- resolve();
8854
- };
8855
- server.once("error", onError);
8856
- server.once("listening", onListening);
8857
- server.listen(port, "127.0.0.1");
8858
- });
8859
- return port;
8860
- } catch (err) {
8861
- if (err.code === "EADDRINUSE") {
8862
- continue;
8863
- }
8864
- throw err;
8865
- }
8866
- }
8867
- throw new Error(
8868
- `Could not bind Codex OAuth callback server to registered ports ${allowedPorts.join(", ")}`
8869
- );
8870
- }
9039
+ const codexOAuth = createLocalPkceOAuthFlow({
9040
+ name: "Codex",
9041
+ logger: logger$k,
9042
+ preferredPorts: [PREFERRED_PORT$1, FALLBACK_PORT$1],
9043
+ timeoutMs: AUTH_TIMEOUT_MS$1,
9044
+ callbackPath: () => "/auth/callback",
9045
+ readState: (url) => url.searchParams.get("state"),
9046
+ authErrorMessage: (url) => url.searchParams.get("error_description") || url.searchParams.get("error"),
9047
+ buildAuthorizeUrl: ({ callbackUrl, pkce, state: state2 }) => buildAuthorizeUrl(callbackUrl, pkce, state2),
9048
+ exchangeCode: ({ code, callbackUrl, codeVerifier }) => exchangeCodeForTokens(code, callbackUrl, codeVerifier),
9049
+ successHtml: (tokens) => {
9050
+ const label = escapeHtml(tokens.accountEmail || tokens.accountId);
9051
+ return `<!DOCTYPE html>
9052
+ <html><head><meta charset="utf-8"><title>Vessel — Signed In</title>
9053
+ <style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#111;color:#eee}</style></head>
9054
+ <body><div style="text-align:center"><h1>Signed In</h1>
9055
+ <p>Connected as ${label}</p><p>You can close this tab.</p></div></body></html>`;
9056
+ },
9057
+ openHosts: ["auth.openai.com"]
9058
+ });
8871
9059
  async function startCodexOAuth(onStatus) {
8872
- if (activeFlow) {
8873
- throw new Error("Auth flow already in progress");
8874
- }
8875
- const pkce = generatePkce();
8876
- const state2 = generateState();
8877
- return new Promise((resolve, reject) => {
8878
- let settled = false;
8879
- const safeOnStatus = (status, error) => {
8880
- try {
8881
- onStatus(status, error);
8882
- } catch {
8883
- logger$i.warn("Codex OAuth status callback failed — window may be closed");
8884
- }
8885
- };
8886
- const wrappedResolve = (tokens) => {
8887
- if (settled) return;
8888
- settled = true;
8889
- cleanup();
8890
- safeOnStatus("connected");
8891
- resolve(tokens);
8892
- };
8893
- const wrappedReject = (err) => {
8894
- if (settled) return;
8895
- settled = true;
8896
- cleanup();
8897
- safeOnStatus("error", err.message);
8898
- reject(err);
8899
- };
8900
- const server = startServer(0, pkce, state2, wrappedResolve, wrappedReject);
8901
- const timeout = setTimeout(() => {
8902
- wrappedReject(new Error("Auth flow timed out after 5 minutes"));
8903
- }, AUTH_TIMEOUT_MS);
8904
- activeFlow = {
8905
- state: state2,
8906
- codeVerifier: pkce.codeVerifier,
8907
- port: 0,
8908
- server,
8909
- timeout,
8910
- onStatus
8911
- };
8912
- const cleanup = () => {
8913
- if (activeFlow?.timeout) clearTimeout(activeFlow.timeout);
8914
- activeFlow?.server.close();
8915
- activeFlow = null;
8916
- };
8917
- bindServer(server).then((port) => {
8918
- if (settled) return;
8919
- activeFlow.port = port;
8920
- const authUrl = buildAuthorizeUrl(port, pkce, state2);
8921
- safeOnStatus("waiting");
8922
- openExternalAllowlisted(authUrl, { hosts: ["auth.openai.com"] }).catch((err) => {
8923
- logger$i.warn("Failed to open browser, user will need the URL:", err);
8924
- });
8925
- }).catch(wrappedReject);
8926
- });
9060
+ return codexOAuth.start(onStatus);
8927
9061
  }
8928
9062
  function cancelCodexOAuth() {
8929
- if (!activeFlow) return;
8930
- activeFlow.server.close();
8931
- if (activeFlow.timeout) clearTimeout(activeFlow.timeout);
8932
- try {
8933
- activeFlow.onStatus("idle");
8934
- } catch {
8935
- logger$i.warn("Codex OAuth cancel status callback failed — window may be closed");
8936
- }
8937
- activeFlow = null;
9063
+ codexOAuth.cancel();
8938
9064
  }
8939
- const logger$h = createLogger("CodexProvider");
9065
+ const logger$j = createLogger("CodexProvider");
8940
9066
  const REFRESH_WINDOW_MS = 5 * 60 * 1e3;
8941
9067
  const CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api/codex";
8942
9068
  const CODEX_CLIENT_VERSION = "0.129.0";
@@ -9001,7 +9127,7 @@ class CodexProvider {
9001
9127
  async ensureFreshTokens() {
9002
9128
  if (Date.now() < this.tokens.expiresAt - REFRESH_WINDOW_MS) return;
9003
9129
  try {
9004
- logger$h.info("Refreshing Codex access token");
9130
+ logger$j.info("Refreshing Codex access token");
9005
9131
  const fresh = await refreshAccessToken(this.tokens);
9006
9132
  this.tokens = fresh;
9007
9133
  writeStoredCodexTokens(fresh);
@@ -9149,7 +9275,7 @@ class CodexProvider {
9149
9275
  } catch (err) {
9150
9276
  if (err.name !== "AbortError") {
9151
9277
  const msg = err instanceof Error ? err.message : String(err);
9152
- logger$h.error("Codex streamQuery error:", err);
9278
+ logger$j.error("Codex streamQuery error:", err);
9153
9279
  onChunk(`
9154
9280
 
9155
9281
  [Error: ${msg}]`);
@@ -9217,7 +9343,7 @@ class CodexProvider {
9217
9343
  } catch (err) {
9218
9344
  if (err.name !== "AbortError") {
9219
9345
  const msg = err instanceof Error ? err.message : String(err);
9220
- logger$h.error("Codex streamAgentQuery error:", err);
9346
+ logger$j.error("Codex streamAgentQuery error:", err);
9221
9347
  onChunk(`
9222
9348
 
9223
9349
  [Error: ${msg}]`);
@@ -9409,7 +9535,7 @@ function createProvider(config) {
9409
9535
  return new OpenAICompatProvider(normalized);
9410
9536
  }
9411
9537
  const require$1 = node_module.createRequire(require("url").pathToFileURL(__filename).href);
9412
- const logger$g = createLogger("DevTrace");
9538
+ const logger$i = createLogger("DevTrace");
9413
9539
  let cachedFactory;
9414
9540
  function createNoopTraceSession() {
9415
9541
  return {
@@ -9442,7 +9568,7 @@ function loadLocalFactory() {
9442
9568
  return cachedFactory;
9443
9569
  }
9444
9570
  } catch (err) {
9445
- logger$g.warn("Failed to load local trace logger:", err);
9571
+ logger$i.warn("Failed to load local trace logger:", err);
9446
9572
  }
9447
9573
  }
9448
9574
  return cachedFactory;
@@ -12665,6 +12791,8 @@ const UNSORTED_ID = "unsorted";
12665
12791
  const ARCHIVE_FOLDER_NAME = "Archive";
12666
12792
  const NETSCAPE_BOOKMARKS_DOCTYPE = "<!DOCTYPE NETSCAPE-Bookmark-file-1>";
12667
12793
  const SAVE_DEBOUNCE_MS$1 = 250;
12794
+ const DEFAULT_BOOKMARK_SEARCH_LIMIT = 50;
12795
+ const MAX_BOOKMARK_SEARCH_LIMIT = 200;
12668
12796
  let state$2 = null;
12669
12797
  const listeners = /* @__PURE__ */ new Set();
12670
12798
  function cloneState(current) {
@@ -12673,6 +12801,10 @@ function cloneState(current) {
12673
12801
  bookmarks: current.bookmarks.map((bookmark) => ({ ...bookmark }))
12674
12802
  };
12675
12803
  }
12804
+ function getFolderMap() {
12805
+ load$1();
12806
+ return new Map(state$2.folders.map((folder) => [folder.id, folder]));
12807
+ }
12676
12808
  function getBookmarksPath() {
12677
12809
  return path.join(electron.app.getPath("userData"), "vessel-bookmarks.json");
12678
12810
  }
@@ -12899,13 +13031,16 @@ function listFolderOverviews() {
12899
13031
  }))
12900
13032
  ];
12901
13033
  }
12902
- function searchBookmarks(query) {
13034
+ function searchBookmarks(query, limit = DEFAULT_BOOKMARK_SEARCH_LIMIT) {
12903
13035
  load$1();
12904
13036
  if (!query.trim()) return [];
13037
+ const foldersById = getFolderMap();
13038
+ const safeLimit = Math.max(
13039
+ 1,
13040
+ Math.min(MAX_BOOKMARK_SEARCH_LIMIT, Math.floor(limit))
13041
+ );
12905
13042
  return state$2.bookmarks.map((bookmark) => {
12906
- const folder = state$2.folders.find(
12907
- (item) => item.id === bookmark.folderId
12908
- );
13043
+ const folder = foldersById.get(bookmark.folderId);
12909
13044
  const { matchedFields, score } = getBookmarkSearchMatch({
12910
13045
  query,
12911
13046
  title: bookmark.title,
@@ -12929,7 +13064,7 @@ function searchBookmarks(query) {
12929
13064
  score: result.score
12930
13065
  })).sort(
12931
13066
  (a, b) => b.score - a.score || b.bookmark.savedAt.localeCompare(a.bookmark.savedAt)
12932
- );
13067
+ ).slice(0, safeLimit);
12933
13068
  }
12934
13069
  function createFolderWithSummary(name, summary) {
12935
13070
  load$1();
@@ -13561,7 +13696,7 @@ function formatDeadLinkMessage(label, result) {
13561
13696
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
13562
13697
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
13563
13698
  }
13564
- const logger$f = createLogger("Screenshot");
13699
+ const logger$h = createLogger("Screenshot");
13565
13700
  const SCREENSHOT_RETRY_COUNT = 3;
13566
13701
  const SCREENSHOT_RETRY_BASE_DELAY_MS = 120;
13567
13702
  async function captureScreenshot(wc) {
@@ -13583,7 +13718,7 @@ async function captureScreenshot(wc) {
13583
13718
  }
13584
13719
  }
13585
13720
  } catch (err) {
13586
- logger$f.debug(
13721
+ logger$h.debug(
13587
13722
  `capturePage attempt ${attempt + 1} failed; retrying if attempts remain.`,
13588
13723
  getErrorMessage(err)
13589
13724
  );
@@ -14177,24 +14312,115 @@ function compactCurrentTabResult(text) {
14177
14312
  function looksLikeRichToolResult(text) {
14178
14313
  return text.startsWith("{") && text.includes('"__richResult":true');
14179
14314
  }
14180
- function formatCompactToolResult(name, result) {
14181
- if (!result || looksLikeRichToolResult(result)) return result;
14182
- switch (name) {
14183
- case "current_tab":
14184
- return compactCurrentTabResult(result);
14185
- case "read_page":
14186
- return compactReadPageResult(result);
14187
- case "search":
14188
- case "navigate":
14189
- case "go_back":
14190
- case "go_forward":
14191
- case "paginate":
14192
- case "wait_for_navigation":
14193
- return compactSearchLikeResult(result);
14194
- case "list_tabs":
14195
- return limitText(result, 10, 900);
14196
- default:
14197
- return limitText(result, 18, 1400);
14315
+ function formatCompactToolResult(name, result) {
14316
+ if (!result || looksLikeRichToolResult(result)) return result;
14317
+ switch (name) {
14318
+ case "current_tab":
14319
+ return compactCurrentTabResult(result);
14320
+ case "read_page":
14321
+ return compactReadPageResult(result);
14322
+ case "search":
14323
+ case "navigate":
14324
+ case "go_back":
14325
+ case "go_forward":
14326
+ case "paginate":
14327
+ case "wait_for_navigation":
14328
+ return compactSearchLikeResult(result);
14329
+ case "list_tabs":
14330
+ return limitText(result, 10, 900);
14331
+ default:
14332
+ return limitText(result, 18, 1400);
14333
+ }
14334
+ }
14335
+ const ADD_TO_CART_PATTERNS = [
14336
+ "add to cart",
14337
+ "add to bag",
14338
+ "add to basket",
14339
+ "add to my cart",
14340
+ "add to my bag",
14341
+ "add to my basket",
14342
+ "add item to cart",
14343
+ "add item to bag",
14344
+ "add item to basket"
14345
+ ];
14346
+ const CART_CLICK_COOLDOWN_MS = 15e3;
14347
+ const CART_ADDED_TTL_MS = 30 * 6e4;
14348
+ const recentCartClicks = /* @__PURE__ */ new Map();
14349
+ const cartAddedProducts = /* @__PURE__ */ new Map();
14350
+ function isAddToCartText(text) {
14351
+ const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
14352
+ return ADD_TO_CART_PATTERNS.some((pattern) => normalized.includes(pattern));
14353
+ }
14354
+ function recordCartClick(url) {
14355
+ recentCartClicks.set(url, Date.now());
14356
+ pruneRecentCartClicks();
14357
+ }
14358
+ function hasRecentCartClick(url) {
14359
+ const recent = recentCartClicks.get(url);
14360
+ if (!recent) return false;
14361
+ if (Date.now() - recent > CART_CLICK_COOLDOWN_MS) {
14362
+ recentCartClicks.delete(url);
14363
+ return false;
14364
+ }
14365
+ return true;
14366
+ }
14367
+ function isDuplicateCartClick(url, text) {
14368
+ return hasRecentCartClick(url) && isAddToCartText(text);
14369
+ }
14370
+ function recordProductAddedToCart(url, productName) {
14371
+ pruneCartAddedProducts();
14372
+ cartAddedProducts.set(normalizeCartProductKey(url), {
14373
+ title: productName || url,
14374
+ ts: Date.now()
14375
+ });
14376
+ }
14377
+ function isProductAlreadyInCart(url) {
14378
+ pruneCartAddedProducts();
14379
+ return cartAddedProducts.has(normalizeCartProductKey(url));
14380
+ }
14381
+ function getCartAddedSummary(url) {
14382
+ pruneCartAddedProducts();
14383
+ const origin = cartOrigin(url);
14384
+ const items = Array.from(cartAddedProducts.entries()).filter(([key2]) => !origin || key2.startsWith(`${origin}/`)).map(([_path, info]) => `- ${info.title}`).join("\n");
14385
+ if (!items) return "";
14386
+ const count = items.split("\n").length;
14387
+ return `
14388
+ Already in cart (${count} items):
14389
+ ${items}`;
14390
+ }
14391
+ function clearCartClickState() {
14392
+ cartAddedProducts.clear();
14393
+ recentCartClicks.clear();
14394
+ }
14395
+ function pruneRecentCartClicks(now = Date.now()) {
14396
+ for (const [key2, ts] of recentCartClicks) {
14397
+ if (now - ts > CART_CLICK_COOLDOWN_MS) {
14398
+ recentCartClicks.delete(key2);
14399
+ }
14400
+ }
14401
+ }
14402
+ function normalizeCartProductKey(url) {
14403
+ try {
14404
+ const parsed = new URL(url);
14405
+ const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
14406
+ return `${parsed.origin}${pathname}`;
14407
+ } catch {
14408
+ return url;
14409
+ }
14410
+ }
14411
+ function pruneCartAddedProducts(now = Date.now()) {
14412
+ for (const [key2, entry] of cartAddedProducts) {
14413
+ if (now - entry.ts > CART_ADDED_TTL_MS) {
14414
+ cartAddedProducts.delete(key2);
14415
+ }
14416
+ }
14417
+ }
14418
+ function cartOrigin(url) {
14419
+ if (!url) return null;
14420
+ try {
14421
+ return new URL(url).origin;
14422
+ } catch {
14423
+ return null;
14198
14424
  }
14199
14425
  }
14200
14426
  const HUGGING_FACE_HUB_HOSTS = /* @__PURE__ */ new Set(["huggingface.co", "www.huggingface.co"]);
@@ -14460,12 +14686,15 @@ function buildHuggingFaceSearchShortcut(currentUrl, rawQuery) {
14460
14686
  class TabMutex {
14461
14687
  queue = Promise.resolve();
14462
14688
  enqueue(fn) {
14463
- return new Promise((resolve, reject) => {
14464
- this.queue = this.queue.then(fn).then(resolve, reject);
14465
- });
14689
+ const run = this.queue.then(fn, fn);
14690
+ this.queue = run.then(
14691
+ () => void 0,
14692
+ () => void 0
14693
+ );
14694
+ return run;
14466
14695
  }
14467
14696
  }
14468
- const logger$e = createLogger("PageActions");
14697
+ const logger$g = createLogger("PageActions");
14469
14698
  function getBookmarkMetadataFromArgs(args) {
14470
14699
  return normalizeBookmarkMetadata({
14471
14700
  intent: args.intent ?? args.intent,
@@ -14651,7 +14880,7 @@ async function executePageScript(wc, script, options) {
14651
14880
  return result;
14652
14881
  } catch (err) {
14653
14882
  const label = options?.label ? ` (${options.label})` : "";
14654
- logger$e.warn(`Failed to execute page script${label}:`, err);
14883
+ logger$g.warn(`Failed to execute page script${label}:`, err);
14655
14884
  return null;
14656
14885
  } finally {
14657
14886
  if (timer) {
@@ -14752,7 +14981,7 @@ Search results snapshot:
14752
14981
  ${truncated}`;
14753
14982
  }
14754
14983
  } catch (err) {
14755
- logger$e.warn("Failed to build post-search summary, falling back to nav summary:", err);
14984
+ logger$g.warn("Failed to build post-search summary, falling back to nav summary:", err);
14756
14985
  }
14757
14986
  const fallback = await getPostNavSummary(wc);
14758
14987
  return fallback ? `${fallback}
@@ -14775,7 +15004,7 @@ Page snapshot after navigation:
14775
15004
  ${truncated}`;
14776
15005
  }
14777
15006
  } catch (err) {
14778
- logger$e.warn("Failed to build post-click navigation summary:", err);
15007
+ logger$g.warn("Failed to build post-click navigation summary:", err);
14779
15008
  }
14780
15009
  return "";
14781
15010
  }
@@ -15269,7 +15498,7 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15269
15498
  }
15270
15499
  }
15271
15500
  } catch (err) {
15272
- logger$e.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
15501
+ logger$g.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
15273
15502
  }
15274
15503
  if (snapshot2.url && snapshot2.url !== wc.getURL()) {
15275
15504
  try {
@@ -15278,7 +15507,7 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15278
15507
  await waitForLoad(wc, 3e3);
15279
15508
  return;
15280
15509
  } catch (err) {
15281
- logger$e.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
15510
+ logger$g.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
15282
15511
  }
15283
15512
  }
15284
15513
  if (snapshot2.url) {
@@ -15286,49 +15515,13 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15286
15515
  await wc.reload();
15287
15516
  await waitForLoad(wc, 3e3);
15288
15517
  } catch (err) {
15289
- logger$e.warn("Failed to restore locale via page reload:", err);
15518
+ logger$g.warn("Failed to restore locale via page reload:", err);
15290
15519
  }
15291
15520
  }
15292
15521
  }
15293
- const ADD_TO_CART_PATTERNS = [
15294
- "add to cart",
15295
- "add to bag",
15296
- "add to basket",
15297
- "add to my cart",
15298
- "add to my bag",
15299
- "add to my basket",
15300
- "add item to cart",
15301
- "add item to bag",
15302
- "add item to basket"
15303
- ];
15304
- const recentCartClicks = /* @__PURE__ */ new Map();
15305
- const CART_CLICK_COOLDOWN_MS = 15e3;
15306
- const CART_ADDED_TTL_MS = 30 * 6e4;
15307
- const cartAddedProducts = /* @__PURE__ */ new Map();
15308
15522
  let clickStreakUrl = null;
15309
15523
  let clickStreakCount = 0;
15310
15524
  const CLICK_STREAK_THRESHOLD = 3;
15311
- function isAddToCartText(text) {
15312
- const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
15313
- return ADD_TO_CART_PATTERNS.some((p) => normalized.includes(p));
15314
- }
15315
- function recordCartClick(url, text) {
15316
- recentCartClicks.set(url, { text, ts: Date.now() });
15317
- for (const [key2, entry] of recentCartClicks) {
15318
- if (Date.now() - entry.ts > CART_CLICK_COOLDOWN_MS) {
15319
- recentCartClicks.delete(key2);
15320
- }
15321
- }
15322
- }
15323
- function isDuplicateCartClick(url, text) {
15324
- const recent = recentCartClicks.get(url);
15325
- if (!recent) return false;
15326
- if (Date.now() - recent.ts > CART_CLICK_COOLDOWN_MS) {
15327
- recentCartClicks.delete(url);
15328
- return false;
15329
- }
15330
- return isAddToCartText(text);
15331
- }
15332
15525
  async function getProductPageTitle(wc) {
15333
15526
  try {
15334
15527
  const heading = await executePageScript(
@@ -15353,54 +15546,8 @@ async function getProductPageTitle(wc) {
15353
15546
  }
15354
15547
  return wc.getTitle() || "";
15355
15548
  }
15356
- function normalizeCartProductKey(url) {
15357
- try {
15358
- const parsed = new URL(url);
15359
- const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
15360
- return `${parsed.origin}${pathname}`;
15361
- } catch {
15362
- return url;
15363
- }
15364
- }
15365
- function pruneCartAddedProducts(now = Date.now()) {
15366
- for (const [key2, entry] of cartAddedProducts) {
15367
- if (now - entry.ts > CART_ADDED_TTL_MS) {
15368
- cartAddedProducts.delete(key2);
15369
- }
15370
- }
15371
- }
15372
- function cartOrigin(url) {
15373
- if (!url) return null;
15374
- try {
15375
- return new URL(url).origin;
15376
- } catch {
15377
- return null;
15378
- }
15379
- }
15380
- function recordProductAddedToCart(url, productName) {
15381
- pruneCartAddedProducts();
15382
- cartAddedProducts.set(normalizeCartProductKey(url), {
15383
- title: productName || url,
15384
- ts: Date.now()
15385
- });
15386
- }
15387
- function isProductAlreadyInCart(url) {
15388
- pruneCartAddedProducts();
15389
- return cartAddedProducts.has(normalizeCartProductKey(url));
15390
- }
15391
- function getCartAddedSummary(url) {
15392
- pruneCartAddedProducts();
15393
- const origin = cartOrigin(url);
15394
- const items = Array.from(cartAddedProducts.entries()).filter(([key2]) => !origin || key2.startsWith(`${origin}/`)).map(([_path, info]) => `- ${info.title}`).join("\n");
15395
- if (!items) return "";
15396
- const count = items.split("\n").length;
15397
- return `
15398
- Already in cart (${count} items):
15399
- ${items}`;
15400
- }
15401
15549
  function clearCartState() {
15402
- cartAddedProducts.clear();
15403
- recentCartClicks.clear();
15550
+ clearCartClickState();
15404
15551
  clickStreakUrl = null;
15405
15552
  clickStreakCount = 0;
15406
15553
  }
@@ -15452,7 +15599,7 @@ Go back and select a different product.`;
15452
15599
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
15453
15600
  if (typeof result === "string" && result.startsWith("Error")) return result;
15454
15601
  if (idxCartMatch) {
15455
- recordCartClick(beforeUrl2, idxLabel);
15602
+ recordCartClick(beforeUrl2);
15456
15603
  }
15457
15604
  await waitForPotentialNavigation(wc, beforeUrl2);
15458
15605
  const afterUrl2 = wc.getURL();
@@ -15519,7 +15666,7 @@ Go back and select a different product.`;
15519
15666
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
15520
15667
  if (typeof result === "string" && result.startsWith("Error")) return result;
15521
15668
  if (shadowCartMatch) {
15522
- recordCartClick(beforeUrl2, shadowLabel);
15669
+ recordCartClick(beforeUrl2);
15523
15670
  }
15524
15671
  await waitForPotentialNavigation(wc, beforeUrl2);
15525
15672
  const afterUrl2 = wc.getURL();
@@ -15555,7 +15702,7 @@ Note: Page did not change after click.`;
15555
15702
  if (cartMatch && isDuplicateCartClick(beforeUrl, elInfo.text)) {
15556
15703
  return `Blocked: "${elInfo.text}" was already clicked on this page. The item is in your cart. Call read_page to see available actions (e.g. View Cart, Continue Shopping).`;
15557
15704
  }
15558
- if (!cartMatch && recentCartClicks.has(beforeUrl)) {
15705
+ if (!cartMatch && hasRecentCartClick(beforeUrl)) {
15559
15706
  const dialogActions = await getCartDialogActions(wc);
15560
15707
  if (dialogActions) {
15561
15708
  return `Blocked: a cart confirmation dialog is open. Do not click background elements.
@@ -15575,7 +15722,7 @@ Click one of these dialog actions instead.`;
15575
15722
  Go back and select a different product.`;
15576
15723
  }
15577
15724
  if (cartMatch) {
15578
- recordCartClick(beforeUrl, elInfo.text);
15725
+ recordCartClick(beforeUrl);
15579
15726
  }
15580
15727
  const tagLabel = elInfo.tag && elInfo.tag !== "a" && elInfo.tag !== "button" ? ` <${elInfo.tag}>` : "";
15581
15728
  const clickText = `Clicked: ${elInfo.text}${tagLabel}`;
@@ -15638,7 +15785,7 @@ ${postActivationOverlayHint}`;
15638
15785
  return `${clickText} -> ${hrefFallbackUrl} (recovered via href fallback)`;
15639
15786
  }
15640
15787
  } catch (err) {
15641
- logger$e.warn("Failed href fallback after click, returning generic click result:", err);
15788
+ logger$g.warn("Failed href fallback after click, returning generic click result:", err);
15642
15789
  }
15643
15790
  }
15644
15791
  }
@@ -15683,7 +15830,7 @@ async function tryAutoDismissCartDialog(wc) {
15683
15830
  return result;
15684
15831
  }
15685
15832
  } catch (err) {
15686
- logger$e.warn("Failed to auto-dismiss cart dialog, falling back to dialog actions:", err);
15833
+ logger$g.warn("Failed to auto-dismiss cart dialog, falling back to dialog actions:", err);
15687
15834
  }
15688
15835
  return null;
15689
15836
  }
@@ -17966,7 +18113,7 @@ async function executeAction(name, args, ctx) {
17966
18113
  )
17967
18114
  ]);
17968
18115
  } catch (err) {
17969
- logger$e.warn("Failed to extract content for read_page, falling back to lighter recovery:", err);
18116
+ logger$g.warn("Failed to extract content for read_page, falling back to lighter recovery:", err);
17970
18117
  content = null;
17971
18118
  }
17972
18119
  if (!content || content.content.length === 0) {
@@ -17983,12 +18130,12 @@ async function executeAction(name, args, ctx) {
17983
18130
  new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
17984
18131
  ]);
17985
18132
  } catch (err) {
17986
- logger$e.warn("Failed to re-extract content after iframe consent dismissal:", err);
18133
+ logger$g.warn("Failed to re-extract content after iframe consent dismissal:", err);
17987
18134
  content = null;
17988
18135
  }
17989
18136
  }
17990
18137
  } catch (err) {
17991
- logger$e.warn("Failed iframe consent dismissal during read_page recovery:", err);
18138
+ logger$g.warn("Failed iframe consent dismissal during read_page recovery:", err);
17992
18139
  }
17993
18140
  }
17994
18141
  if (content && content.content.length > 0) {
@@ -18401,7 +18548,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
18401
18548
  try {
18402
18549
  page = await extractContent(wc);
18403
18550
  } catch (err) {
18404
- logger$e.warn("Failed to extract content for suggest:", err);
18551
+ logger$g.warn("Failed to extract content for suggest:", err);
18405
18552
  return "Could not read page. Try navigate to a working URL.";
18406
18553
  }
18407
18554
  const suggestions = [];
@@ -19009,6 +19156,57 @@ function onAIStreamIdle(listener) {
19009
19156
  idleListeners.add(listener);
19010
19157
  return () => idleListeners.delete(listener);
19011
19158
  }
19159
+ const MAX_PROVIDER_HISTORY_MESSAGES = 24;
19160
+ const MAX_PROVIDER_HISTORY_CHARS = 24e3;
19161
+ const MAX_PROVIDER_HISTORY_MESSAGE_CHARS = 3e3;
19162
+ const MAX_PROVIDER_HISTORY_SUMMARY_CHARS = 2e3;
19163
+ function truncateText(value, maxLength) {
19164
+ if (value.length <= maxLength) return value;
19165
+ return `${value.slice(0, maxLength - 3).trimEnd()}...`;
19166
+ }
19167
+ function normalizeHistoryMessage(message) {
19168
+ return {
19169
+ role: message.role,
19170
+ content: truncateText(message.content, MAX_PROVIDER_HISTORY_MESSAGE_CHARS)
19171
+ };
19172
+ }
19173
+ function totalHistoryChars(history) {
19174
+ return history.reduce((total, message) => total + message.content.length, 0);
19175
+ }
19176
+ function summarizeOmittedHistory(history) {
19177
+ const snippets = history.slice(-12).map((message) => `${message.role}: ${truncateText(message.content.replace(/\s+/g, " ").trim(), 220)}`).filter((line) => line.length > "assistant: ".length);
19178
+ const content = truncateText(
19179
+ [
19180
+ `[Earlier conversation compacted: ${history.length} message${history.length === 1 ? "" : "s"} omitted.]`,
19181
+ ...snippets
19182
+ ].join("\n"),
19183
+ MAX_PROVIDER_HISTORY_SUMMARY_CHARS
19184
+ );
19185
+ return { role: "user", content };
19186
+ }
19187
+ function compactProviderHistory(history = []) {
19188
+ const normalized = history.map(normalizeHistoryMessage);
19189
+ if (normalized.length <= MAX_PROVIDER_HISTORY_MESSAGES && totalHistoryChars(normalized) <= MAX_PROVIDER_HISTORY_CHARS) {
19190
+ return normalized;
19191
+ }
19192
+ const recent = [];
19193
+ const recentBudget = MAX_PROVIDER_HISTORY_CHARS - MAX_PROVIDER_HISTORY_SUMMARY_CHARS;
19194
+ let usedChars = 0;
19195
+ for (let index = normalized.length - 1; index >= 0; index--) {
19196
+ const message = normalized[index];
19197
+ const nextChars = usedChars + message.content.length;
19198
+ if (recent.length >= MAX_PROVIDER_HISTORY_MESSAGES || nextChars > recentBudget) {
19199
+ break;
19200
+ }
19201
+ recent.unshift(message);
19202
+ usedChars = nextChars;
19203
+ }
19204
+ if (recent.length === 0 && normalized.length > 0) {
19205
+ recent.unshift(normalized[normalized.length - 1]);
19206
+ }
19207
+ const omitted = normalized.slice(0, normalized.length - recent.length);
19208
+ return omitted.length > 0 ? [summarizeOmittedHistory(omitted), ...recent] : recent;
19209
+ }
19012
19210
  const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
19013
19211
  const DEFAULT_NOTE_FOLDER = "Vessel/Research";
19014
19212
  const DEFAULT_BOOKMARK_FOLDER = "Vessel/Bookmarks";
@@ -19799,7 +19997,7 @@ Exception: ${result.exceptionDetails}`);
19799
19997
  }
19800
19998
  );
19801
19999
  }
19802
- const logger$d = createLogger("VaultShared");
20000
+ const logger$f = createLogger("VaultShared");
19803
20001
  const ALGORITHM = "aes-256-gcm";
19804
20002
  const IV_LENGTH = 12;
19805
20003
  const AUTH_TAG_LENGTH = 16;
@@ -19893,7 +20091,7 @@ function createVaultIO(vaultFilename, encrypt2, decrypt2) {
19893
20091
  cachedEntries = JSON.parse(json);
19894
20092
  return cachedEntries;
19895
20093
  } catch (err) {
19896
- logger$d.error("Failed to load vault:", err);
20094
+ logger$f.error("Failed to load vault:", err);
19897
20095
  throw new Error("Could not unlock the vault. Check OS secret storage availability.");
19898
20096
  }
19899
20097
  }
@@ -19976,7 +20174,7 @@ function createAuditLog(filename, maxEntries) {
19976
20174
  } catch {
19977
20175
  }
19978
20176
  } catch (err) {
19979
- logger$d.error("Failed to write audit log:", err);
20177
+ logger$f.error("Failed to write audit log:", err);
19980
20178
  }
19981
20179
  }
19982
20180
  function readAuditLog2(limit = 100) {
@@ -19986,7 +20184,7 @@ function createAuditLog(filename, maxEntries) {
19986
20184
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
19987
20185
  return lines.slice(-Math.min(limit, maxEntries)).map((line) => JSON.parse(line)).reverse();
19988
20186
  } catch (err) {
19989
- logger$d.error("Failed to read audit log:", err);
20187
+ logger$f.error("Failed to read audit log:", err);
19990
20188
  return [];
19991
20189
  }
19992
20190
  }
@@ -20090,7 +20288,7 @@ async function requestConsent(request) {
20090
20288
  }
20091
20289
  const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
20092
20290
  const MAX_ENTRIES = 1e3;
20093
- const logger$c = createLogger("VaultAudit");
20291
+ const logger$e = createLogger("VaultAudit");
20094
20292
  function getAuditPath() {
20095
20293
  return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
20096
20294
  }
@@ -20104,7 +20302,7 @@ function appendAuditEntry(entry) {
20104
20302
  });
20105
20303
  fs$1.chmodSync(auditPath, 384);
20106
20304
  } catch (err) {
20107
- logger$c.error("Failed to write audit log:", err);
20305
+ logger$e.error("Failed to write audit log:", err);
20108
20306
  }
20109
20307
  }
20110
20308
  function readAuditLog$1(limit = 100) {
@@ -20114,7 +20312,7 @@ function readAuditLog$1(limit = 100) {
20114
20312
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
20115
20313
  return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
20116
20314
  } catch (err) {
20117
- logger$c.error("Failed to read audit log:", err);
20315
+ logger$e.error("Failed to read audit log:", err);
20118
20316
  return [];
20119
20317
  }
20120
20318
  }
@@ -20282,7 +20480,7 @@ async function requestHumanVaultConsent(request) {
20282
20480
  }
20283
20481
  let httpServer = null;
20284
20482
  let mcpAuthToken = null;
20285
- const logger$b = createLogger("MCP");
20483
+ const logger$d = createLogger("MCP");
20286
20484
  const MCP_AUTH_FILENAME = "mcp-auth.json";
20287
20485
  function getMcpAuthFilePath() {
20288
20486
  const configDir = process.env.VESSEL_CONFIG_DIR || path$1.join(
@@ -20319,7 +20517,7 @@ function writeMcpAuthFile(endpoint, token) {
20319
20517
  );
20320
20518
  fs$1.chmodSync(filePath2, 384);
20321
20519
  } catch (err) {
20322
- logger$b.warn("Failed to write auth file:", err);
20520
+ logger$d.warn("Failed to write auth file:", err);
20323
20521
  }
20324
20522
  }
20325
20523
  function clearMcpAuthFile() {
@@ -20345,7 +20543,7 @@ function clearMcpAuthFile() {
20345
20543
  );
20346
20544
  fs$1.chmodSync(filePath2, 384);
20347
20545
  } catch (err) {
20348
- logger$b.warn("Failed to clear auth file:", err);
20546
+ logger$d.warn("Failed to clear auth file:", err);
20349
20547
  }
20350
20548
  }
20351
20549
  function regenerateMcpAuthToken() {
@@ -20364,6 +20562,14 @@ function asErrorTextResponse(message) {
20364
20562
  function asNoActiveTabResponse() {
20365
20563
  return asErrorTextResponse("No active tab");
20366
20564
  }
20565
+ function getPremiumToolGateResponse(toolName) {
20566
+ try {
20567
+ assertToolUnlocked(toolName);
20568
+ return null;
20569
+ } catch (error) {
20570
+ return asTextResponse(getErrorMessage(error));
20571
+ }
20572
+ }
20367
20573
  function asPromptResponse(text) {
20368
20574
  return {
20369
20575
  messages: [
@@ -20447,7 +20653,7 @@ async function getPostActionState(tabManager, name) {
20447
20653
  }
20448
20654
  }
20449
20655
  } catch (err) {
20450
- logger$b.warn("Failed to compute post-action state warning:", err);
20656
+ logger$d.warn("Failed to compute post-action state warning:", err);
20451
20657
  }
20452
20658
  return `${warning}
20453
20659
  [state: url=${wc.getURL()}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]`;
@@ -20466,6 +20672,8 @@ async function getPostActionState(tabManager, name) {
20466
20672
  return "";
20467
20673
  }
20468
20674
  async function withAction(runtime2, tabManager, name, args, executor) {
20675
+ const premiumGate = getPremiumToolGateResponse(name);
20676
+ if (premiumGate) return premiumGate;
20469
20677
  try {
20470
20678
  const result = await runtime2.runControlledAction({
20471
20679
  source: "mcp",
@@ -20550,7 +20758,7 @@ async function waitForConditionMcp(wc, text, selector, timeoutMs) {
20550
20758
  }
20551
20759
  })()
20552
20760
  `).catch((err) => {
20553
- logger$b.warn("Failed to gather wait_for timeout diagnostic:", err);
20761
+ logger$d.warn("Failed to gather wait_for timeout diagnostic:", err);
20554
20762
  return null;
20555
20763
  });
20556
20764
  if (typeof diagnostic === "string" && diagnostic.trim()) {
@@ -20637,7 +20845,7 @@ function registerTools(server, tabManager, runtime2) {
20637
20845
  const page = await extractContent(wc);
20638
20846
  pageType = detectPageType(page);
20639
20847
  } catch (err) {
20640
- logger$b.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
20848
+ logger$d.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
20641
20849
  }
20642
20850
  }
20643
20851
  const scored = TOOL_DEFINITIONS.map((def) => {
@@ -21775,6 +21983,8 @@ ${buildScopedContext(pageContent, mode)}`;
21775
21983
  description: "Capture a screenshot of the current page. Returns a base64-encoded PNG image."
21776
21984
  },
21777
21985
  async () => {
21986
+ const premiumGate = getPremiumToolGateResponse("screenshot");
21987
+ if (premiumGate) return premiumGate;
21778
21988
  const tab = tabManager.getActiveTab();
21779
21989
  if (!tab) return asNoActiveTabResponse();
21780
21990
  try {
@@ -22035,7 +22245,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
22035
22245
  void 0,
22036
22246
  h.color
22037
22247
  ).catch(
22038
- (err) => logger$b.warn("Failed to restore highlight after removal:", err)
22248
+ (err) => logger$d.warn("Failed to restore highlight after removal:", err)
22039
22249
  );
22040
22250
  }
22041
22251
  }
@@ -22797,6 +23007,8 @@ ${JSON.stringify(otherHighlights, null, 2)}`
22797
23007
  }
22798
23008
  },
22799
23009
  async ({ goal, steps }) => {
23010
+ const premiumGate = getPremiumToolGateResponse("flow_start");
23011
+ if (premiumGate) return premiumGate;
22800
23012
  const normalizedSteps = coerceStringArray(steps) ?? [];
22801
23013
  const tab = tabManager.getActiveTab();
22802
23014
  const flow = runtime2.startFlow(
@@ -22820,6 +23032,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
22820
23032
  }
22821
23033
  },
22822
23034
  async ({ detail }) => {
23035
+ const premiumGate = getPremiumToolGateResponse("flow_advance");
23036
+ if (premiumGate) return premiumGate;
22823
23037
  const flow = runtime2.advanceFlow(detail);
22824
23038
  if (!flow) return asTextResponse("No active flow to advance");
22825
23039
  const ctx = runtime2.getFlowContext();
@@ -22833,6 +23047,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
22833
23047
  description: "Check the current workflow progress."
22834
23048
  },
22835
23049
  async () => {
23050
+ const premiumGate = getPremiumToolGateResponse("flow_status");
23051
+ if (premiumGate) return premiumGate;
22836
23052
  const flow = runtime2.getFlowState();
22837
23053
  if (!flow) return asTextResponse("No active workflow.");
22838
23054
  return asTextResponse(runtime2.getFlowContext());
@@ -22845,6 +23061,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
22845
23061
  description: "Clear the active workflow tracker."
22846
23062
  },
22847
23063
  async () => {
23064
+ const premiumGate = getPremiumToolGateResponse("flow_end");
23065
+ if (premiumGate) return premiumGate;
22848
23066
  runtime2.clearFlow();
22849
23067
  return asTextResponse("Workflow ended.");
22850
23068
  }
@@ -22883,7 +23101,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
22883
23101
  try {
22884
23102
  page = await extractContent(wc);
22885
23103
  } catch (err) {
22886
- logger$b.warn("Failed to extract page while generating suggestions:", err);
23104
+ logger$d.warn("Failed to extract page while generating suggestions:", err);
22887
23105
  return asTextResponse(
22888
23106
  "Could not read page. Try navigate to a working URL."
22889
23107
  );
@@ -23485,6 +23703,8 @@ ${JSON.stringify(tableJson, null, 2)}`;
23485
23703
  }
23486
23704
  },
23487
23705
  async ({ domain }) => {
23706
+ const premiumGate = getPremiumToolGateResponse("vault_status");
23707
+ if (premiumGate) return premiumGate;
23488
23708
  let targetDomain = domain;
23489
23709
  if (!targetDomain) {
23490
23710
  const tab = tabManager.getActiveTab();
@@ -23492,7 +23712,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
23492
23712
  try {
23493
23713
  targetDomain = new URL(tab.state.url).hostname;
23494
23714
  } catch (err) {
23495
- logger$b.warn("Failed to parse active tab URL for vault_status:", err);
23715
+ logger$d.warn("Failed to parse active tab URL for vault_status:", err);
23496
23716
  return asErrorTextResponse("Could not parse active tab URL");
23497
23717
  }
23498
23718
  }
@@ -23551,6 +23771,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23551
23771
  submit_after,
23552
23772
  submit_index
23553
23773
  }) => {
23774
+ const premiumGate = getPremiumToolGateResponse("vault_login");
23775
+ if (premiumGate) return premiumGate;
23554
23776
  const tab = tabManager.getActiveTab();
23555
23777
  if (!tab) return asNoActiveTabResponse();
23556
23778
  const wc = tab.view.webContents;
@@ -23558,7 +23780,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23558
23780
  try {
23559
23781
  hostname = new URL(tab.state.url).hostname;
23560
23782
  } catch (err) {
23561
- logger$b.warn("Failed to parse active tab URL for vault_login:", err);
23783
+ logger$d.warn("Failed to parse active tab URL for vault_login:", err);
23562
23784
  return asErrorTextResponse("Could not parse active tab URL");
23563
23785
  }
23564
23786
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -23645,6 +23867,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23645
23867
  }
23646
23868
  },
23647
23869
  async ({ credential_label, code_index, submit_after, submit_index }) => {
23870
+ const premiumGate = getPremiumToolGateResponse("vault_totp");
23871
+ if (premiumGate) return premiumGate;
23648
23872
  const tab = tabManager.getActiveTab();
23649
23873
  if (!tab) return asNoActiveTabResponse();
23650
23874
  const wc = tab.view.webContents;
@@ -23652,7 +23876,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23652
23876
  try {
23653
23877
  hostname = new URL(tab.state.url).hostname;
23654
23878
  } catch (err) {
23655
- logger$b.warn("Failed to parse active tab URL for vault_totp:", err);
23879
+ logger$d.warn("Failed to parse active tab URL for vault_totp:", err);
23656
23880
  return asErrorTextResponse("Could not parse active tab URL");
23657
23881
  }
23658
23882
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -23724,6 +23948,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23724
23948
  })
23725
23949
  },
23726
23950
  async ({ domain }) => {
23951
+ const premiumGate = getPremiumToolGateResponse("human_vault_list");
23952
+ if (premiumGate) return premiumGate;
23727
23953
  const consent = await requestHumanVaultConsent({
23728
23954
  action: "list",
23729
23955
  domain: domain ?? "all"
@@ -23774,6 +24000,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23774
24000
  })
23775
24001
  },
23776
24002
  async ({ entry_id, username_index, password_index, submit_after, submit_index }) => {
24003
+ const premiumGate = getPremiumToolGateResponse("human_vault_fill");
24004
+ if (premiumGate) return premiumGate;
23777
24005
  const tab = tabManager.getActiveTab();
23778
24006
  if (!tab) return asNoActiveTabResponse();
23779
24007
  let hostname;
@@ -23866,6 +24094,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23866
24094
  })
23867
24095
  },
23868
24096
  async ({ entry_id }) => {
24097
+ const premiumGate = getPremiumToolGateResponse("human_vault_remove");
24098
+ if (premiumGate) return premiumGate;
23869
24099
  const entry = getEntry(entry_id);
23870
24100
  if (!entry) {
23871
24101
  return asErrorTextResponse(`No entry found with ID ${entry_id}.`);
@@ -23890,6 +24120,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23890
24120
  inputSchema: zod.z.object({})
23891
24121
  },
23892
24122
  async () => {
24123
+ const premiumGate = getPremiumToolGateResponse("metrics");
24124
+ if (premiumGate) return premiumGate;
23893
24125
  const m = runtime2.getMetrics();
23894
24126
  const lines = [
23895
24127
  `Session Metrics:`,
@@ -23992,7 +24224,7 @@ function startMcpServer(tabManager, runtime2, port) {
23992
24224
  await mcpServer.connect(transport);
23993
24225
  await transport.handleRequest(req, res);
23994
24226
  } catch (error) {
23995
- logger$b.error("Error handling request:", error);
24227
+ logger$d.error("Error handling request:", error);
23996
24228
  if (!res.headersSent) {
23997
24229
  res.writeHead(500, { "Content-Type": "application/json" });
23998
24230
  res.end(
@@ -24011,7 +24243,7 @@ function startMcpServer(tabManager, runtime2, port) {
24011
24243
  };
24012
24244
  server.once("error", (error) => {
24013
24245
  const message = error.code === "EADDRINUSE" ? `Port ${port} is already in use. MCP server not started.` : error.message;
24014
- logger$b.error("Server error:", error);
24246
+ logger$d.error("Server error:", error);
24015
24247
  clearMcpAuthFile();
24016
24248
  setMcpHealth({
24017
24249
  configuredPort: port,
@@ -24043,7 +24275,7 @@ function startMcpServer(tabManager, runtime2, port) {
24043
24275
  message: `MCP server listening on ${endpoint}.`
24044
24276
  });
24045
24277
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
24046
- logger$b.info(`Server listening on ${endpoint} (auth enabled)`);
24278
+ logger$d.info(`Server listening on ${endpoint} (auth enabled)`);
24047
24279
  }
24048
24280
  if (mcpAuthToken) {
24049
24281
  writeMcpAuthFile(endpoint, mcpAuthToken);
@@ -24082,7 +24314,7 @@ function stopMcpServer() {
24082
24314
  message: "MCP server is stopped."
24083
24315
  });
24084
24316
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
24085
- logger$b.info("Server stopped");
24317
+ logger$d.info("Server stopped");
24086
24318
  }
24087
24319
  resolve();
24088
24320
  });
@@ -24103,7 +24335,7 @@ const KIT_ID_UNSAFE_CHAR_PATTERN = /[\/\\\0]/;
24103
24335
  function isSafeAutomationKitId(id) {
24104
24336
  return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
24105
24337
  }
24106
- const logger$a = createLogger("KitRegistry");
24338
+ const logger$c = createLogger("KitRegistry");
24107
24339
  function getUserKitsDir() {
24108
24340
  return path$1.join(electron.app.getPath("userData"), "kits");
24109
24341
  }
@@ -24141,10 +24373,10 @@ function getInstalledKits() {
24141
24373
  if (isValidKit(parsed)) {
24142
24374
  kits.push(parsed);
24143
24375
  } else {
24144
- logger$a.warn(`Skipping invalid kit file: ${file}`);
24376
+ logger$c.warn(`Skipping invalid kit file: ${file}`);
24145
24377
  }
24146
24378
  } catch (err) {
24147
- logger$a.warn(`Failed to read kit file: ${file}`, err);
24379
+ logger$c.warn(`Failed to read kit file: ${file}`, err);
24148
24380
  }
24149
24381
  }
24150
24382
  return kits;
@@ -24243,7 +24475,7 @@ function assertNumber(value, name) {
24243
24475
  }
24244
24476
  }
24245
24477
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
24246
- function isValidEmail(value) {
24478
+ function isValidEmail$1(value) {
24247
24479
  return EMAIL_RE.test(value.trim());
24248
24480
  }
24249
24481
  function getActiveTabInfo(tabManager) {
@@ -24253,8 +24485,10 @@ function getActiveTabInfo(tabManager) {
24253
24485
  if (wc.isDestroyed()) return null;
24254
24486
  return { tab, wc };
24255
24487
  }
24256
- const logger$9 = createLogger("Scheduler");
24488
+ const logger$b = createLogger("Scheduler");
24257
24489
  let jobs = [];
24490
+ let pollInterval = null;
24491
+ let alignStartTimeout = null;
24258
24492
  let removeIdleListener = null;
24259
24493
  let broadcastFn = null;
24260
24494
  function getScheduledKitIds() {
@@ -24284,7 +24518,7 @@ function saveJobs() {
24284
24518
  });
24285
24519
  fs$1.chmodSync(jobsPath, 384);
24286
24520
  } catch (err) {
24287
- logger$9.warn("Failed to save jobs:", err);
24521
+ logger$b.warn("Failed to save jobs:", err);
24288
24522
  }
24289
24523
  }
24290
24524
  function normalizeJob(job, now = /* @__PURE__ */ new Date()) {
@@ -24343,10 +24577,12 @@ function computeNextRun(schedule, from = /* @__PURE__ */ new Date()) {
24343
24577
  const next = new Date(from);
24344
24578
  next.setHours(schedule.hour, schedule.minute, 0, 0);
24345
24579
  const daysUntil = (schedule.dayOfWeek - next.getDay() + 7) % 7;
24346
- if (daysUntil === 0 && next <= from) {
24347
- next.setDate(next.getDate() + 7);
24580
+ if (daysUntil === 0) {
24581
+ if (next <= from) {
24582
+ next.setDate(next.getDate() + 7);
24583
+ }
24348
24584
  } else {
24349
- next.setDate(next.getDate() + (daysUntil || 7));
24585
+ next.setDate(next.getDate() + daysUntil);
24350
24586
  }
24351
24587
  return next;
24352
24588
  }
@@ -24406,7 +24642,7 @@ async function fireJob(job, windowState, runtime2) {
24406
24642
  };
24407
24643
  startActivity();
24408
24644
  if (!settings2.chatProvider) {
24409
- logger$9.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
24645
+ logger$b.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
24410
24646
  appendActivity(
24411
24647
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
24412
24648
  );
@@ -24414,7 +24650,7 @@ async function fireJob(job, windowState, runtime2) {
24414
24650
  return;
24415
24651
  }
24416
24652
  if (process.env.VESSEL_DEBUG_SCHEDULER === "1" || process.env.VESSEL_DEBUG_SCHEDULER === "true") {
24417
- logger$9.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
24653
+ logger$b.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
24418
24654
  }
24419
24655
  try {
24420
24656
  const provider = createProvider(settings2.chatProvider);
@@ -24467,24 +24703,25 @@ function tick(windowState, runtime2) {
24467
24703
  saveJobs();
24468
24704
  broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
24469
24705
  void fireJob(job, windowState, runtime2).catch((err) => {
24470
- logger$9.warn("Unexpected error firing job:", err);
24706
+ logger$b.warn("Unexpected error firing job:", err);
24471
24707
  }).finally(fireNext);
24472
24708
  };
24473
24709
  fireNext();
24474
24710
  }
24475
24711
  function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24712
+ stopScheduler();
24476
24713
  broadcastFn = sendToAll;
24477
24714
  loadJobs();
24478
24715
  if (normalizeJobs()) {
24479
24716
  saveJobs();
24480
24717
  }
24481
- removeIdleListener?.();
24482
24718
  removeIdleListener = onAIStreamIdle(() => tick(windowState, runtime2));
24483
24719
  const now = /* @__PURE__ */ new Date();
24484
24720
  const msToNextMinute = (60 - now.getSeconds()) * 1e3 - now.getMilliseconds();
24485
- setTimeout(() => {
24721
+ alignStartTimeout = setTimeout(() => {
24722
+ alignStartTimeout = null;
24486
24723
  tick(windowState, runtime2);
24487
- setInterval(() => tick(windowState, runtime2), 6e4);
24724
+ pollInterval = setInterval(() => tick(windowState, runtime2), 6e4);
24488
24725
  }, msToNextMinute);
24489
24726
  electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, (event) => {
24490
24727
  assertTrustedIpcSender(event);
@@ -24546,6 +24783,20 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24546
24783
  return true;
24547
24784
  });
24548
24785
  }
24786
+ function stopScheduler() {
24787
+ if (removeIdleListener) {
24788
+ removeIdleListener();
24789
+ removeIdleListener = null;
24790
+ }
24791
+ if (alignStartTimeout) {
24792
+ clearTimeout(alignStartTimeout);
24793
+ alignStartTimeout = null;
24794
+ }
24795
+ if (pollInterval) {
24796
+ clearInterval(pollInterval);
24797
+ pollInterval = null;
24798
+ }
24799
+ }
24549
24800
  const SAVE_DEBOUNCE_MS = 250;
24550
24801
  const PROFILE_FIELDS = [
24551
24802
  "label",
@@ -24943,6 +25194,7 @@ function registerAutofillHandlers(windowState) {
24943
25194
  }
24944
25195
  function registerPageDiffHandlers(windowState, sendToRendererViews) {
24945
25196
  const pageEventBuckets = /* @__PURE__ */ new Map();
25197
+ const isActiveWebContents = (webContentsId) => windowState.tabManager.getActiveTab()?.view.webContents.id === webContentsId;
24946
25198
  const allowPageEvent = (webContentsId) => {
24947
25199
  const now = Date.now();
24948
25200
  const bucket = pageEventBuckets.get(webContentsId);
@@ -24979,14 +25231,20 @@ function registerPageDiffHandlers(windowState, sendToRendererViews) {
24979
25231
  if (!wc || wc.isDestroyed()) return;
24980
25232
  if (!isManagedTabIpcSender(event, windowState.tabManager)) return;
24981
25233
  if (!allowPageEvent(wc.id)) return;
24982
- notePageMutationActivity(wc, sendToRendererViews);
25234
+ invalidateExtractionCache(wc);
25235
+ notePageMutationActivity(wc, sendToRendererViews, {
25236
+ isActive: () => isActiveWebContents(wc.id)
25237
+ });
24983
25238
  });
24984
25239
  electron.ipcMain.on(Channels.PAGE_DIFF_DIRTY, (event) => {
24985
25240
  const wc = event.sender;
24986
25241
  if (!wc || wc.isDestroyed()) return;
24987
25242
  if (!isManagedTabIpcSender(event, windowState.tabManager)) return;
24988
25243
  if (!allowPageEvent(wc.id)) return;
24989
- schedulePageSnapshotCapture(wc, sendToRendererViews);
25244
+ invalidateExtractionCache(wc);
25245
+ schedulePageSnapshotCapture(wc, sendToRendererViews, 0, {
25246
+ isActive: () => isActiveWebContents(wc.id)
25247
+ });
24990
25248
  });
24991
25249
  }
24992
25250
  function renderReportAsMarkdown(report, traces) {
@@ -25047,7 +25305,7 @@ function renderReportAsMarkdown(report, traces) {
25047
25305
  }
25048
25306
  return sections.join("\n");
25049
25307
  }
25050
- const logger$8 = createLogger("ResearchIPC");
25308
+ const logger$a = createLogger("ResearchIPC");
25051
25309
  function registerResearchHandlers(getOrchestrator) {
25052
25310
  electron.ipcMain.handle(Channels.RESEARCH_STATE_GET, () => {
25053
25311
  return getOrchestrator().getState();
@@ -25066,7 +25324,7 @@ function registerResearchHandlers(getOrchestrator) {
25066
25324
  await getOrchestrator().startBrief(trimmedQuery);
25067
25325
  return { accepted: true };
25068
25326
  } catch (err) {
25069
- logger$8.error("RESEARCH_START_BRIEF failed", err);
25327
+ logger$a.error("RESEARCH_START_BRIEF failed", err);
25070
25328
  return { accepted: false, reason: "error" };
25071
25329
  }
25072
25330
  }
@@ -25083,7 +25341,7 @@ function registerResearchHandlers(getOrchestrator) {
25083
25341
  orchestrator.confirmBrief();
25084
25342
  return { accepted: true };
25085
25343
  } catch (err) {
25086
- logger$8.error("RESEARCH_CONFIRM_BRIEF failed", err);
25344
+ logger$a.error("RESEARCH_CONFIRM_BRIEF failed", err);
25087
25345
  return { accepted: false, reason: "error" };
25088
25346
  }
25089
25347
  });
@@ -25104,11 +25362,11 @@ function registerResearchHandlers(getOrchestrator) {
25104
25362
  options.includeTraces
25105
25363
  );
25106
25364
  orchestrator.executeSubAgents().catch((err) => {
25107
- logger$8.error("Background sub-agent execution failed", err);
25365
+ logger$a.error("Background sub-agent execution failed", err);
25108
25366
  });
25109
25367
  return { accepted: true };
25110
25368
  } catch (err) {
25111
- logger$8.error("RESEARCH_APPROVE_OBJECTIVES failed", err);
25369
+ logger$a.error("RESEARCH_APPROVE_OBJECTIVES failed", err);
25112
25370
  return { accepted: false, reason: "error" };
25113
25371
  }
25114
25372
  }
@@ -25158,7 +25416,7 @@ function registerResearchHandlers(getOrchestrator) {
25158
25416
  await promises.writeFile(filePath2, markdown, "utf-8");
25159
25417
  return { accepted: true, savedPath: filePath2 };
25160
25418
  } catch (err) {
25161
- logger$8.error("RESEARCH_EXPORT_REPORT failed", err);
25419
+ logger$a.error("RESEARCH_EXPORT_REPORT failed", err);
25162
25420
  return { accepted: false, reason: "error" };
25163
25421
  }
25164
25422
  });
@@ -25253,11 +25511,42 @@ RULES:
25253
25511
  4. Omit empty arrays entirely (contradictions, gaps) — do not include "contradictions": [] if there are none.
25254
25512
  5. Do not use emojis.`;
25255
25513
  }
25256
- const logger$7 = createLogger("ResearchOrchestrator");
25514
+ const logger$9 = createLogger("ResearchOrchestrator");
25257
25515
  const MAX_THREADS = 5;
25516
+ const MAX_TRACE_ARGS_CHARS = 1200;
25517
+ const MAX_TRACE_RESULT_CHARS = 2e3;
25258
25518
  function clone$1(value) {
25259
25519
  return structuredClone(value);
25260
25520
  }
25521
+ function truncateTraceText(value, maxLength) {
25522
+ if (value.length <= maxLength) return value;
25523
+ return `${value.slice(0, maxLength - 3).trimEnd()}...`;
25524
+ }
25525
+ function slimTraceArgs(args) {
25526
+ const json = JSON.stringify(args);
25527
+ if (json.length <= MAX_TRACE_ARGS_CHARS) return clone$1(args);
25528
+ return {
25529
+ _truncated: true,
25530
+ originalChars: json.length,
25531
+ preview: truncateTraceText(json, MAX_TRACE_ARGS_CHARS)
25532
+ };
25533
+ }
25534
+ function slimTraceResult(result) {
25535
+ if (result.length <= MAX_TRACE_RESULT_CHARS) return result;
25536
+ return [
25537
+ `[Trace result truncated from ${result.length} chars.]`,
25538
+ truncateTraceText(result, MAX_TRACE_RESULT_CHARS)
25539
+ ].join("\n");
25540
+ }
25541
+ function createTraceToolCall(tool, args, result, startedAt) {
25542
+ return {
25543
+ tool,
25544
+ args: slimTraceArgs(args),
25545
+ result: slimTraceResult(result),
25546
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25547
+ durationMs: Date.now() - startedAt
25548
+ };
25549
+ }
25261
25550
  function normalizeSourceDomain(value) {
25262
25551
  const trimmed = value.trim().toLowerCase();
25263
25552
  if (!trimmed) return "";
@@ -25411,7 +25700,7 @@ class ResearchOrchestrator {
25411
25700
  }
25412
25701
  stopAndSynthesizeCurrentFindings() {
25413
25702
  if (this.state.phase !== "executing") {
25414
- logger$7.warn("Not executing, ignoring stopAndSynthesizeCurrentFindings");
25703
+ logger$9.warn("Not executing, ignoring stopAndSynthesizeCurrentFindings");
25415
25704
  return;
25416
25705
  }
25417
25706
  this.stopRequested = true;
@@ -25445,23 +25734,23 @@ class ResearchOrchestrator {
25445
25734
  async startBrief(userQuery) {
25446
25735
  const query = userQuery.trim();
25447
25736
  if (!query) {
25448
- logger$7.warn("Ignoring empty Research Desk query");
25737
+ logger$9.warn("Ignoring empty Research Desk query");
25449
25738
  return;
25450
25739
  }
25451
25740
  if (this.state.phase !== "idle") {
25452
- logger$7.warn("Research already in progress, ignoring startBrief");
25741
+ logger$9.warn("Research already in progress, ignoring startBrief");
25453
25742
  return;
25454
25743
  }
25455
25744
  this.state = this.initialState();
25456
25745
  this.state.originalQuery = query;
25457
25746
  this.state.startedAt = (/* @__PURE__ */ new Date()).toISOString();
25458
25747
  this.setPhase("briefing");
25459
- logger$7.info(`Brief started for query: ${query.slice(0, 120)}`);
25748
+ logger$9.info(`Brief started for query: ${query.slice(0, 120)}`);
25460
25749
  }
25461
25750
  // ── phase: briefing → planning ─────────────────────────────────
25462
25751
  confirmBrief() {
25463
25752
  if (this.state.phase !== "briefing") {
25464
- logger$7.warn("Not in briefing phase, ignoring confirmBrief");
25753
+ logger$9.warn("Not in briefing phase, ignoring confirmBrief");
25465
25754
  return;
25466
25755
  }
25467
25756
  this.setPhase("planning");
@@ -25469,7 +25758,7 @@ class ResearchOrchestrator {
25469
25758
  // ── phase: planning → awaiting_approval ────────────────────────
25470
25759
  setObjectives(objectives) {
25471
25760
  if (this.state.phase !== "planning") {
25472
- logger$7.warn("Not in planning phase, ignoring setObjectives");
25761
+ logger$9.warn("Not in planning phase, ignoring setObjectives");
25473
25762
  return;
25474
25763
  }
25475
25764
  const threads = objectives.threads.slice(0, MAX_THREADS).map(mergeBlockedSourceDomains);
@@ -25498,11 +25787,11 @@ class ResearchOrchestrator {
25498
25787
  try {
25499
25788
  const parsed = JSON.parse(json);
25500
25789
  if (typeof parsed.researchQuestion !== "string" || !parsed.researchQuestion.trim()) {
25501
- logger$7.warn("Missing researchQuestion in objectives JSON");
25790
+ logger$9.warn("Missing researchQuestion in objectives JSON");
25502
25791
  return false;
25503
25792
  }
25504
25793
  if (!Array.isArray(parsed.threads) || parsed.threads.length === 0) {
25505
- logger$7.warn("Missing or empty threads array in objectives JSON");
25794
+ logger$9.warn("Missing or empty threads array in objectives JSON");
25506
25795
  return false;
25507
25796
  }
25508
25797
  const threads = parsed.threads.map((t, i) => {
@@ -25520,7 +25809,7 @@ class ResearchOrchestrator {
25520
25809
  };
25521
25810
  }).filter((thread) => thread.question && thread.searchQueries.length > 0).slice(0, MAX_THREADS);
25522
25811
  if (threads.length === 0) {
25523
- logger$7.warn("Objectives JSON did not contain any valid research threads");
25812
+ logger$9.warn("Objectives JSON did not contain any valid research threads");
25524
25813
  return false;
25525
25814
  }
25526
25815
  const objectives = {
@@ -25531,17 +25820,17 @@ class ResearchOrchestrator {
25531
25820
  totalSourceBudget: threads.reduce((sum, t) => sum + t.sourceBudget, 0)
25532
25821
  };
25533
25822
  this.setObjectives(objectives);
25534
- logger$7.info(`Parsed ${objectives.threads.length} threads from objectives`);
25823
+ logger$9.info(`Parsed ${objectives.threads.length} threads from objectives`);
25535
25824
  return true;
25536
25825
  } catch (err) {
25537
- logger$7.warn("Failed to parse objectives JSON", err);
25826
+ logger$9.warn("Failed to parse objectives JSON", err);
25538
25827
  return false;
25539
25828
  }
25540
25829
  }
25541
25830
  // ── phase: awaiting_approval → executing ───────────────────────
25542
25831
  approveObjectives(mode, includeTraces) {
25543
25832
  if (this.state.phase !== "awaiting_approval") {
25544
- logger$7.warn("Not awaiting approval, ignoring approveObjectives");
25833
+ logger$9.warn("Not awaiting approval, ignoring approveObjectives");
25545
25834
  return;
25546
25835
  }
25547
25836
  if (mode) this.state.supervisionMode = mode;
@@ -25576,7 +25865,7 @@ class ResearchOrchestrator {
25576
25865
  this.state.threads.map((thread) => {
25577
25866
  if (this.state.phase !== "executing") return null;
25578
25867
  return this.runSubAgent(thread, tabMutex).catch((err) => {
25579
- logger$7.error(`Sub-agent "${thread.label}" failed`, err);
25868
+ logger$9.error(`Sub-agent "${thread.label}" failed`, err);
25580
25869
  return {
25581
25870
  threadLabel: thread.label,
25582
25871
  threadQuestion: thread.question,
@@ -25605,7 +25894,7 @@ class ResearchOrchestrator {
25605
25894
  try {
25606
25895
  await this.synthesizeReport();
25607
25896
  } catch (err) {
25608
- logger$7.error("Auto-synthesis failed", err);
25897
+ logger$9.error("Auto-synthesis failed", err);
25609
25898
  this.state.error = `Synthesis failed: ${String(err)}`;
25610
25899
  this.setPhase("delivered");
25611
25900
  }
@@ -25666,13 +25955,7 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25666
25955
  title: String(args.url || "excluded source"),
25667
25956
  reason: msg
25668
25957
  });
25669
- trace.toolCalls.push({
25670
- tool: name,
25671
- args,
25672
- result: msg,
25673
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25674
- durationMs: 0
25675
- });
25958
+ trace.toolCalls.push(createTraceToolCall(name, args, msg, t0));
25676
25959
  return msg;
25677
25960
  }
25678
25961
  }
@@ -25680,25 +25963,13 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25680
25963
  sourcesConsumed++;
25681
25964
  if (sourcesConsumed > thread.sourceBudget) {
25682
25965
  const msg = `Source budget (${thread.sourceBudget}) exceeded. Summarize findings and stop.`;
25683
- trace.toolCalls.push({
25684
- tool: name,
25685
- args,
25686
- result: msg,
25687
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25688
- durationMs: 0
25689
- });
25966
+ trace.toolCalls.push(createTraceToolCall(name, args, msg, t0));
25690
25967
  return msg;
25691
25968
  }
25692
25969
  }
25693
25970
  try {
25694
25971
  const output = await executeAction(name, args, actionCtx);
25695
- trace.toolCalls.push({
25696
- tool: name,
25697
- args,
25698
- result: output,
25699
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25700
- durationMs: Date.now() - t0
25701
- });
25972
+ trace.toolCalls.push(createTraceToolCall(name, args, output, t0));
25702
25973
  return output;
25703
25974
  } catch (err) {
25704
25975
  const msg = err instanceof Error ? err.message : String(err);
@@ -25713,13 +25984,7 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25713
25984
  message: msg,
25714
25985
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
25715
25986
  });
25716
- trace.toolCalls.push({
25717
- tool: name,
25718
- args,
25719
- result: `Error: ${msg}`,
25720
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25721
- durationMs: Date.now() - t0
25722
- });
25987
+ trace.toolCalls.push(createTraceToolCall(name, args, `Error: ${msg}`, t0));
25723
25988
  return `Error: ${msg}`;
25724
25989
  }
25725
25990
  },
@@ -25740,7 +26005,7 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25740
26005
  try {
25741
26006
  this.tabManager.closeTab(tabId);
25742
26007
  } catch (err) {
25743
- logger$7.warn(`Failed to close sub-agent tab ${tabId}`, err);
26008
+ logger$9.warn(`Failed to close sub-agent tab ${tabId}`, err);
25744
26009
  }
25745
26010
  }
25746
26011
  }
@@ -25749,7 +26014,7 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25749
26014
  try {
25750
26015
  claims = await this.extractClaimsFromTranscript(thread, transcript);
25751
26016
  } catch (err) {
25752
- logger$7.warn(`Claim extraction failed for "${thread.label}"`, err);
26017
+ logger$9.warn(`Claim extraction failed for "${thread.label}"`, err);
25753
26018
  }
25754
26019
  }
25755
26020
  if (this.state.phase === "executing" && this.state.includeTraces) {
@@ -25839,7 +26104,7 @@ ${transcript.slice(0, 32e3)}`;
25839
26104
  (claim) => claim.claim && claim.sourceUrl && claim.extractedQuote
25840
26105
  );
25841
26106
  } catch {
25842
- logger$7.warn(`Failed to parse claims JSON for "${thread.label}"`);
26107
+ logger$9.warn(`Failed to parse claims JSON for "${thread.label}"`);
25843
26108
  return [];
25844
26109
  }
25845
26110
  }
@@ -25924,7 +26189,7 @@ ${transcript.slice(0, 32e3)}`;
25924
26189
  objectives
25925
26190
  };
25926
26191
  } catch (err) {
25927
- logger$7.warn("Failed to parse synthesis JSON, using sourced fallback report", err);
26192
+ logger$9.warn("Failed to parse synthesis JSON, using sourced fallback report", err);
25928
26193
  return buildFallbackReport(objectives, findings, String(err));
25929
26194
  }
25930
26195
  }
@@ -25939,15 +26204,20 @@ ${transcript.slice(0, 32e3)}`;
25939
26204
  this.emit();
25940
26205
  }
25941
26206
  }
26207
+ function assertVaultUnlocked() {
26208
+ assertFeatureUnlocked("vault", "Agent Credential Vault");
26209
+ }
25942
26210
  function registerVaultHandlers() {
25943
26211
  electron.ipcMain.handle(Channels.VAULT_LIST, (event) => {
25944
26212
  assertTrustedIpcSender(event);
26213
+ assertVaultUnlocked();
25945
26214
  return listEntries$1();
25946
26215
  });
25947
26216
  electron.ipcMain.handle(
25948
26217
  Channels.VAULT_ADD,
25949
26218
  (event, entry) => {
25950
26219
  assertTrustedIpcSender(event);
26220
+ assertVaultUnlocked();
25951
26221
  if (!entry || typeof entry !== "object") {
25952
26222
  throw new Error("Invalid vault entry");
25953
26223
  }
@@ -25974,6 +26244,7 @@ function registerVaultHandlers() {
25974
26244
  Channels.VAULT_UPDATE,
25975
26245
  (event, id, updates) => {
25976
26246
  assertTrustedIpcSender(event);
26247
+ assertVaultUnlocked();
25977
26248
  assertString(id, "id");
25978
26249
  if (!updates || typeof updates !== "object") {
25979
26250
  throw new Error("Invalid updates");
@@ -25983,12 +26254,14 @@ function registerVaultHandlers() {
25983
26254
  );
25984
26255
  electron.ipcMain.handle(Channels.VAULT_REMOVE, (event, id) => {
25985
26256
  assertTrustedIpcSender(event);
26257
+ assertVaultUnlocked();
25986
26258
  assertString(id, "id");
25987
26259
  trackVaultAction("credential_removed");
25988
26260
  return removeEntry$1(id);
25989
26261
  });
25990
26262
  electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (event, limit) => {
25991
26263
  assertTrustedIpcSender(event);
26264
+ assertVaultUnlocked();
25992
26265
  return readAuditLog$1(limit);
25993
26266
  });
25994
26267
  }
@@ -26006,14 +26279,19 @@ function normalizeTags(value) {
26006
26279
  const tags = value.filter((tag) => typeof tag === "string").map((tag) => tag.trim()).filter(Boolean);
26007
26280
  return tags.length > 0 ? tags : void 0;
26008
26281
  }
26282
+ function assertHumanVaultUnlocked() {
26283
+ assertFeatureUnlocked("human_vault", "Passwords");
26284
+ }
26009
26285
  function registerHumanVaultHandlers() {
26010
26286
  electron.ipcMain.handle(Channels.HUMAN_VAULT_LIST, (event, domain) => {
26011
26287
  assertTrustedIpcSender(event);
26288
+ assertHumanVaultUnlocked();
26012
26289
  if (domain !== void 0) assertString(domain, "domain");
26013
26290
  return domain ? findForDomain(domain) : listEntries();
26014
26291
  });
26015
26292
  electron.ipcMain.handle(Channels.HUMAN_VAULT_GET, (event, id) => {
26016
26293
  assertTrustedIpcSender(event);
26294
+ assertHumanVaultUnlocked();
26017
26295
  assertString(id, "id");
26018
26296
  return getEntrySafe(id);
26019
26297
  });
@@ -26021,6 +26299,7 @@ function registerHumanVaultHandlers() {
26021
26299
  Channels.HUMAN_VAULT_SAVE,
26022
26300
  (event, input) => {
26023
26301
  assertTrustedIpcSender(event);
26302
+ assertHumanVaultUnlocked();
26024
26303
  if (!input || typeof input !== "object") {
26025
26304
  throw new Error("Invalid credential entry");
26026
26305
  }
@@ -26052,6 +26331,7 @@ function registerHumanVaultHandlers() {
26052
26331
  Channels.HUMAN_VAULT_UPDATE,
26053
26332
  (event, id, updates) => {
26054
26333
  assertTrustedIpcSender(event);
26334
+ assertHumanVaultUnlocked();
26055
26335
  assertString(id, "id");
26056
26336
  if (!updates || typeof updates !== "object") {
26057
26337
  throw new Error("Invalid updates");
@@ -26095,11 +26375,13 @@ function registerHumanVaultHandlers() {
26095
26375
  );
26096
26376
  electron.ipcMain.handle(Channels.HUMAN_VAULT_REMOVE, (event, id) => {
26097
26377
  assertTrustedIpcSender(event);
26378
+ assertHumanVaultUnlocked();
26098
26379
  assertString(id, "id");
26099
26380
  return removeEntry(id);
26100
26381
  });
26101
26382
  electron.ipcMain.handle(Channels.HUMAN_VAULT_AUDIT_LOG, (event, limit) => {
26102
26383
  assertTrustedIpcSender(event);
26384
+ assertHumanVaultUnlocked();
26103
26385
  return readAuditLog(limit);
26104
26386
  });
26105
26387
  }
@@ -26646,7 +26928,7 @@ function createFindInPageBridge(tabManager, chromeView) {
26646
26928
  }
26647
26929
  };
26648
26930
  }
26649
- const logger$6 = createLogger("PrivateWindow");
26931
+ const logger$8 = createLogger("PrivateWindow");
26650
26932
  const privateWindows = /* @__PURE__ */ new Set();
26651
26933
  function layoutPrivateViews(state2) {
26652
26934
  const { window: win, chromeView, tabManager } = state2;
@@ -26869,7 +27151,7 @@ function createPrivateWindow() {
26869
27151
  privateSession.clearStorageData(),
26870
27152
  privateSession.clearCache()
26871
27153
  ]).catch((error) => {
26872
- logger$6.warn("Failed to clear private browsing session:", error);
27154
+ logger$8.warn("Failed to clear private browsing session:", error);
26873
27155
  });
26874
27156
  });
26875
27157
  privateWindows.add(state2);
@@ -26879,7 +27161,7 @@ function createPrivateWindow() {
26879
27161
  });
26880
27162
  loadPrivateRenderer(chromeView);
26881
27163
  win.show();
26882
- logger$6.info("Private browsing window opened");
27164
+ logger$8.info("Private browsing window opened");
26883
27165
  return state2;
26884
27166
  }
26885
27167
  const secondaryWindows = /* @__PURE__ */ new Set();
@@ -27235,6 +27517,10 @@ function registerHistoryHandlers() {
27235
27517
  assertTrustedIpcSender(event);
27236
27518
  return getState$1();
27237
27519
  });
27520
+ electron.ipcMain.handle(Channels.HISTORY_LIST, (event, offset, limit) => {
27521
+ assertTrustedIpcSender(event);
27522
+ return listEntries$2(offset, limit);
27523
+ });
27238
27524
  electron.ipcMain.handle(Channels.HISTORY_SEARCH, (event, query) => {
27239
27525
  assertTrustedIpcSender(event);
27240
27526
  return search(query);
@@ -27389,7 +27675,7 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
27389
27675
  electron.ipcMain.handle(Channels.PREMIUM_ACTIVATION_START, async (event, email) => {
27390
27676
  assertTrustedIpcSender(event);
27391
27677
  assertString(email, "email");
27392
- if (!isValidEmail(email)) {
27678
+ if (!isValidEmail$1(email)) {
27393
27679
  return errorResult("Invalid email format");
27394
27680
  }
27395
27681
  trackPremiumFunnel("activation_attempted");
@@ -27416,7 +27702,7 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
27416
27702
  assertString(email, "email");
27417
27703
  assertString(code, "code");
27418
27704
  assertString(challengeToken, "challengeToken");
27419
- if (!isValidEmail(email)) {
27705
+ if (!isValidEmail$1(email)) {
27420
27706
  return errorResult("Invalid email format", {
27421
27707
  state: getPremiumState()
27422
27708
  });
@@ -27575,7 +27861,7 @@ function registerSecurityHandlers(tabManager) {
27575
27861
  tabManager.goBackToSafety(tabId);
27576
27862
  });
27577
27863
  }
27578
- const logger$5 = createLogger("CodexIPC");
27864
+ const logger$7 = createLogger("CodexIPC");
27579
27865
  function registerCodexHandlers() {
27580
27866
  electron.ipcMain.handle(Channels.CODEX_START_AUTH, async (event) => {
27581
27867
  assertTrustedIpcSender(event);
@@ -27590,7 +27876,7 @@ function registerCodexHandlers() {
27590
27876
  try {
27591
27877
  wc.send(Channels.CODEX_AUTH_STATUS, { status, error: error || null });
27592
27878
  } catch {
27593
- logger$5.warn("Codex auth status send failed — window may be closed");
27879
+ logger$7.warn("Codex auth status send failed — window may be closed");
27594
27880
  }
27595
27881
  };
27596
27882
  try {
@@ -27602,7 +27888,7 @@ function registerCodexHandlers() {
27602
27888
  accountId: tokens.accountId
27603
27889
  };
27604
27890
  } catch (err) {
27605
- logger$5.error("Codex auth failed:", err);
27891
+ logger$7.error("Codex auth failed:", err);
27606
27892
  return {
27607
27893
  ok: false,
27608
27894
  error: err instanceof Error ? err.message : "Unknown error"
@@ -27620,6 +27906,160 @@ function registerCodexHandlers() {
27620
27906
  return { ok: true };
27621
27907
  });
27622
27908
  }
27909
+ const logger$6 = createLogger("OpenRouterOAuth");
27910
+ const AUTH_BASE_URL = "https://openrouter.ai/auth";
27911
+ const KEY_EXCHANGE_URL = "https://openrouter.ai/api/v1/auth/keys";
27912
+ const AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
27913
+ const PREFERRED_PORT = 1460;
27914
+ const FALLBACK_PORT = 1461;
27915
+ async function exchangeCodeForApiKey(code, codeVerifier) {
27916
+ const response = await fetch(KEY_EXCHANGE_URL, {
27917
+ method: "POST",
27918
+ headers: {
27919
+ "Content-Type": "application/json"
27920
+ },
27921
+ body: JSON.stringify({
27922
+ code,
27923
+ code_verifier: codeVerifier,
27924
+ code_challenge_method: "S256"
27925
+ })
27926
+ });
27927
+ if (!response.ok) {
27928
+ let errorMsg = `OpenRouter key exchange failed: ${response.status}`;
27929
+ try {
27930
+ const payload2 = await response.json();
27931
+ if (typeof payload2.error === "string") {
27932
+ errorMsg = payload2.error;
27933
+ } else if (typeof payload2.message === "string") {
27934
+ errorMsg = payload2.message;
27935
+ }
27936
+ } catch {
27937
+ }
27938
+ throw new Error(errorMsg);
27939
+ }
27940
+ const payload = await response.json();
27941
+ if (typeof payload.key !== "string" || !payload.key.trim()) {
27942
+ throw new Error("OpenRouter did not return an API key");
27943
+ }
27944
+ return payload.key.trim();
27945
+ }
27946
+ const openRouterOAuth = createLocalPkceOAuthFlow({
27947
+ name: "OpenRouter",
27948
+ logger: logger$6,
27949
+ preferredPorts: [PREFERRED_PORT, FALLBACK_PORT],
27950
+ timeoutMs: AUTH_TIMEOUT_MS,
27951
+ callbackPath: (state2) => `/auth/openrouter/callback/${state2}`,
27952
+ readState: (url) => decodeURIComponent(url.pathname.split("/").pop() || ""),
27953
+ buildAuthorizeUrl: ({ callbackUrl, pkce }) => {
27954
+ const params = new URLSearchParams({
27955
+ callback_url: callbackUrl,
27956
+ code_challenge: pkce.codeChallenge,
27957
+ code_challenge_method: "S256"
27958
+ });
27959
+ return `${AUTH_BASE_URL}?${params.toString()}`;
27960
+ },
27961
+ exchangeCode: ({ code, codeVerifier }) => exchangeCodeForApiKey(code, codeVerifier),
27962
+ successHtml: () => `<!DOCTYPE html>
27963
+ <html><head><meta charset="utf-8"><title>Vessel OpenRouter Setup</title>
27964
+ <style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#111;color:#eee}</style></head>
27965
+ <body><div style="text-align:center"><h1>OpenRouter connected</h1><p>Vessel is ready. You can close this tab.</p></div></body></html>`,
27966
+ openHosts: ["openrouter.ai"]
27967
+ });
27968
+ function startOpenRouterOAuth(onStatus) {
27969
+ return openRouterOAuth.start(onStatus);
27970
+ }
27971
+ function cancelOpenRouterOAuth() {
27972
+ openRouterOAuth.cancel();
27973
+ }
27974
+ const logger$5 = createLogger("OpenRouterIPC");
27975
+ function registerOpenRouterHandlers(applySettingChange) {
27976
+ electron.ipcMain.handle(Channels.OPENROUTER_START_AUTH, async (event) => {
27977
+ assertTrustedIpcSender(event);
27978
+ const wc = event.sender;
27979
+ if (!wc || wc.isDestroyed()) {
27980
+ return {
27981
+ ok: false,
27982
+ error: "No active window found for sender"
27983
+ };
27984
+ }
27985
+ const sendStatus = (status, error) => {
27986
+ try {
27987
+ wc.send(Channels.OPENROUTER_AUTH_STATUS, { status, error: error || null });
27988
+ } catch {
27989
+ logger$5.warn("OpenRouter auth status send failed - window may be closed");
27990
+ }
27991
+ };
27992
+ try {
27993
+ const apiKey = await startOpenRouterOAuth(sendStatus);
27994
+ const openRouterConfig = {
27995
+ id: "openrouter",
27996
+ apiKey,
27997
+ hasApiKey: true,
27998
+ model: PROVIDERS.openrouter.defaultModel,
27999
+ baseUrl: PROVIDERS.openrouter.defaultBaseUrl,
28000
+ reasoningEffort: "off"
28001
+ };
28002
+ await applySettingChange("chatProvider", openRouterConfig);
28003
+ return {
28004
+ ok: true,
28005
+ providerId: "openrouter",
28006
+ model: openRouterConfig.model
28007
+ };
28008
+ } catch (err) {
28009
+ logger$5.error("OpenRouter auth failed:", err);
28010
+ return {
28011
+ ok: false,
28012
+ error: err instanceof Error ? err.message : "Unknown error"
28013
+ };
28014
+ }
28015
+ });
28016
+ electron.ipcMain.handle(Channels.OPENROUTER_CANCEL_AUTH, (event) => {
28017
+ assertTrustedIpcSender(event);
28018
+ cancelOpenRouterOAuth();
28019
+ return { ok: true };
28020
+ });
28021
+ }
28022
+ const SUPPORT_API = process.env.VESSEL_SUPPORT_API || process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
28023
+ const MAX_FEEDBACK_MESSAGE_LENGTH = 5e3;
28024
+ const FEEDBACK_REQUEST_TIMEOUT_MS = 15e3;
28025
+ function isValidEmail(email) {
28026
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
28027
+ }
28028
+ async function submitFeedback(payload) {
28029
+ const email = payload.email.trim().toLowerCase();
28030
+ const message = payload.message.trim();
28031
+ if (!isValidEmail(email)) {
28032
+ return errorResult("Enter a valid reply email.");
28033
+ }
28034
+ if (!message) {
28035
+ return errorResult("Write a feedback message before sending.");
28036
+ }
28037
+ if (message.length > MAX_FEEDBACK_MESSAGE_LENGTH) {
28038
+ return errorResult(
28039
+ `Feedback must be ${MAX_FEEDBACK_MESSAGE_LENGTH.toLocaleString()} characters or less.`
28040
+ );
28041
+ }
28042
+ try {
28043
+ const signal = AbortSignal.timeout(FEEDBACK_REQUEST_TIMEOUT_MS);
28044
+ const res = await fetch(`${SUPPORT_API}/feedback`, {
28045
+ method: "POST",
28046
+ headers: { "Content-Type": "application/json" },
28047
+ signal,
28048
+ body: JSON.stringify({
28049
+ email,
28050
+ message,
28051
+ source: payload.source
28052
+ })
28053
+ });
28054
+ const data = await res.json().catch(() => ({}));
28055
+ if (!res.ok) {
28056
+ return errorResult(data.error || `HTTP ${res.status}`);
28057
+ }
28058
+ return okResult();
28059
+ } catch (error) {
28060
+ return errorResult(getErrorMessage(error, "Failed to send feedback."));
28061
+ }
28062
+ }
27623
28063
  const filePath = () => path$1.join(electron.app.getPath("userData"), "vessel-permissions.json");
27624
28064
  const ALLOWED_PERMISSION_TYPES = /* @__PURE__ */ new Set([
27625
28065
  "clipboard-read",
@@ -27948,6 +28388,26 @@ function registerIpcHandlers(windowState, runtime2) {
27948
28388
  const count = await getActiveHighlightCountSafe();
27949
28389
  sendToRendererViews(Channels.HIGHLIGHT_COUNT_UPDATE, count);
27950
28390
  };
28391
+ const applySettingChange = async (key2, value) => {
28392
+ const updatedSettings = setSetting(key2, value);
28393
+ trackSettingChanged(key2);
28394
+ if (key2 === "approvalMode") {
28395
+ runtime2.setApprovalMode(value);
28396
+ }
28397
+ if (key2 === "mcpPort") {
28398
+ await stopMcpServer();
28399
+ await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
28400
+ }
28401
+ if (key2 === "chatProvider" && researchOrchestrator) {
28402
+ try {
28403
+ researchOrchestrator.setProvider(createProvider(value));
28404
+ } catch {
28405
+ }
28406
+ }
28407
+ const rendererSettings = getRendererSettings();
28408
+ sendToRendererViews(Channels.SETTINGS_UPDATE, rendererSettings);
28409
+ return rendererSettings;
28410
+ };
27951
28411
  runtime2.setUpdateListener((state2) => {
27952
28412
  scheduleRuntimeUpdate(state2);
27953
28413
  });
@@ -28133,7 +28593,7 @@ function registerIpcHandlers(windowState, runtime2) {
28133
28593
  () => sendToRendererViews(Channels.AI_STREAM_END, "completed"),
28134
28594
  tabManager,
28135
28595
  runtime2,
28136
- history,
28596
+ compactProviderHistory(history),
28137
28597
  researchOrchestrator
28138
28598
  );
28139
28599
  } catch (err) {
@@ -28285,6 +28745,12 @@ function registerIpcHandlers(windowState, runtime2) {
28285
28745
  requireTrusted(event);
28286
28746
  return regenerateMcpAuthToken();
28287
28747
  });
28748
+ electron.ipcMain.handle(Channels.SUPPORT_SUBMIT_FEEDBACK, async (event, email, message) => {
28749
+ requireTrusted(event);
28750
+ assertString(email, "email");
28751
+ assertString(message, "message");
28752
+ return submitFeedback({ email, message, source: "settings_account" });
28753
+ });
28288
28754
  electron.ipcMain.handle(Channels.SETTINGS_SET, async (event, key2, value) => {
28289
28755
  requireTrusted(event);
28290
28756
  assertString(key2, "key");
@@ -28292,24 +28758,7 @@ function registerIpcHandlers(windowState, runtime2) {
28292
28758
  throw new Error(`Unknown setting key: ${key2}`);
28293
28759
  }
28294
28760
  const settingsKey = key2;
28295
- const updatedSettings = setSetting(settingsKey, value);
28296
- trackSettingChanged(key2);
28297
- if (key2 === "approvalMode") {
28298
- runtime2.setApprovalMode(value);
28299
- }
28300
- if (key2 === "mcpPort") {
28301
- await stopMcpServer();
28302
- await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
28303
- }
28304
- if (key2 === "chatProvider" && researchOrchestrator) {
28305
- try {
28306
- researchOrchestrator.setProvider(createProvider(value));
28307
- } catch {
28308
- }
28309
- }
28310
- const rendererSettings = getRendererSettings();
28311
- sendToRendererViews(Channels.SETTINGS_UPDATE, rendererSettings);
28312
- return rendererSettings;
28761
+ return applySettingChange(settingsKey, value);
28313
28762
  });
28314
28763
  electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, (event) => {
28315
28764
  requireTrusted(event);
@@ -28498,6 +28947,7 @@ function registerIpcHandlers(windowState, runtime2) {
28498
28947
  registerHumanVaultHandlers();
28499
28948
  registerWindowControlHandlers(mainWindow);
28500
28949
  registerCodexHandlers();
28950
+ registerOpenRouterHandlers(applySettingChange);
28501
28951
  electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, (event) => {
28502
28952
  requireTrusted(event);
28503
28953
  return getInstalledKits();
@@ -29926,7 +30376,7 @@ async function bootstrap() {
29926
30376
  );
29927
30377
  }
29928
30378
  };
29929
- const windowState = createMainWindow((tabs, activeId) => {
30379
+ const windowState = createMainWindow((tabs, activeId, meta) => {
29930
30380
  windowState.chromeView.webContents.send(
29931
30381
  Channels.TAB_STATE_UPDATE,
29932
30382
  tabs,
@@ -29934,7 +30384,9 @@ async function bootstrap() {
29934
30384
  );
29935
30385
  void syncActiveHighlightCount(windowState);
29936
30386
  layoutViews(windowState);
29937
- runtime?.onTabStateChanged();
30387
+ if (meta.persistSession) {
30388
+ runtime?.onTabStateChanged();
30389
+ }
29938
30390
  });
29939
30391
  let didRevealMainWindow = false;
29940
30392
  const revealMainWindow = () => {