@quanta-intellect/vessel-browser 0.1.103 → 0.1.114

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
@@ -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 () => {
@@ -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) {
@@ -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() {
@@ -3358,9 +3384,12 @@ class TabManager {
3358
3384
  (err) => logger$m.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",
@@ -3596,7 +3628,7 @@ const MAX_DIFF_BLOCKS = 500;
3596
3628
  function normalizeText$2(value) {
3597
3629
  return value.replace(/\s+/g, " ").trim();
3598
3630
  }
3599
- function truncateText(value, max = 180) {
3631
+ function truncateText$1(value, max = 180) {
3600
3632
  const normalized = normalizeText$2(value);
3601
3633
  if (normalized.length <= max) return normalized;
3602
3634
  return `${normalized.slice(0, max - 3)}...`;
@@ -3770,10 +3802,10 @@ function diffSnapshots(oldSnap, currentContent, currentTitle, currentHeadings) {
3770
3802
  addedBlocks.length,
3771
3803
  removedBlocks.length
3772
3804
  ),
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))
3805
+ before: changedPairs[0] ? truncateText$1(changedPairs[0].before) : removedBlocks[0] ? truncateText$1(removedBlocks[0]) : void 0,
3806
+ after: changedPairs[0] ? truncateText$1(changedPairs[0].after) : addedBlocks[0] ? truncateText$1(addedBlocks[0]) : void 0,
3807
+ addedItems: addedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText$1(item)),
3808
+ removedItems: removedBlocks.slice(0, MAX_DETAIL_ITEMS).map((item) => truncateText$1(item))
3777
3809
  });
3778
3810
  }
3779
3811
  }
@@ -4591,18 +4623,28 @@ const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
4591
4623
  "research_approve_objectives",
4592
4624
  "research_export_report"
4593
4625
  ]);
4626
+ const PREMIUM_FEATURES = /* @__PURE__ */ new Set([
4627
+ "obsidian",
4628
+ "devtools",
4629
+ "unlimited_iterations",
4630
+ "vault",
4631
+ "human_vault",
4632
+ "automation_kits",
4633
+ "research"
4634
+ ]);
4594
4635
  function isPremium() {
4595
4636
  const { premium } = loadSettings();
4596
- if (premium.status === "active" || premium.status === "trialing") {
4637
+ if (premium.status !== "active" && premium.status !== "trialing") {
4638
+ return false;
4639
+ }
4640
+ if (!premium.validatedAt) {
4597
4641
  return true;
4598
4642
  }
4599
- if (premium.validatedAt && premium.status !== "free") {
4600
- const lastValidated = new Date(premium.validatedAt).getTime();
4601
- if (Date.now() - lastValidated < OFFLINE_GRACE_PERIOD_MS) {
4602
- return true;
4603
- }
4643
+ const lastValidated = new Date(premium.validatedAt).getTime();
4644
+ if (!Number.isFinite(lastValidated)) {
4645
+ return false;
4604
4646
  }
4605
- return false;
4647
+ return Date.now() - lastValidated < OFFLINE_GRACE_PERIOD_MS;
4606
4648
  }
4607
4649
  function getPremiumState() {
4608
4650
  return { ...loadSettings().premium };
@@ -4628,6 +4670,22 @@ function resetPremium() {
4628
4670
  function isToolGated(toolName) {
4629
4671
  return PREMIUM_TOOLS.has(toolName) && !isPremium();
4630
4672
  }
4673
+ function isFeatureGated(featureName) {
4674
+ return PREMIUM_FEATURES.has(featureName) && !isPremium();
4675
+ }
4676
+ function getPremiumToolGateMessage(toolName) {
4677
+ return `This tool (${toolName}) requires Vessel Premium. Upgrade at Settings > Premium to unlock screenshot, session management, workflow tracking, and more.`;
4678
+ }
4679
+ function assertToolUnlocked(toolName) {
4680
+ if (isToolGated(toolName)) {
4681
+ throw new Error(getPremiumToolGateMessage(toolName));
4682
+ }
4683
+ }
4684
+ function assertFeatureUnlocked(featureName, featureLabel = featureName) {
4685
+ if (isFeatureGated(featureName)) {
4686
+ throw new Error(`${featureLabel} requires Vessel Premium.`);
4687
+ }
4688
+ }
4631
4689
  async function getCheckoutUrl(email) {
4632
4690
  try {
4633
4691
  const params = new URLSearchParams();
@@ -4647,9 +4705,31 @@ async function getCheckoutUrl(email) {
4647
4705
  }
4648
4706
  }
4649
4707
  async function getPortalUrl() {
4650
- return errorResult(
4651
- "Billing portal access is temporarily disabled until authenticated customer access is implemented."
4652
- );
4708
+ const current = loadSettings().premium;
4709
+ const identifier = current.verificationToken;
4710
+ if (!identifier) {
4711
+ return errorResult(
4712
+ "Verify your Premium subscription before opening billing management."
4713
+ );
4714
+ }
4715
+ try {
4716
+ const res = await fetch(`${VERIFICATION_API}/portal`, {
4717
+ method: "POST",
4718
+ headers: { "Content-Type": "application/json" },
4719
+ body: JSON.stringify({ identifier })
4720
+ });
4721
+ if (!res.ok) {
4722
+ const detail = await readApiErrorDetail(res);
4723
+ return errorResult(detail || `HTTP ${res.status}`);
4724
+ }
4725
+ const { url } = await res.json();
4726
+ if (typeof url !== "string" || !url.trim()) {
4727
+ return errorResult("Billing portal did not return a valid URL.");
4728
+ }
4729
+ return okResult({ url });
4730
+ } catch (err) {
4731
+ return errorResult(getErrorMessage(err, "Failed to open billing portal"));
4732
+ }
4653
4733
  }
4654
4734
  async function verifySubscription$1(identifier) {
4655
4735
  const current = loadSettings().premium;
@@ -5297,6 +5377,17 @@ const MUTATION_CAPTURE_INTERVAL_MS = 5e3;
5297
5377
  const MUTATION_SETTLE_AFTER_MS = 1500;
5298
5378
  const AGENT_STREAM_IDLE_TIMEOUT_MS = 3e4;
5299
5379
  const logger$k = createLogger("Extractor");
5380
+ const EXTRACTION_CACHE_TTL_MS = 1500;
5381
+ const MAX_EXTRACTION_CACHE_ENTRIES = 50;
5382
+ const extractionCache = /* @__PURE__ */ new Map();
5383
+ function invalidateExtractionCache(webContents) {
5384
+ const prefix = `${webContents.id}:`;
5385
+ for (const key2 of extractionCache.keys()) {
5386
+ if (key2.startsWith(prefix)) {
5387
+ extractionCache.delete(key2);
5388
+ }
5389
+ }
5390
+ }
5300
5391
  const EMPTY_PAGE_CONTENT = {
5301
5392
  title: "",
5302
5393
  content: "",
@@ -6183,9 +6274,14 @@ async function extractContentInner(webContents) {
6183
6274
  );
6184
6275
  }
6185
6276
  async function extractContent(webContents) {
6277
+ const cacheKey = `${webContents.id}:${webContents.getURL() || ""}`;
6278
+ const cached = extractionCache.get(cacheKey);
6279
+ if (cached && Date.now() - cached.capturedAt < EXTRACTION_CACHE_TTL_MS) {
6280
+ return structuredClone(cached.content);
6281
+ }
6186
6282
  try {
6187
6283
  const timeoutMs = await estimateExtractionTimeout(webContents);
6188
- return await Promise.race([
6284
+ const content = await Promise.race([
6189
6285
  extractContentInner(webContents),
6190
6286
  new Promise(
6191
6287
  (_, reject) => setTimeout(
@@ -6194,6 +6290,15 @@ async function extractContent(webContents) {
6194
6290
  )
6195
6291
  )
6196
6292
  ]);
6293
+ extractionCache.set(cacheKey, {
6294
+ capturedAt: Date.now(),
6295
+ content: structuredClone(content)
6296
+ });
6297
+ if (extractionCache.size > MAX_EXTRACTION_CACHE_ENTRIES) {
6298
+ const oldestKey = extractionCache.keys().next().value;
6299
+ if (oldestKey) extractionCache.delete(oldestKey);
6300
+ }
6301
+ return content;
6197
6302
  } catch (err) {
6198
6303
  const url = webContents.getURL() || "";
6199
6304
  let domain = "unknown";
@@ -6288,6 +6393,7 @@ function attachDestroyCleanup(wc) {
6288
6393
  const MAX_PERSISTED_DIFF_BURSTS = 50;
6289
6394
  const MAX_HISTORY_DAYS = 30;
6290
6395
  const SAVE_DEBOUNCE_MS$2 = 500;
6396
+ const BACKGROUND_DIFF_CAPTURE_DELAY_MS = 15e3;
6291
6397
  function getHistoryFilePath() {
6292
6398
  return path.join(electron.app.getPath("userData"), "vessel-page-diff-history.json");
6293
6399
  }
@@ -6420,7 +6526,7 @@ function computeNextSnapshotDueAt(wcId, now, delayMs) {
6420
6526
  const stableAfterActivityAt = lastActivityAt ? lastActivityAt + MUTATION_SETTLE_AFTER_MS : 0;
6421
6527
  return Math.max(now + delayMs, earliestAllowedAt, stableAfterActivityAt);
6422
6528
  }
6423
- function scheduleTimerAt(wc, sendToRendererViews, dueAt) {
6529
+ function scheduleTimerAt(wc, sendToRendererViews, dueAt, options = {}) {
6424
6530
  attachDestroyCleanup(wc);
6425
6531
  const wcId = wc.id;
6426
6532
  const existing = pendingPageSnapshotTimers.get(wcId);
@@ -6428,14 +6534,24 @@ function scheduleTimerAt(wc, sendToRendererViews, dueAt) {
6428
6534
  const timer = setTimeout(() => {
6429
6535
  cleanupTimersForWcId(wcId);
6430
6536
  if (wc.isDestroyed()) return;
6537
+ if (options.isActive && !options.isActive()) {
6538
+ scheduleTimerAt(
6539
+ wc,
6540
+ sendToRendererViews,
6541
+ Date.now() + BACKGROUND_DIFF_CAPTURE_DELAY_MS,
6542
+ options
6543
+ );
6544
+ return;
6545
+ }
6431
6546
  lastMutationSnapshotAt.set(wcId, Date.now());
6432
6547
  void capturePageSnapshot(wc.getURL(), wc, sendToRendererViews);
6433
6548
  }, Math.max(0, dueAt - Date.now()));
6434
6549
  pendingPageSnapshotTimers.set(wcId, timer);
6435
6550
  pendingPageSnapshotDueAt.set(wcId, dueAt);
6436
6551
  }
6437
- function notePageMutationActivity(wc, sendToRendererViews) {
6552
+ function notePageMutationActivity(wc, sendToRendererViews, options = {}) {
6438
6553
  if (wc.isDestroyed()) return;
6554
+ if (options.isActive && !options.isActive()) return;
6439
6555
  const wcId = wc.id;
6440
6556
  const now = Date.now();
6441
6557
  lastMutationActivityAt.set(wcId, now);
@@ -6443,18 +6559,19 @@ function notePageMutationActivity(wc, sendToRendererViews) {
6443
6559
  if (existingDueAt == null) return;
6444
6560
  const nextDueAt = computeNextSnapshotDueAt(wcId, now, 0);
6445
6561
  if (nextDueAt <= existingDueAt) return;
6446
- scheduleTimerAt(wc, sendToRendererViews, nextDueAt);
6562
+ scheduleTimerAt(wc, sendToRendererViews, nextDueAt, options);
6447
6563
  }
6448
- function schedulePageSnapshotCapture(wc, sendToRendererViews, delayMs = 0) {
6564
+ function schedulePageSnapshotCapture(wc, sendToRendererViews, delayMs = 0, options = {}) {
6449
6565
  if (wc.isDestroyed()) return;
6450
6566
  const wcId = wc.id;
6451
6567
  const now = Date.now();
6452
- const nextDueAt = computeNextSnapshotDueAt(wcId, now, delayMs);
6568
+ const effectiveDelayMs = options.isActive && !options.isActive() ? Math.max(delayMs, BACKGROUND_DIFF_CAPTURE_DELAY_MS) : delayMs;
6569
+ const nextDueAt = computeNextSnapshotDueAt(wcId, now, effectiveDelayMs);
6453
6570
  const existingDueAt = pendingPageSnapshotDueAt.get(wcId);
6454
6571
  if (existingDueAt != null && existingDueAt >= nextDueAt) {
6455
6572
  return;
6456
6573
  }
6457
- scheduleTimerAt(wc, sendToRendererViews, nextDueAt);
6574
+ scheduleTimerAt(wc, sendToRendererViews, nextDueAt, options);
6458
6575
  }
6459
6576
  function enableClipboardShortcuts(view) {
6460
6577
  view.webContents.on("before-input-event", (event, input) => {
@@ -6670,6 +6787,8 @@ function createMainWindow(onTabStateChange) {
6670
6787
  sidebarView.webContents.send(channel, ...args);
6671
6788
  };
6672
6789
  tabManager.onPageLoad((url, wc) => {
6790
+ const activeWc = tabManager.getActiveTab()?.view.webContents;
6791
+ if (activeWc?.id !== wc.id) return;
6673
6792
  void capturePageSnapshot(url, wc, sendToRendererViews);
6674
6793
  });
6675
6794
  const state2 = {
@@ -7436,6 +7555,36 @@ const PROVIDERS = {
7436
7555
  apiKeyHint: "Optional — only if your endpoint requires authentication"
7437
7556
  }
7438
7557
  };
7558
+ function parseModelSizeInBillions(model) {
7559
+ const match = model.toLowerCase().match(/(?:^|[:/_\-\s])(\d+(?:\.\d+)?)b(?:$|[:/_\-\s])/i);
7560
+ if (!match) return null;
7561
+ const parsed = Number(match[1]);
7562
+ return Number.isFinite(parsed) ? parsed : null;
7563
+ }
7564
+ function isLoopbackBaseUrl(baseUrl) {
7565
+ if (!baseUrl) return false;
7566
+ try {
7567
+ const url = new URL(baseUrl);
7568
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
7569
+ } catch {
7570
+ return false;
7571
+ }
7572
+ }
7573
+ function resolveAgentToolProfile(config) {
7574
+ const providerId = config.id;
7575
+ const isLocalProvider = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
7576
+ if (!isLocalProvider) return "default";
7577
+ const sizeInBillions = parseModelSizeInBillions(config.model);
7578
+ if (sizeInBillions === null) {
7579
+ return "compact";
7580
+ }
7581
+ return sizeInBillions <= 14 ? "compact" : "default";
7582
+ }
7583
+ const MAX_CONTEXT_CONTENT_LENGTH = 6e4;
7584
+ const MAX_MCP_NAV_CONTENT_LENGTH = 3e4;
7585
+ const MAX_AGENT_DEBUG_CONTENT_LENGTH = 2e4;
7586
+ const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
7587
+ const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
7439
7588
  const SAFE_TOOL_ALIASES = {
7440
7589
  goto_url: "navigate",
7441
7590
  go_to_url: "navigate",
@@ -7500,407 +7649,117 @@ function normalizeToolAlias(name) {
7500
7649
  }
7501
7650
  return name;
7502
7651
  }
7503
- function parseModelSizeInBillions(model) {
7504
- const match = model.toLowerCase().match(/(?:^|[:/_\-\s])(\d+(?:\.\d+)?)b(?:$|[:/_\-\s])/i);
7505
- if (!match) return null;
7506
- const parsed = Number(match[1]);
7507
- return Number.isFinite(parsed) ? parsed : null;
7652
+ function stableToolSignature(name, args) {
7653
+ const canonicalArgs = canonicalizeArgsForTool(name, args);
7654
+ const sortedEntries = Object.entries(canonicalArgs).sort(
7655
+ ([left], [right]) => left.localeCompare(right)
7656
+ );
7657
+ return JSON.stringify([name, sortedEntries]);
7508
7658
  }
7509
- function isLoopbackBaseUrl(baseUrl) {
7510
- if (!baseUrl) return false;
7659
+ function normalizeToolToken(value) {
7660
+ return value.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
7661
+ }
7662
+ function canonicalizeUrlLike(value) {
7511
7663
  try {
7512
- const url = new URL(baseUrl);
7513
- return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
7664
+ const url = new URL(value.trim());
7665
+ if (url.protocol === "http:" || url.protocol === "https:") {
7666
+ url.hostname = url.hostname.replace(/^www\./, "");
7667
+ url.hash = "";
7668
+ if (url.pathname.endsWith("/") && url.pathname !== "/") {
7669
+ url.pathname = url.pathname.replace(/\/+$/, "");
7670
+ }
7671
+ return url.toString();
7672
+ }
7514
7673
  } catch {
7515
- return false;
7516
7674
  }
7675
+ return value.trim();
7517
7676
  }
7518
- function resolveAgentToolProfile(config) {
7519
- const providerId = config.id;
7520
- const isLocalProvider = providerId === "ollama" || providerId === "llama_cpp" || providerId === "custom" && isLoopbackBaseUrl(config.baseUrl);
7521
- if (!isLocalProvider) return "default";
7522
- const sizeInBillions = parseModelSizeInBillions(config.model);
7523
- if (sizeInBillions === null) {
7524
- return "compact";
7677
+ function toLikelyUrl(value) {
7678
+ const trimmed = value.trim().replace(/^["']|["']$/g, "");
7679
+ if (!trimmed) return null;
7680
+ if (/^https?:\/\//i.test(trimmed)) return trimmed;
7681
+ if (/^[a-z0-9-]+\.(com|org|net|io|dev|app|ai|co|edu|gov)(\/\S*)?$/i.test(trimmed)) {
7682
+ return `https://${trimmed}`;
7525
7683
  }
7526
- return sizeInBillions <= 14 ? "compact" : "default";
7527
- }
7528
- const MAX_CONTEXT_CONTENT_LENGTH = 6e4;
7529
- const MAX_MCP_NAV_CONTENT_LENGTH = 3e4;
7530
- const MAX_AGENT_DEBUG_CONTENT_LENGTH = 2e4;
7531
- const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
7532
- const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
7533
- const logger$j = createLogger("OpenAIProvider");
7534
- function shouldDebugAgentLoop() {
7535
- const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
7536
- return value === "1" || value === "true";
7537
- }
7538
- function previewDebugValue(value, maxLength = 800) {
7539
- const normalized = value.replace(/\s+/g, " ").trim();
7540
- if (normalized.length <= maxLength) return normalized;
7541
- return `${normalized.slice(0, maxLength)}…`;
7684
+ return null;
7542
7685
  }
7543
- function previewToolDebugContent(content) {
7544
- return previewDebugValue(content, 500);
7686
+ function scalarArgsForTool(name, scalar) {
7687
+ const trimmed = scalar.trim();
7688
+ if (!trimmed) return null;
7689
+ if (name === "navigate") {
7690
+ const url = toLikelyUrl(trimmed);
7691
+ return url ? { url } : null;
7692
+ }
7693
+ if (name === "search") {
7694
+ return { query: trimmed.replace(/^["']|["']$/g, "") };
7695
+ }
7696
+ if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
7697
+ return { text: trimmed.replace(/^["']|["']$/g, "") };
7698
+ }
7699
+ if (name === "read_page") {
7700
+ const mode = trimmed.replace(/^["']|["']$/g, "").toLowerCase();
7701
+ if (mode) return { mode };
7702
+ }
7703
+ if (name === "save_bookmark") {
7704
+ const url = toLikelyUrl(trimmed);
7705
+ if (url) return { url };
7706
+ const lastSpace = trimmed.lastIndexOf(" ");
7707
+ if (lastSpace > 0) {
7708
+ const maybeUrl = toLikelyUrl(trimmed.slice(lastSpace + 1));
7709
+ if (maybeUrl) {
7710
+ return {
7711
+ url: maybeUrl,
7712
+ title: trimmed.slice(0, lastSpace).replace(/^["']|["']$/g, "")
7713
+ };
7714
+ }
7715
+ }
7716
+ }
7717
+ return null;
7545
7718
  }
7546
- function toOpenAITools(tools) {
7547
- return tools.map((t) => ({
7548
- type: "function",
7549
- function: {
7550
- name: t.name,
7551
- description: t.description ?? "",
7552
- parameters: t.input_schema
7719
+ function firstStringArg(args, keys) {
7720
+ for (const key2 of keys) {
7721
+ const value = args[key2];
7722
+ if (typeof value === "string" && value.trim()) {
7723
+ return value.trim();
7553
7724
  }
7554
- }));
7725
+ }
7726
+ return null;
7555
7727
  }
7556
- function agentTemperatureForProfile(profile) {
7557
- return profile === "compact" ? 0.2 : void 0;
7728
+ function normalizeElementTargetArgs(args) {
7729
+ const normalized = { ...args };
7730
+ if (typeof normalized.index === "string" && /^\d+$/.test(normalized.index.trim())) {
7731
+ normalized.index = Number(normalized.index.trim());
7732
+ }
7733
+ if (typeof normalized.selector !== "string" || !normalized.selector.trim()) {
7734
+ const selector = firstStringArg(normalized, [
7735
+ "cssSelector",
7736
+ "css_selector",
7737
+ "querySelector",
7738
+ "query_selector"
7739
+ ]);
7740
+ if (selector) normalized.selector = selector;
7741
+ }
7742
+ if (typeof normalized.text !== "string" || !normalized.text.trim()) {
7743
+ const text = firstStringArg(normalized, [
7744
+ "label",
7745
+ "title",
7746
+ "name",
7747
+ "target",
7748
+ "element",
7749
+ "linkText",
7750
+ "link_text",
7751
+ "ariaLabel",
7752
+ "aria_label"
7753
+ ]);
7754
+ if (text) normalized.text = text;
7755
+ }
7756
+ return normalized;
7558
7757
  }
7559
- function modelLikelySupportsOpenAIReasoningEffort(model) {
7560
- return /^(?:o\d|o[1-9]|gpt-5|codex|computer-use)/i.test(model.trim());
7758
+ function hasElementTarget(args) {
7759
+ return typeof args.index === "number" || typeof args.selector === "string" && args.selector.trim().length > 0 || typeof args.text === "string" && args.text.trim().length > 0;
7561
7760
  }
7562
- function toOpenAIReasoningEffort(effort, providerId, model) {
7563
- const supportsReasoningParam = providerId === "openrouter" || providerId === "custom" || providerId === "openai" && modelLikelySupportsOpenAIReasoningEffort(model);
7564
- if (!supportsReasoningParam) return void 0;
7565
- switch (effort) {
7566
- case "off":
7567
- if (providerId === "openai" && !/^gpt-5\.1/i.test(model.trim())) {
7568
- return void 0;
7569
- }
7570
- return "none";
7571
- case "low":
7572
- return "low";
7573
- case "medium":
7574
- return "medium";
7575
- case "high":
7576
- return "high";
7577
- case "max":
7578
- return "xhigh";
7579
- default:
7580
- return void 0;
7581
- }
7582
- }
7583
- function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
7584
- if (profile !== "compact") return null;
7585
- const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
7586
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
7587
- return {
7588
- role: "user",
7589
- content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
7590
- 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 ? `
7591
- ${stateReminder}` : "") + (phaseReminder ? `
7592
- ${phaseReminder}` : "")
7593
- };
7594
- }
7595
- function extractSingleGoalDomain(goal) {
7596
- const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
7597
- if (!matches || matches.length !== 1) return null;
7598
- return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
7599
- }
7600
- function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
7601
- const phaseReminder = buildPhaseReminder(userMessage, assistantText);
7602
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
7603
- const goalDomain = extractSingleGoalDomain(userMessage);
7604
- const latest = (latestToolResultPreview || "").toLowerCase();
7605
- const assistant = assistantText.toLowerCase();
7606
- const alreadyOnGoalSite = !!goalDomain && (latest.includes(goalDomain) || assistant.includes(`https://${goalDomain}`) || assistant.includes(`https://www.${goalDomain}`));
7607
- const lines = [
7608
- `The task is still in progress: ${userMessage}`,
7609
- `Do not ask the user for permission to continue. Choose the next tool now unless the request is fully complete.`
7610
- ];
7611
- if (alreadyOnGoalSite) {
7612
- lines.push(
7613
- `You are already on the requested site (${goalDomain}). Do not navigate to the homepage again and do not restart discovery from scratch.`
7614
- );
7615
- }
7616
- if (stateReminder) {
7617
- lines.push(stateReminder);
7618
- }
7619
- if (phaseReminder) {
7620
- lines.push(phaseReminder);
7621
- }
7622
- return lines.join("\n");
7623
- }
7624
- function buildPhaseReminder(userMessage, assistantText) {
7625
- const goal = userMessage.toLowerCase();
7626
- const text = assistantText.toLowerCase();
7627
- if (!goal || !text) return "";
7628
- const wantsCart = /\b(cart|bag|basket|checkout)\b/.test(goal);
7629
- const wantsExplanation = /\b(explain|reason|why)\b/.test(goal);
7630
- const wantsBookRecommendations = /\b(book|books|recommend|recommended|interesting|novel|fiction|nonfiction)\b/.test(
7631
- goal
7632
- );
7633
- 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);
7634
- 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);
7635
- 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);
7636
- const cartDone = /(added to cart|added them to the cart|cart confirmation|view cart|checkout)/.test(
7637
- text
7638
- );
7639
- 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);
7640
- 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(
7641
- text
7642
- );
7643
- 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(
7644
- text
7645
- );
7646
- 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(
7647
- text
7648
- ) && !/(cart confirmation|view cart|shopping cart|checkout|continue shopping)/.test(
7649
- text
7650
- );
7651
- const skippedSingleResultSignals = /did not yield a direct match|no direct match|no matches|unavailable on powell|out of stock or unavailable/.test(
7652
- text
7653
- ) && /proceed to (?:add|search for) the next book|move on to the next book|next book from my list/.test(
7654
- text
7655
- );
7656
- 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(
7657
- text
7658
- );
7659
- 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(
7660
- text
7661
- );
7662
- 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(
7663
- text
7664
- );
7665
- 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(
7666
- text
7667
- ) && !/(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(
7668
- text
7669
- );
7670
- if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && listingLoopSignals) {
7671
- 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.`;
7672
- }
7673
- if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && missedResultsSignals) {
7674
- 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.`;
7675
- }
7676
- if (wantsCart && falseCartSuccessSignals && !selectedItems && !explanationDone) {
7677
- 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.`;
7678
- }
7679
- if (wantsCart && skippedSingleResultSignals && !selectedItems) {
7680
- 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.`;
7681
- }
7682
- if (wantsCart && intermediateCartDialogSignals && !explanationDone) {
7683
- 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.`;
7684
- }
7685
- if (wantsCart && selectedItems && !cartDone && selectedItemsRestartSignals) {
7686
- 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.`;
7687
- }
7688
- if (wantsCart && wantsBookRecommendations && !cartDone && (multiClickSelectionSignals || staleSelectionSignals)) {
7689
- 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.`;
7690
- }
7691
- if (wantsCart && selectedItems && (intendsCart || !cartDone)) {
7692
- 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.`;
7693
- }
7694
- if (wantsCart && wantsExplanation && cartDone && !explanationDone) {
7695
- return `Progress reminder: The cart step appears complete. Do not resume browsing. Finish by explaining why the chosen items were recommended.`;
7696
- }
7697
- return "";
7698
- }
7699
- function buildLatestStateReminder(toolResultPreview) {
7700
- const text = toolResultPreview.trim();
7701
- if (!text) return "";
7702
- const stateMatch = text.match(
7703
- /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
7704
- );
7705
- if (stateMatch) {
7706
- const url = stateMatch[1]?.trim();
7707
- const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
7708
- if (url) {
7709
- return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
7710
- }
7711
- }
7712
- const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
7713
- const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
7714
- if (structuredUrl) {
7715
- return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
7716
- }
7717
- const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i)?.[1]?.trim();
7718
- const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
7719
- if (navigatedUrl) {
7720
- return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
7721
- }
7722
- return "";
7723
- }
7724
- function shouldRecoverCompactStall(text, userMessage) {
7725
- const trimmed = text.trim().toLowerCase();
7726
- if (!trimmed) return true;
7727
- if (trimmed.length <= 160 && trimmed.includes("?")) return true;
7728
- if (userMessage && buildPhaseReminder(userMessage, text)) {
7729
- return true;
7730
- }
7731
- const repetitivePlanningSignals = [
7732
- "next step:",
7733
- "i will now inspect",
7734
- "i will now read",
7735
- "i will now click",
7736
- "i'll use readpage",
7737
- "i'll use read_page",
7738
- "i'll start by clicking",
7739
- "i have clicked on five different book titles",
7740
- "clicked on five different book titles",
7741
- "i'll begin with",
7742
- "if the selection is unclear"
7743
- ];
7744
- if (repetitivePlanningSignals.some((pattern) => trimmed.includes(pattern))) {
7745
- return true;
7746
- }
7747
- 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(
7748
- trimmed
7749
- ) && !/(cart confirmation|view cart|continue shopping|shopping cart|checkout|why i chose|here is why i chose|here are my reasons)/.test(
7750
- trimmed
7751
- );
7752
- if (falseCartSuccessWithoutConfirmation) {
7753
- return true;
7754
- }
7755
- const completionSignals = [
7756
- "i found",
7757
- "i chose",
7758
- "i selected",
7759
- "i added",
7760
- "here are",
7761
- "these are",
7762
- "recommendations",
7763
- "reasoning",
7764
- "why i chose",
7765
- "added them to the cart"
7766
- ];
7767
- if (completionSignals.some((pattern) => trimmed.includes(pattern))) {
7768
- return false;
7769
- }
7770
- return [
7771
- "what are you hoping",
7772
- "what would you like",
7773
- "how can i help",
7774
- "let me know",
7775
- "are you looking for",
7776
- "just browsing",
7777
- "i need to",
7778
- "i will",
7779
- "i'll",
7780
- "since i cannot see",
7781
- "since i can't see",
7782
- "cannot see the current page",
7783
- "scroll down to",
7784
- "load more results",
7785
- "as placeholders",
7786
- "would you like me to proceed",
7787
- "action:",
7788
- "one moment",
7789
- "i will now navigate",
7790
- "navigating to ",
7791
- "this will take me",
7792
- "i will use the browser"
7793
- ].some((pattern) => trimmed.includes(pattern));
7794
- }
7795
- function shouldRetryCompactToolLoop(profile, text, hasToolHistory, userMessage) {
7796
- return profile === "compact" && hasToolHistory && shouldRecoverCompactStall(text, userMessage);
7797
- }
7798
- function stableToolSignature(name, args) {
7799
- const canonicalArgs = canonicalizeArgsForTool(name, args);
7800
- const sortedEntries = Object.entries(canonicalArgs).sort(
7801
- ([left], [right]) => left.localeCompare(right)
7802
- );
7803
- return JSON.stringify([name, sortedEntries]);
7804
- }
7805
- function normalizeToolToken(value) {
7806
- return value.trim().toLowerCase().replace(/[.\s/-]+/g, "_");
7807
- }
7808
- function canonicalizeUrlLike(value) {
7809
- try {
7810
- const url = new URL(value.trim());
7811
- if (url.protocol === "http:" || url.protocol === "https:") {
7812
- url.hostname = url.hostname.replace(/^www\./, "");
7813
- url.hash = "";
7814
- if (url.pathname.endsWith("/") && url.pathname !== "/") {
7815
- url.pathname = url.pathname.replace(/\/+$/, "");
7816
- }
7817
- return url.toString();
7818
- }
7819
- } catch {
7820
- }
7821
- return value.trim();
7822
- }
7823
- function toLikelyUrl(value) {
7824
- const trimmed = value.trim().replace(/^["']|["']$/g, "");
7825
- if (!trimmed) return null;
7826
- if (/^https?:\/\//i.test(trimmed)) return trimmed;
7827
- if (/^[a-z0-9-]+\.(com|org|net|io|dev|app|ai|co|edu|gov)(\/\S*)?$/i.test(trimmed)) {
7828
- return `https://${trimmed}`;
7829
- }
7830
- return null;
7831
- }
7832
- function scalarArgsForTool(name, scalar) {
7833
- const trimmed = scalar.trim();
7834
- if (!trimmed) return null;
7835
- if (name === "navigate") {
7836
- const url = toLikelyUrl(trimmed);
7837
- return url ? { url } : null;
7838
- }
7839
- if (name === "search") {
7840
- return { query: trimmed.replace(/^["']|["']$/g, "") };
7841
- }
7842
- if (name === "click" || name === "inspect_element" || name === "scroll_to_element") {
7843
- return { text: trimmed.replace(/^["']|["']$/g, "") };
7844
- }
7845
- if (name === "read_page") {
7846
- const mode = trimmed.replace(/^["']|["']$/g, "").toLowerCase();
7847
- if (mode) return { mode };
7848
- }
7849
- if (name === "save_bookmark") {
7850
- const url = toLikelyUrl(trimmed);
7851
- if (url) return { url };
7852
- const lastSpace = trimmed.lastIndexOf(" ");
7853
- if (lastSpace > 0) {
7854
- const maybeUrl = toLikelyUrl(trimmed.slice(lastSpace + 1));
7855
- if (maybeUrl) return { url: maybeUrl, title: trimmed.slice(0, lastSpace).replace(/^["']|["']$/g, "") };
7856
- }
7857
- }
7858
- return null;
7859
- }
7860
- function firstStringArg(args, keys) {
7861
- for (const key2 of keys) {
7862
- const value = args[key2];
7863
- if (typeof value === "string" && value.trim()) {
7864
- return value.trim();
7865
- }
7866
- }
7867
- return null;
7868
- }
7869
- function normalizeElementTargetArgs(args) {
7870
- const normalized = { ...args };
7871
- if (typeof normalized.index === "string" && /^\d+$/.test(normalized.index.trim())) {
7872
- normalized.index = Number(normalized.index.trim());
7873
- }
7874
- if (typeof normalized.selector !== "string" || !normalized.selector.trim()) {
7875
- const selector = firstStringArg(normalized, [
7876
- "cssSelector",
7877
- "css_selector",
7878
- "querySelector",
7879
- "query_selector"
7880
- ]);
7881
- if (selector) normalized.selector = selector;
7882
- }
7883
- if (typeof normalized.text !== "string" || !normalized.text.trim()) {
7884
- const text = firstStringArg(normalized, [
7885
- "label",
7886
- "title",
7887
- "name",
7888
- "target",
7889
- "element",
7890
- "linkText",
7891
- "link_text",
7892
- "ariaLabel",
7893
- "aria_label"
7894
- ]);
7895
- if (text) normalized.text = text;
7896
- }
7897
- return normalized;
7898
- }
7899
- function hasElementTarget(args) {
7900
- return typeof args.index === "number" || typeof args.selector === "string" && args.selector.trim().length > 0 || typeof args.text === "string" && args.text.trim().length > 0;
7901
- }
7902
- function isTargetlessClickArgs(args) {
7903
- return !hasElementTarget(normalizeElementTargetArgs(args));
7761
+ function isTargetlessClickArgs(args) {
7762
+ return !hasElementTarget(normalizeElementTargetArgs(args));
7904
7763
  }
7905
7764
  function tryParseJsonWithCommonRepairs(raw) {
7906
7765
  const trimmed = raw.trim();
@@ -8044,21 +7903,13 @@ function resolveToolCallName(rawName, args, availableToolNames) {
8044
7903
  if (availableToolNames.has("search") && (/search|find|lookup|query/.test(normalized) || normalized === "google" || normalized.startsWith("google_"))) {
8045
7904
  return "search";
8046
7905
  }
8047
- if (availableToolNames.has("scroll") && /scroll|page_?down|page_?up/.test(normalized)) {
8048
- return "scroll";
8049
- }
8050
- if (availableToolNames.has("read_page") && /read|scan|inspect|analy[sz]e|summari[sz]e/.test(normalized)) {
8051
- return "read_page";
8052
- }
8053
- return aliased;
8054
- }
8055
- function logAgentLoopDebug(payload) {
8056
- if (!shouldDebugAgentLoop()) return;
8057
- try {
8058
- logger$j.info(`[agent-debug] ${JSON.stringify(payload)}`);
8059
- } catch (err) {
8060
- logger$j.warn("Failed to serialize debug payload:", err);
7906
+ if (availableToolNames.has("scroll") && /scroll|page_?down|page_?up/.test(normalized)) {
7907
+ return "scroll";
7908
+ }
7909
+ if (availableToolNames.has("read_page") && /read|scan|inspect|analy[sz]e|summari[sz]e/.test(normalized)) {
7910
+ return "read_page";
8061
7911
  }
7912
+ return aliased;
8062
7913
  }
8063
7914
  function recoverTextEncodedToolCalls(text, availableToolNames) {
8064
7915
  const trimmed = text.trim();
@@ -8175,7 +8026,280 @@ function recoverNarratedActionToolCalls(text, availableToolNames) {
8175
8026
  });
8176
8027
  return recovered;
8177
8028
  }
8178
- return recovered;
8029
+ return recovered;
8030
+ }
8031
+ const logger$j = createLogger("OpenAIProvider");
8032
+ function shouldDebugAgentLoop() {
8033
+ const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
8034
+ return value === "1" || value === "true";
8035
+ }
8036
+ function previewDebugValue(value, maxLength = 800) {
8037
+ const normalized = value.replace(/\s+/g, " ").trim();
8038
+ if (normalized.length <= maxLength) return normalized;
8039
+ return `${normalized.slice(0, maxLength)}…`;
8040
+ }
8041
+ function previewToolDebugContent(content) {
8042
+ return previewDebugValue(content, 500);
8043
+ }
8044
+ function toOpenAITools(tools) {
8045
+ return tools.map((t) => ({
8046
+ type: "function",
8047
+ function: {
8048
+ name: t.name,
8049
+ description: t.description ?? "",
8050
+ parameters: t.input_schema
8051
+ }
8052
+ }));
8053
+ }
8054
+ function agentTemperatureForProfile(profile) {
8055
+ return profile === "compact" ? 0.2 : void 0;
8056
+ }
8057
+ function modelLikelySupportsOpenAIReasoningEffort(model) {
8058
+ return /^(?:o\d|o[1-9]|gpt-5|codex|computer-use)/i.test(model.trim());
8059
+ }
8060
+ function toOpenAIReasoningEffort(effort, providerId, model) {
8061
+ const supportsReasoningParam = providerId === "openrouter" || providerId === "custom" || providerId === "openai" && modelLikelySupportsOpenAIReasoningEffort(model);
8062
+ if (!supportsReasoningParam) return void 0;
8063
+ switch (effort) {
8064
+ case "off":
8065
+ if (providerId === "openai" && !/^gpt-5\.1/i.test(model.trim())) {
8066
+ return void 0;
8067
+ }
8068
+ return "none";
8069
+ case "low":
8070
+ return "low";
8071
+ case "medium":
8072
+ return "medium";
8073
+ case "high":
8074
+ return "high";
8075
+ case "max":
8076
+ return "xhigh";
8077
+ default:
8078
+ return void 0;
8079
+ }
8080
+ }
8081
+ function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
8082
+ if (profile !== "compact") return null;
8083
+ const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
8084
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
8085
+ return {
8086
+ role: "user",
8087
+ content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
8088
+ 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 ? `
8089
+ ${stateReminder}` : "") + (phaseReminder ? `
8090
+ ${phaseReminder}` : "")
8091
+ };
8092
+ }
8093
+ function extractSingleGoalDomain(goal) {
8094
+ const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
8095
+ if (!matches || matches.length !== 1) return null;
8096
+ return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
8097
+ }
8098
+ function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
8099
+ const phaseReminder = buildPhaseReminder(userMessage, assistantText);
8100
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
8101
+ const goalDomain = extractSingleGoalDomain(userMessage);
8102
+ const latest = (latestToolResultPreview || "").toLowerCase();
8103
+ const assistant = assistantText.toLowerCase();
8104
+ const alreadyOnGoalSite = !!goalDomain && (latest.includes(goalDomain) || assistant.includes(`https://${goalDomain}`) || assistant.includes(`https://www.${goalDomain}`));
8105
+ const lines = [
8106
+ `The task is still in progress: ${userMessage}`,
8107
+ `Do not ask the user for permission to continue. Choose the next tool now unless the request is fully complete.`
8108
+ ];
8109
+ if (alreadyOnGoalSite) {
8110
+ lines.push(
8111
+ `You are already on the requested site (${goalDomain}). Do not navigate to the homepage again and do not restart discovery from scratch.`
8112
+ );
8113
+ }
8114
+ if (stateReminder) {
8115
+ lines.push(stateReminder);
8116
+ }
8117
+ if (phaseReminder) {
8118
+ lines.push(phaseReminder);
8119
+ }
8120
+ return lines.join("\n");
8121
+ }
8122
+ function buildPhaseReminder(userMessage, assistantText) {
8123
+ const goal = userMessage.toLowerCase();
8124
+ const text = assistantText.toLowerCase();
8125
+ if (!goal || !text) return "";
8126
+ const wantsCart = /\b(cart|bag|basket|checkout)\b/.test(goal);
8127
+ const wantsExplanation = /\b(explain|reason|why)\b/.test(goal);
8128
+ const wantsBookRecommendations = /\b(book|books|recommend|recommended|interesting|novel|fiction|nonfiction)\b/.test(
8129
+ goal
8130
+ );
8131
+ 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);
8132
+ 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);
8133
+ 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);
8134
+ const cartDone = /(added to cart|added them to the cart|cart confirmation|view cart|checkout)/.test(
8135
+ text
8136
+ );
8137
+ 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);
8138
+ 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(
8139
+ text
8140
+ );
8141
+ 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(
8142
+ text
8143
+ );
8144
+ 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(
8145
+ text
8146
+ ) && !/(cart confirmation|view cart|shopping cart|checkout|continue shopping)/.test(
8147
+ text
8148
+ );
8149
+ const skippedSingleResultSignals = /did not yield a direct match|no direct match|no matches|unavailable on powell|out of stock or unavailable/.test(
8150
+ text
8151
+ ) && /proceed to (?:add|search for) the next book|move on to the next book|next book from my list/.test(
8152
+ text
8153
+ );
8154
+ 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(
8155
+ text
8156
+ );
8157
+ 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(
8158
+ text
8159
+ );
8160
+ 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(
8161
+ text
8162
+ );
8163
+ 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(
8164
+ text
8165
+ ) && !/(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(
8166
+ text
8167
+ );
8168
+ if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && listingLoopSignals) {
8169
+ 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.`;
8170
+ }
8171
+ if (wantsCart && wantsBookRecommendations && !selectedItems && !cartDone && missedResultsSignals) {
8172
+ 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.`;
8173
+ }
8174
+ if (wantsCart && falseCartSuccessSignals && !selectedItems && !explanationDone) {
8175
+ 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.`;
8176
+ }
8177
+ if (wantsCart && skippedSingleResultSignals && !selectedItems) {
8178
+ 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.`;
8179
+ }
8180
+ if (wantsCart && intermediateCartDialogSignals && !explanationDone) {
8181
+ 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.`;
8182
+ }
8183
+ if (wantsCart && selectedItems && !cartDone && selectedItemsRestartSignals) {
8184
+ 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.`;
8185
+ }
8186
+ if (wantsCart && wantsBookRecommendations && !cartDone && (multiClickSelectionSignals || staleSelectionSignals)) {
8187
+ 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.`;
8188
+ }
8189
+ if (wantsCart && selectedItems && (intendsCart || !cartDone)) {
8190
+ 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.`;
8191
+ }
8192
+ if (wantsCart && wantsExplanation && cartDone && !explanationDone) {
8193
+ return `Progress reminder: The cart step appears complete. Do not resume browsing. Finish by explaining why the chosen items were recommended.`;
8194
+ }
8195
+ return "";
8196
+ }
8197
+ function buildLatestStateReminder(toolResultPreview) {
8198
+ const text = toolResultPreview.trim();
8199
+ if (!text) return "";
8200
+ const stateMatch = text.match(
8201
+ /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
8202
+ );
8203
+ if (stateMatch) {
8204
+ const url = stateMatch[1]?.trim();
8205
+ const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
8206
+ if (url) {
8207
+ return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
8208
+ }
8209
+ }
8210
+ const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8211
+ const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8212
+ if (structuredUrl) {
8213
+ return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8214
+ }
8215
+ const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i)?.[1]?.trim();
8216
+ const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
8217
+ if (navigatedUrl) {
8218
+ return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8219
+ }
8220
+ return "";
8221
+ }
8222
+ function shouldRecoverCompactStall(text, userMessage) {
8223
+ const trimmed = text.trim().toLowerCase();
8224
+ if (!trimmed) return true;
8225
+ if (trimmed.length <= 160 && trimmed.includes("?")) return true;
8226
+ if (userMessage && buildPhaseReminder(userMessage, text)) {
8227
+ return true;
8228
+ }
8229
+ const repetitivePlanningSignals = [
8230
+ "next step:",
8231
+ "i will now inspect",
8232
+ "i will now read",
8233
+ "i will now click",
8234
+ "i'll use readpage",
8235
+ "i'll use read_page",
8236
+ "i'll start by clicking",
8237
+ "i have clicked on five different book titles",
8238
+ "clicked on five different book titles",
8239
+ "i'll begin with",
8240
+ "if the selection is unclear"
8241
+ ];
8242
+ if (repetitivePlanningSignals.some((pattern) => trimmed.includes(pattern))) {
8243
+ return true;
8244
+ }
8245
+ 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(
8246
+ trimmed
8247
+ ) && !/(cart confirmation|view cart|continue shopping|shopping cart|checkout|why i chose|here is why i chose|here are my reasons)/.test(
8248
+ trimmed
8249
+ );
8250
+ if (falseCartSuccessWithoutConfirmation) {
8251
+ return true;
8252
+ }
8253
+ const completionSignals = [
8254
+ "i found",
8255
+ "i chose",
8256
+ "i selected",
8257
+ "i added",
8258
+ "here are",
8259
+ "these are",
8260
+ "recommendations",
8261
+ "reasoning",
8262
+ "why i chose",
8263
+ "added them to the cart"
8264
+ ];
8265
+ if (completionSignals.some((pattern) => trimmed.includes(pattern))) {
8266
+ return false;
8267
+ }
8268
+ return [
8269
+ "what are you hoping",
8270
+ "what would you like",
8271
+ "how can i help",
8272
+ "let me know",
8273
+ "are you looking for",
8274
+ "just browsing",
8275
+ "i need to",
8276
+ "i will",
8277
+ "i'll",
8278
+ "since i cannot see",
8279
+ "since i can't see",
8280
+ "cannot see the current page",
8281
+ "scroll down to",
8282
+ "load more results",
8283
+ "as placeholders",
8284
+ "would you like me to proceed",
8285
+ "action:",
8286
+ "one moment",
8287
+ "i will now navigate",
8288
+ "navigating to ",
8289
+ "this will take me",
8290
+ "i will use the browser"
8291
+ ].some((pattern) => trimmed.includes(pattern));
8292
+ }
8293
+ function shouldRetryCompactToolLoop(profile, text, hasToolHistory, userMessage) {
8294
+ return profile === "compact" && hasToolHistory && shouldRecoverCompactStall(text, userMessage);
8295
+ }
8296
+ function logAgentLoopDebug(payload) {
8297
+ if (!shouldDebugAgentLoop()) return;
8298
+ try {
8299
+ logger$j.info(`[agent-debug] ${JSON.stringify(payload)}`);
8300
+ } catch (err) {
8301
+ logger$j.warn("Failed to serialize debug payload:", err);
8302
+ }
8179
8303
  }
8180
8304
  function formatOpenAICompatErrorMessage(providerId, message) {
8181
8305
  if (providerId === "llama_cpp" && /(available context size|context size exceeded|exceeds the available context size|try increasing it)/i.test(
@@ -12642,6 +12766,8 @@ const UNSORTED_ID = "unsorted";
12642
12766
  const ARCHIVE_FOLDER_NAME = "Archive";
12643
12767
  const NETSCAPE_BOOKMARKS_DOCTYPE = "<!DOCTYPE NETSCAPE-Bookmark-file-1>";
12644
12768
  const SAVE_DEBOUNCE_MS$1 = 250;
12769
+ const DEFAULT_BOOKMARK_SEARCH_LIMIT = 50;
12770
+ const MAX_BOOKMARK_SEARCH_LIMIT = 200;
12645
12771
  let state$2 = null;
12646
12772
  const listeners = /* @__PURE__ */ new Set();
12647
12773
  function cloneState(current) {
@@ -12650,6 +12776,10 @@ function cloneState(current) {
12650
12776
  bookmarks: current.bookmarks.map((bookmark) => ({ ...bookmark }))
12651
12777
  };
12652
12778
  }
12779
+ function getFolderMap() {
12780
+ load$1();
12781
+ return new Map(state$2.folders.map((folder) => [folder.id, folder]));
12782
+ }
12653
12783
  function getBookmarksPath() {
12654
12784
  return path.join(electron.app.getPath("userData"), "vessel-bookmarks.json");
12655
12785
  }
@@ -12876,13 +13006,16 @@ function listFolderOverviews() {
12876
13006
  }))
12877
13007
  ];
12878
13008
  }
12879
- function searchBookmarks(query) {
13009
+ function searchBookmarks(query, limit = DEFAULT_BOOKMARK_SEARCH_LIMIT) {
12880
13010
  load$1();
12881
13011
  if (!query.trim()) return [];
13012
+ const foldersById = getFolderMap();
13013
+ const safeLimit = Math.max(
13014
+ 1,
13015
+ Math.min(MAX_BOOKMARK_SEARCH_LIMIT, Math.floor(limit))
13016
+ );
12882
13017
  return state$2.bookmarks.map((bookmark) => {
12883
- const folder = state$2.folders.find(
12884
- (item) => item.id === bookmark.folderId
12885
- );
13018
+ const folder = foldersById.get(bookmark.folderId);
12886
13019
  const { matchedFields, score } = getBookmarkSearchMatch({
12887
13020
  query,
12888
13021
  title: bookmark.title,
@@ -12906,7 +13039,7 @@ function searchBookmarks(query) {
12906
13039
  score: result.score
12907
13040
  })).sort(
12908
13041
  (a, b) => b.score - a.score || b.bookmark.savedAt.localeCompare(a.bookmark.savedAt)
12909
- );
13042
+ ).slice(0, safeLimit);
12910
13043
  }
12911
13044
  function createFolderWithSummary(name, summary) {
12912
13045
  load$1();
@@ -14174,6 +14307,97 @@ function formatCompactToolResult(name, result) {
14174
14307
  return limitText(result, 18, 1400);
14175
14308
  }
14176
14309
  }
14310
+ const ADD_TO_CART_PATTERNS = [
14311
+ "add to cart",
14312
+ "add to bag",
14313
+ "add to basket",
14314
+ "add to my cart",
14315
+ "add to my bag",
14316
+ "add to my basket",
14317
+ "add item to cart",
14318
+ "add item to bag",
14319
+ "add item to basket"
14320
+ ];
14321
+ const CART_CLICK_COOLDOWN_MS = 15e3;
14322
+ const CART_ADDED_TTL_MS = 30 * 6e4;
14323
+ const recentCartClicks = /* @__PURE__ */ new Map();
14324
+ const cartAddedProducts = /* @__PURE__ */ new Map();
14325
+ function isAddToCartText(text) {
14326
+ const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
14327
+ return ADD_TO_CART_PATTERNS.some((pattern) => normalized.includes(pattern));
14328
+ }
14329
+ function recordCartClick(url) {
14330
+ recentCartClicks.set(url, Date.now());
14331
+ pruneRecentCartClicks();
14332
+ }
14333
+ function hasRecentCartClick(url) {
14334
+ const recent = recentCartClicks.get(url);
14335
+ if (!recent) return false;
14336
+ if (Date.now() - recent > CART_CLICK_COOLDOWN_MS) {
14337
+ recentCartClicks.delete(url);
14338
+ return false;
14339
+ }
14340
+ return true;
14341
+ }
14342
+ function isDuplicateCartClick(url, text) {
14343
+ return hasRecentCartClick(url) && isAddToCartText(text);
14344
+ }
14345
+ function recordProductAddedToCart(url, productName) {
14346
+ pruneCartAddedProducts();
14347
+ cartAddedProducts.set(normalizeCartProductKey(url), {
14348
+ title: productName || url,
14349
+ ts: Date.now()
14350
+ });
14351
+ }
14352
+ function isProductAlreadyInCart(url) {
14353
+ pruneCartAddedProducts();
14354
+ return cartAddedProducts.has(normalizeCartProductKey(url));
14355
+ }
14356
+ function getCartAddedSummary(url) {
14357
+ pruneCartAddedProducts();
14358
+ const origin = cartOrigin(url);
14359
+ const items = Array.from(cartAddedProducts.entries()).filter(([key2]) => !origin || key2.startsWith(`${origin}/`)).map(([_path, info]) => `- ${info.title}`).join("\n");
14360
+ if (!items) return "";
14361
+ const count = items.split("\n").length;
14362
+ return `
14363
+ Already in cart (${count} items):
14364
+ ${items}`;
14365
+ }
14366
+ function clearCartClickState() {
14367
+ cartAddedProducts.clear();
14368
+ recentCartClicks.clear();
14369
+ }
14370
+ function pruneRecentCartClicks(now = Date.now()) {
14371
+ for (const [key2, ts] of recentCartClicks) {
14372
+ if (now - ts > CART_CLICK_COOLDOWN_MS) {
14373
+ recentCartClicks.delete(key2);
14374
+ }
14375
+ }
14376
+ }
14377
+ function normalizeCartProductKey(url) {
14378
+ try {
14379
+ const parsed = new URL(url);
14380
+ const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
14381
+ return `${parsed.origin}${pathname}`;
14382
+ } catch {
14383
+ return url;
14384
+ }
14385
+ }
14386
+ function pruneCartAddedProducts(now = Date.now()) {
14387
+ for (const [key2, entry] of cartAddedProducts) {
14388
+ if (now - entry.ts > CART_ADDED_TTL_MS) {
14389
+ cartAddedProducts.delete(key2);
14390
+ }
14391
+ }
14392
+ }
14393
+ function cartOrigin(url) {
14394
+ if (!url) return null;
14395
+ try {
14396
+ return new URL(url).origin;
14397
+ } catch {
14398
+ return null;
14399
+ }
14400
+ }
14177
14401
  const HUGGING_FACE_HUB_HOSTS = /* @__PURE__ */ new Set(["huggingface.co", "www.huggingface.co"]);
14178
14402
  const HUGGING_FACE_MODEL_TASKS = [
14179
14403
  {
@@ -14437,9 +14661,12 @@ function buildHuggingFaceSearchShortcut(currentUrl, rawQuery) {
14437
14661
  class TabMutex {
14438
14662
  queue = Promise.resolve();
14439
14663
  enqueue(fn) {
14440
- return new Promise((resolve, reject) => {
14441
- this.queue = this.queue.then(fn).then(resolve, reject);
14442
- });
14664
+ const run = this.queue.then(fn, fn);
14665
+ this.queue = run.then(
14666
+ () => void 0,
14667
+ () => void 0
14668
+ );
14669
+ return run;
14443
14670
  }
14444
14671
  }
14445
14672
  const logger$e = createLogger("PageActions");
@@ -15267,45 +15494,9 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15267
15494
  }
15268
15495
  }
15269
15496
  }
15270
- const ADD_TO_CART_PATTERNS = [
15271
- "add to cart",
15272
- "add to bag",
15273
- "add to basket",
15274
- "add to my cart",
15275
- "add to my bag",
15276
- "add to my basket",
15277
- "add item to cart",
15278
- "add item to bag",
15279
- "add item to basket"
15280
- ];
15281
- const recentCartClicks = /* @__PURE__ */ new Map();
15282
- const CART_CLICK_COOLDOWN_MS = 15e3;
15283
- const CART_ADDED_TTL_MS = 30 * 6e4;
15284
- const cartAddedProducts = /* @__PURE__ */ new Map();
15285
15497
  let clickStreakUrl = null;
15286
15498
  let clickStreakCount = 0;
15287
15499
  const CLICK_STREAK_THRESHOLD = 3;
15288
- function isAddToCartText(text) {
15289
- const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
15290
- return ADD_TO_CART_PATTERNS.some((p) => normalized.includes(p));
15291
- }
15292
- function recordCartClick(url, text) {
15293
- recentCartClicks.set(url, { text, ts: Date.now() });
15294
- for (const [key2, entry] of recentCartClicks) {
15295
- if (Date.now() - entry.ts > CART_CLICK_COOLDOWN_MS) {
15296
- recentCartClicks.delete(key2);
15297
- }
15298
- }
15299
- }
15300
- function isDuplicateCartClick(url, text) {
15301
- const recent = recentCartClicks.get(url);
15302
- if (!recent) return false;
15303
- if (Date.now() - recent.ts > CART_CLICK_COOLDOWN_MS) {
15304
- recentCartClicks.delete(url);
15305
- return false;
15306
- }
15307
- return isAddToCartText(text);
15308
- }
15309
15500
  async function getProductPageTitle(wc) {
15310
15501
  try {
15311
15502
  const heading = await executePageScript(
@@ -15330,54 +15521,8 @@ async function getProductPageTitle(wc) {
15330
15521
  }
15331
15522
  return wc.getTitle() || "";
15332
15523
  }
15333
- function normalizeCartProductKey(url) {
15334
- try {
15335
- const parsed = new URL(url);
15336
- const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
15337
- return `${parsed.origin}${pathname}`;
15338
- } catch {
15339
- return url;
15340
- }
15341
- }
15342
- function pruneCartAddedProducts(now = Date.now()) {
15343
- for (const [key2, entry] of cartAddedProducts) {
15344
- if (now - entry.ts > CART_ADDED_TTL_MS) {
15345
- cartAddedProducts.delete(key2);
15346
- }
15347
- }
15348
- }
15349
- function cartOrigin(url) {
15350
- if (!url) return null;
15351
- try {
15352
- return new URL(url).origin;
15353
- } catch {
15354
- return null;
15355
- }
15356
- }
15357
- function recordProductAddedToCart(url, productName) {
15358
- pruneCartAddedProducts();
15359
- cartAddedProducts.set(normalizeCartProductKey(url), {
15360
- title: productName || url,
15361
- ts: Date.now()
15362
- });
15363
- }
15364
- function isProductAlreadyInCart(url) {
15365
- pruneCartAddedProducts();
15366
- return cartAddedProducts.has(normalizeCartProductKey(url));
15367
- }
15368
- function getCartAddedSummary(url) {
15369
- pruneCartAddedProducts();
15370
- const origin = cartOrigin(url);
15371
- const items = Array.from(cartAddedProducts.entries()).filter(([key2]) => !origin || key2.startsWith(`${origin}/`)).map(([_path, info]) => `- ${info.title}`).join("\n");
15372
- if (!items) return "";
15373
- const count = items.split("\n").length;
15374
- return `
15375
- Already in cart (${count} items):
15376
- ${items}`;
15377
- }
15378
15524
  function clearCartState() {
15379
- cartAddedProducts.clear();
15380
- recentCartClicks.clear();
15525
+ clearCartClickState();
15381
15526
  clickStreakUrl = null;
15382
15527
  clickStreakCount = 0;
15383
15528
  }
@@ -15429,7 +15574,7 @@ Go back and select a different product.`;
15429
15574
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
15430
15575
  if (typeof result === "string" && result.startsWith("Error")) return result;
15431
15576
  if (idxCartMatch) {
15432
- recordCartClick(beforeUrl2, idxLabel);
15577
+ recordCartClick(beforeUrl2);
15433
15578
  }
15434
15579
  await waitForPotentialNavigation(wc, beforeUrl2);
15435
15580
  const afterUrl2 = wc.getURL();
@@ -15496,7 +15641,7 @@ Go back and select a different product.`;
15496
15641
  if (result === PAGE_SCRIPT_TIMEOUT) return pageBusyError("click");
15497
15642
  if (typeof result === "string" && result.startsWith("Error")) return result;
15498
15643
  if (shadowCartMatch) {
15499
- recordCartClick(beforeUrl2, shadowLabel);
15644
+ recordCartClick(beforeUrl2);
15500
15645
  }
15501
15646
  await waitForPotentialNavigation(wc, beforeUrl2);
15502
15647
  const afterUrl2 = wc.getURL();
@@ -15532,7 +15677,7 @@ Note: Page did not change after click.`;
15532
15677
  if (cartMatch && isDuplicateCartClick(beforeUrl, elInfo.text)) {
15533
15678
  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).`;
15534
15679
  }
15535
- if (!cartMatch && recentCartClicks.has(beforeUrl)) {
15680
+ if (!cartMatch && hasRecentCartClick(beforeUrl)) {
15536
15681
  const dialogActions = await getCartDialogActions(wc);
15537
15682
  if (dialogActions) {
15538
15683
  return `Blocked: a cart confirmation dialog is open. Do not click background elements.
@@ -15552,7 +15697,7 @@ Click one of these dialog actions instead.`;
15552
15697
  Go back and select a different product.`;
15553
15698
  }
15554
15699
  if (cartMatch) {
15555
- recordCartClick(beforeUrl, elInfo.text);
15700
+ recordCartClick(beforeUrl);
15556
15701
  }
15557
15702
  const tagLabel = elInfo.tag && elInfo.tag !== "a" && elInfo.tag !== "button" ? ` <${elInfo.tag}>` : "";
15558
15703
  const clickText = `Clicked: ${elInfo.text}${tagLabel}`;
@@ -18986,6 +19131,57 @@ function onAIStreamIdle(listener) {
18986
19131
  idleListeners.add(listener);
18987
19132
  return () => idleListeners.delete(listener);
18988
19133
  }
19134
+ const MAX_PROVIDER_HISTORY_MESSAGES = 24;
19135
+ const MAX_PROVIDER_HISTORY_CHARS = 24e3;
19136
+ const MAX_PROVIDER_HISTORY_MESSAGE_CHARS = 3e3;
19137
+ const MAX_PROVIDER_HISTORY_SUMMARY_CHARS = 2e3;
19138
+ function truncateText(value, maxLength) {
19139
+ if (value.length <= maxLength) return value;
19140
+ return `${value.slice(0, maxLength - 3).trimEnd()}...`;
19141
+ }
19142
+ function normalizeHistoryMessage(message) {
19143
+ return {
19144
+ role: message.role,
19145
+ content: truncateText(message.content, MAX_PROVIDER_HISTORY_MESSAGE_CHARS)
19146
+ };
19147
+ }
19148
+ function totalHistoryChars(history) {
19149
+ return history.reduce((total, message) => total + message.content.length, 0);
19150
+ }
19151
+ function summarizeOmittedHistory(history) {
19152
+ const snippets = history.slice(-12).map((message) => `${message.role}: ${truncateText(message.content.replace(/\s+/g, " ").trim(), 220)}`).filter((line) => line.length > "assistant: ".length);
19153
+ const content = truncateText(
19154
+ [
19155
+ `[Earlier conversation compacted: ${history.length} message${history.length === 1 ? "" : "s"} omitted.]`,
19156
+ ...snippets
19157
+ ].join("\n"),
19158
+ MAX_PROVIDER_HISTORY_SUMMARY_CHARS
19159
+ );
19160
+ return { role: "user", content };
19161
+ }
19162
+ function compactProviderHistory(history = []) {
19163
+ const normalized = history.map(normalizeHistoryMessage);
19164
+ if (normalized.length <= MAX_PROVIDER_HISTORY_MESSAGES && totalHistoryChars(normalized) <= MAX_PROVIDER_HISTORY_CHARS) {
19165
+ return normalized;
19166
+ }
19167
+ const recent = [];
19168
+ const recentBudget = MAX_PROVIDER_HISTORY_CHARS - MAX_PROVIDER_HISTORY_SUMMARY_CHARS;
19169
+ let usedChars = 0;
19170
+ for (let index = normalized.length - 1; index >= 0; index--) {
19171
+ const message = normalized[index];
19172
+ const nextChars = usedChars + message.content.length;
19173
+ if (recent.length >= MAX_PROVIDER_HISTORY_MESSAGES || nextChars > recentBudget) {
19174
+ break;
19175
+ }
19176
+ recent.unshift(message);
19177
+ usedChars = nextChars;
19178
+ }
19179
+ if (recent.length === 0 && normalized.length > 0) {
19180
+ recent.unshift(normalized[normalized.length - 1]);
19181
+ }
19182
+ const omitted = normalized.slice(0, normalized.length - recent.length);
19183
+ return omitted.length > 0 ? [summarizeOmittedHistory(omitted), ...recent] : recent;
19184
+ }
18989
19185
  const DEFAULT_PAGE_FOLDER = "Vessel/Pages";
18990
19186
  const DEFAULT_NOTE_FOLDER = "Vessel/Research";
18991
19187
  const DEFAULT_BOOKMARK_FOLDER = "Vessel/Bookmarks";
@@ -20341,6 +20537,14 @@ function asErrorTextResponse(message) {
20341
20537
  function asNoActiveTabResponse() {
20342
20538
  return asErrorTextResponse("No active tab");
20343
20539
  }
20540
+ function getPremiumToolGateResponse(toolName) {
20541
+ try {
20542
+ assertToolUnlocked(toolName);
20543
+ return null;
20544
+ } catch (error) {
20545
+ return asTextResponse(getErrorMessage(error));
20546
+ }
20547
+ }
20344
20548
  function asPromptResponse(text) {
20345
20549
  return {
20346
20550
  messages: [
@@ -20443,6 +20647,8 @@ async function getPostActionState(tabManager, name) {
20443
20647
  return "";
20444
20648
  }
20445
20649
  async function withAction(runtime2, tabManager, name, args, executor) {
20650
+ const premiumGate = getPremiumToolGateResponse(name);
20651
+ if (premiumGate) return premiumGate;
20446
20652
  try {
20447
20653
  const result = await runtime2.runControlledAction({
20448
20654
  source: "mcp",
@@ -21752,6 +21958,8 @@ ${buildScopedContext(pageContent, mode)}`;
21752
21958
  description: "Capture a screenshot of the current page. Returns a base64-encoded PNG image."
21753
21959
  },
21754
21960
  async () => {
21961
+ const premiumGate = getPremiumToolGateResponse("screenshot");
21962
+ if (premiumGate) return premiumGate;
21755
21963
  const tab = tabManager.getActiveTab();
21756
21964
  if (!tab) return asNoActiveTabResponse();
21757
21965
  try {
@@ -22774,6 +22982,8 @@ ${JSON.stringify(otherHighlights, null, 2)}`
22774
22982
  }
22775
22983
  },
22776
22984
  async ({ goal, steps }) => {
22985
+ const premiumGate = getPremiumToolGateResponse("flow_start");
22986
+ if (premiumGate) return premiumGate;
22777
22987
  const normalizedSteps = coerceStringArray(steps) ?? [];
22778
22988
  const tab = tabManager.getActiveTab();
22779
22989
  const flow = runtime2.startFlow(
@@ -22797,6 +23007,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
22797
23007
  }
22798
23008
  },
22799
23009
  async ({ detail }) => {
23010
+ const premiumGate = getPremiumToolGateResponse("flow_advance");
23011
+ if (premiumGate) return premiumGate;
22800
23012
  const flow = runtime2.advanceFlow(detail);
22801
23013
  if (!flow) return asTextResponse("No active flow to advance");
22802
23014
  const ctx = runtime2.getFlowContext();
@@ -22810,6 +23022,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
22810
23022
  description: "Check the current workflow progress."
22811
23023
  },
22812
23024
  async () => {
23025
+ const premiumGate = getPremiumToolGateResponse("flow_status");
23026
+ if (premiumGate) return premiumGate;
22813
23027
  const flow = runtime2.getFlowState();
22814
23028
  if (!flow) return asTextResponse("No active workflow.");
22815
23029
  return asTextResponse(runtime2.getFlowContext());
@@ -22822,6 +23036,8 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
22822
23036
  description: "Clear the active workflow tracker."
22823
23037
  },
22824
23038
  async () => {
23039
+ const premiumGate = getPremiumToolGateResponse("flow_end");
23040
+ if (premiumGate) return premiumGate;
22825
23041
  runtime2.clearFlow();
22826
23042
  return asTextResponse("Workflow ended.");
22827
23043
  }
@@ -23462,6 +23678,8 @@ ${JSON.stringify(tableJson, null, 2)}`;
23462
23678
  }
23463
23679
  },
23464
23680
  async ({ domain }) => {
23681
+ const premiumGate = getPremiumToolGateResponse("vault_status");
23682
+ if (premiumGate) return premiumGate;
23465
23683
  let targetDomain = domain;
23466
23684
  if (!targetDomain) {
23467
23685
  const tab = tabManager.getActiveTab();
@@ -23528,6 +23746,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23528
23746
  submit_after,
23529
23747
  submit_index
23530
23748
  }) => {
23749
+ const premiumGate = getPremiumToolGateResponse("vault_login");
23750
+ if (premiumGate) return premiumGate;
23531
23751
  const tab = tabManager.getActiveTab();
23532
23752
  if (!tab) return asNoActiveTabResponse();
23533
23753
  const wc = tab.view.webContents;
@@ -23622,6 +23842,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23622
23842
  }
23623
23843
  },
23624
23844
  async ({ credential_label, code_index, submit_after, submit_index }) => {
23845
+ const premiumGate = getPremiumToolGateResponse("vault_totp");
23846
+ if (premiumGate) return premiumGate;
23625
23847
  const tab = tabManager.getActiveTab();
23626
23848
  if (!tab) return asNoActiveTabResponse();
23627
23849
  const wc = tab.view.webContents;
@@ -23701,6 +23923,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23701
23923
  })
23702
23924
  },
23703
23925
  async ({ domain }) => {
23926
+ const premiumGate = getPremiumToolGateResponse("human_vault_list");
23927
+ if (premiumGate) return premiumGate;
23704
23928
  const consent = await requestHumanVaultConsent({
23705
23929
  action: "list",
23706
23930
  domain: domain ?? "all"
@@ -23751,6 +23975,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23751
23975
  })
23752
23976
  },
23753
23977
  async ({ entry_id, username_index, password_index, submit_after, submit_index }) => {
23978
+ const premiumGate = getPremiumToolGateResponse("human_vault_fill");
23979
+ if (premiumGate) return premiumGate;
23754
23980
  const tab = tabManager.getActiveTab();
23755
23981
  if (!tab) return asNoActiveTabResponse();
23756
23982
  let hostname;
@@ -23843,6 +24069,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23843
24069
  })
23844
24070
  },
23845
24071
  async ({ entry_id }) => {
24072
+ const premiumGate = getPremiumToolGateResponse("human_vault_remove");
24073
+ if (premiumGate) return premiumGate;
23846
24074
  const entry = getEntry(entry_id);
23847
24075
  if (!entry) {
23848
24076
  return asErrorTextResponse(`No entry found with ID ${entry_id}.`);
@@ -23867,6 +24095,8 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23867
24095
  inputSchema: zod.z.object({})
23868
24096
  },
23869
24097
  async () => {
24098
+ const premiumGate = getPremiumToolGateResponse("metrics");
24099
+ if (premiumGate) return premiumGate;
23870
24100
  const m = runtime2.getMetrics();
23871
24101
  const lines = [
23872
24102
  `Session Metrics:`,
@@ -24220,7 +24450,7 @@ function assertNumber(value, name) {
24220
24450
  }
24221
24451
  }
24222
24452
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
24223
- function isValidEmail(value) {
24453
+ function isValidEmail$1(value) {
24224
24454
  return EMAIL_RE.test(value.trim());
24225
24455
  }
24226
24456
  function getActiveTabInfo(tabManager) {
@@ -24232,6 +24462,8 @@ function getActiveTabInfo(tabManager) {
24232
24462
  }
24233
24463
  const logger$9 = createLogger("Scheduler");
24234
24464
  let jobs = [];
24465
+ let pollInterval = null;
24466
+ let alignStartTimeout = null;
24235
24467
  let removeIdleListener = null;
24236
24468
  let broadcastFn = null;
24237
24469
  function getScheduledKitIds() {
@@ -24320,10 +24552,12 @@ function computeNextRun(schedule, from = /* @__PURE__ */ new Date()) {
24320
24552
  const next = new Date(from);
24321
24553
  next.setHours(schedule.hour, schedule.minute, 0, 0);
24322
24554
  const daysUntil = (schedule.dayOfWeek - next.getDay() + 7) % 7;
24323
- if (daysUntil === 0 && next <= from) {
24324
- next.setDate(next.getDate() + 7);
24555
+ if (daysUntil === 0) {
24556
+ if (next <= from) {
24557
+ next.setDate(next.getDate() + 7);
24558
+ }
24325
24559
  } else {
24326
- next.setDate(next.getDate() + (daysUntil || 7));
24560
+ next.setDate(next.getDate() + daysUntil);
24327
24561
  }
24328
24562
  return next;
24329
24563
  }
@@ -24450,18 +24684,19 @@ function tick(windowState, runtime2) {
24450
24684
  fireNext();
24451
24685
  }
24452
24686
  function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24687
+ stopScheduler();
24453
24688
  broadcastFn = sendToAll;
24454
24689
  loadJobs();
24455
24690
  if (normalizeJobs()) {
24456
24691
  saveJobs();
24457
24692
  }
24458
- removeIdleListener?.();
24459
24693
  removeIdleListener = onAIStreamIdle(() => tick(windowState, runtime2));
24460
24694
  const now = /* @__PURE__ */ new Date();
24461
24695
  const msToNextMinute = (60 - now.getSeconds()) * 1e3 - now.getMilliseconds();
24462
- setTimeout(() => {
24696
+ alignStartTimeout = setTimeout(() => {
24697
+ alignStartTimeout = null;
24463
24698
  tick(windowState, runtime2);
24464
- setInterval(() => tick(windowState, runtime2), 6e4);
24699
+ pollInterval = setInterval(() => tick(windowState, runtime2), 6e4);
24465
24700
  }, msToNextMinute);
24466
24701
  electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, (event) => {
24467
24702
  assertTrustedIpcSender(event);
@@ -24523,6 +24758,20 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24523
24758
  return true;
24524
24759
  });
24525
24760
  }
24761
+ function stopScheduler() {
24762
+ if (removeIdleListener) {
24763
+ removeIdleListener();
24764
+ removeIdleListener = null;
24765
+ }
24766
+ if (alignStartTimeout) {
24767
+ clearTimeout(alignStartTimeout);
24768
+ alignStartTimeout = null;
24769
+ }
24770
+ if (pollInterval) {
24771
+ clearInterval(pollInterval);
24772
+ pollInterval = null;
24773
+ }
24774
+ }
24526
24775
  const SAVE_DEBOUNCE_MS = 250;
24527
24776
  const PROFILE_FIELDS = [
24528
24777
  "label",
@@ -24920,6 +25169,7 @@ function registerAutofillHandlers(windowState) {
24920
25169
  }
24921
25170
  function registerPageDiffHandlers(windowState, sendToRendererViews) {
24922
25171
  const pageEventBuckets = /* @__PURE__ */ new Map();
25172
+ const isActiveWebContents = (webContentsId) => windowState.tabManager.getActiveTab()?.view.webContents.id === webContentsId;
24923
25173
  const allowPageEvent = (webContentsId) => {
24924
25174
  const now = Date.now();
24925
25175
  const bucket = pageEventBuckets.get(webContentsId);
@@ -24956,14 +25206,20 @@ function registerPageDiffHandlers(windowState, sendToRendererViews) {
24956
25206
  if (!wc || wc.isDestroyed()) return;
24957
25207
  if (!isManagedTabIpcSender(event, windowState.tabManager)) return;
24958
25208
  if (!allowPageEvent(wc.id)) return;
24959
- notePageMutationActivity(wc, sendToRendererViews);
25209
+ invalidateExtractionCache(wc);
25210
+ notePageMutationActivity(wc, sendToRendererViews, {
25211
+ isActive: () => isActiveWebContents(wc.id)
25212
+ });
24960
25213
  });
24961
25214
  electron.ipcMain.on(Channels.PAGE_DIFF_DIRTY, (event) => {
24962
25215
  const wc = event.sender;
24963
25216
  if (!wc || wc.isDestroyed()) return;
24964
25217
  if (!isManagedTabIpcSender(event, windowState.tabManager)) return;
24965
25218
  if (!allowPageEvent(wc.id)) return;
24966
- schedulePageSnapshotCapture(wc, sendToRendererViews);
25219
+ invalidateExtractionCache(wc);
25220
+ schedulePageSnapshotCapture(wc, sendToRendererViews, 0, {
25221
+ isActive: () => isActiveWebContents(wc.id)
25222
+ });
24967
25223
  });
24968
25224
  }
24969
25225
  function renderReportAsMarkdown(report, traces) {
@@ -25232,9 +25488,40 @@ RULES:
25232
25488
  }
25233
25489
  const logger$7 = createLogger("ResearchOrchestrator");
25234
25490
  const MAX_THREADS = 5;
25491
+ const MAX_TRACE_ARGS_CHARS = 1200;
25492
+ const MAX_TRACE_RESULT_CHARS = 2e3;
25235
25493
  function clone$1(value) {
25236
25494
  return structuredClone(value);
25237
25495
  }
25496
+ function truncateTraceText(value, maxLength) {
25497
+ if (value.length <= maxLength) return value;
25498
+ return `${value.slice(0, maxLength - 3).trimEnd()}...`;
25499
+ }
25500
+ function slimTraceArgs(args) {
25501
+ const json = JSON.stringify(args);
25502
+ if (json.length <= MAX_TRACE_ARGS_CHARS) return clone$1(args);
25503
+ return {
25504
+ _truncated: true,
25505
+ originalChars: json.length,
25506
+ preview: truncateTraceText(json, MAX_TRACE_ARGS_CHARS)
25507
+ };
25508
+ }
25509
+ function slimTraceResult(result) {
25510
+ if (result.length <= MAX_TRACE_RESULT_CHARS) return result;
25511
+ return [
25512
+ `[Trace result truncated from ${result.length} chars.]`,
25513
+ truncateTraceText(result, MAX_TRACE_RESULT_CHARS)
25514
+ ].join("\n");
25515
+ }
25516
+ function createTraceToolCall(tool, args, result, startedAt) {
25517
+ return {
25518
+ tool,
25519
+ args: slimTraceArgs(args),
25520
+ result: slimTraceResult(result),
25521
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25522
+ durationMs: Date.now() - startedAt
25523
+ };
25524
+ }
25238
25525
  function normalizeSourceDomain(value) {
25239
25526
  const trimmed = value.trim().toLowerCase();
25240
25527
  if (!trimmed) return "";
@@ -25643,13 +25930,7 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25643
25930
  title: String(args.url || "excluded source"),
25644
25931
  reason: msg
25645
25932
  });
25646
- trace.toolCalls.push({
25647
- tool: name,
25648
- args,
25649
- result: msg,
25650
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25651
- durationMs: 0
25652
- });
25933
+ trace.toolCalls.push(createTraceToolCall(name, args, msg, t0));
25653
25934
  return msg;
25654
25935
  }
25655
25936
  }
@@ -25657,25 +25938,13 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25657
25938
  sourcesConsumed++;
25658
25939
  if (sourcesConsumed > thread.sourceBudget) {
25659
25940
  const msg = `Source budget (${thread.sourceBudget}) exceeded. Summarize findings and stop.`;
25660
- trace.toolCalls.push({
25661
- tool: name,
25662
- args,
25663
- result: msg,
25664
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25665
- durationMs: 0
25666
- });
25941
+ trace.toolCalls.push(createTraceToolCall(name, args, msg, t0));
25667
25942
  return msg;
25668
25943
  }
25669
25944
  }
25670
25945
  try {
25671
25946
  const output = await executeAction(name, args, actionCtx);
25672
- trace.toolCalls.push({
25673
- tool: name,
25674
- args,
25675
- result: output,
25676
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25677
- durationMs: Date.now() - t0
25678
- });
25947
+ trace.toolCalls.push(createTraceToolCall(name, args, output, t0));
25679
25948
  return output;
25680
25949
  } catch (err) {
25681
25950
  const msg = err instanceof Error ? err.message : String(err);
@@ -25690,13 +25959,7 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25690
25959
  message: msg,
25691
25960
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
25692
25961
  });
25693
- trace.toolCalls.push({
25694
- tool: name,
25695
- args,
25696
- result: `Error: ${msg}`,
25697
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25698
- durationMs: Date.now() - t0
25699
- });
25962
+ trace.toolCalls.push(createTraceToolCall(name, args, `Error: ${msg}`, t0));
25700
25963
  return `Error: ${msg}`;
25701
25964
  }
25702
25965
  },
@@ -25916,15 +26179,20 @@ ${transcript.slice(0, 32e3)}`;
25916
26179
  this.emit();
25917
26180
  }
25918
26181
  }
26182
+ function assertVaultUnlocked() {
26183
+ assertFeatureUnlocked("vault", "Agent Credential Vault");
26184
+ }
25919
26185
  function registerVaultHandlers() {
25920
26186
  electron.ipcMain.handle(Channels.VAULT_LIST, (event) => {
25921
26187
  assertTrustedIpcSender(event);
26188
+ assertVaultUnlocked();
25922
26189
  return listEntries$1();
25923
26190
  });
25924
26191
  electron.ipcMain.handle(
25925
26192
  Channels.VAULT_ADD,
25926
26193
  (event, entry) => {
25927
26194
  assertTrustedIpcSender(event);
26195
+ assertVaultUnlocked();
25928
26196
  if (!entry || typeof entry !== "object") {
25929
26197
  throw new Error("Invalid vault entry");
25930
26198
  }
@@ -25951,6 +26219,7 @@ function registerVaultHandlers() {
25951
26219
  Channels.VAULT_UPDATE,
25952
26220
  (event, id, updates) => {
25953
26221
  assertTrustedIpcSender(event);
26222
+ assertVaultUnlocked();
25954
26223
  assertString(id, "id");
25955
26224
  if (!updates || typeof updates !== "object") {
25956
26225
  throw new Error("Invalid updates");
@@ -25960,12 +26229,14 @@ function registerVaultHandlers() {
25960
26229
  );
25961
26230
  electron.ipcMain.handle(Channels.VAULT_REMOVE, (event, id) => {
25962
26231
  assertTrustedIpcSender(event);
26232
+ assertVaultUnlocked();
25963
26233
  assertString(id, "id");
25964
26234
  trackVaultAction("credential_removed");
25965
26235
  return removeEntry$1(id);
25966
26236
  });
25967
26237
  electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (event, limit) => {
25968
26238
  assertTrustedIpcSender(event);
26239
+ assertVaultUnlocked();
25969
26240
  return readAuditLog$1(limit);
25970
26241
  });
25971
26242
  }
@@ -25983,14 +26254,19 @@ function normalizeTags(value) {
25983
26254
  const tags = value.filter((tag) => typeof tag === "string").map((tag) => tag.trim()).filter(Boolean);
25984
26255
  return tags.length > 0 ? tags : void 0;
25985
26256
  }
26257
+ function assertHumanVaultUnlocked() {
26258
+ assertFeatureUnlocked("human_vault", "Passwords");
26259
+ }
25986
26260
  function registerHumanVaultHandlers() {
25987
26261
  electron.ipcMain.handle(Channels.HUMAN_VAULT_LIST, (event, domain) => {
25988
26262
  assertTrustedIpcSender(event);
26263
+ assertHumanVaultUnlocked();
25989
26264
  if (domain !== void 0) assertString(domain, "domain");
25990
26265
  return domain ? findForDomain(domain) : listEntries();
25991
26266
  });
25992
26267
  electron.ipcMain.handle(Channels.HUMAN_VAULT_GET, (event, id) => {
25993
26268
  assertTrustedIpcSender(event);
26269
+ assertHumanVaultUnlocked();
25994
26270
  assertString(id, "id");
25995
26271
  return getEntrySafe(id);
25996
26272
  });
@@ -25998,6 +26274,7 @@ function registerHumanVaultHandlers() {
25998
26274
  Channels.HUMAN_VAULT_SAVE,
25999
26275
  (event, input) => {
26000
26276
  assertTrustedIpcSender(event);
26277
+ assertHumanVaultUnlocked();
26001
26278
  if (!input || typeof input !== "object") {
26002
26279
  throw new Error("Invalid credential entry");
26003
26280
  }
@@ -26029,6 +26306,7 @@ function registerHumanVaultHandlers() {
26029
26306
  Channels.HUMAN_VAULT_UPDATE,
26030
26307
  (event, id, updates) => {
26031
26308
  assertTrustedIpcSender(event);
26309
+ assertHumanVaultUnlocked();
26032
26310
  assertString(id, "id");
26033
26311
  if (!updates || typeof updates !== "object") {
26034
26312
  throw new Error("Invalid updates");
@@ -26072,11 +26350,13 @@ function registerHumanVaultHandlers() {
26072
26350
  );
26073
26351
  electron.ipcMain.handle(Channels.HUMAN_VAULT_REMOVE, (event, id) => {
26074
26352
  assertTrustedIpcSender(event);
26353
+ assertHumanVaultUnlocked();
26075
26354
  assertString(id, "id");
26076
26355
  return removeEntry(id);
26077
26356
  });
26078
26357
  electron.ipcMain.handle(Channels.HUMAN_VAULT_AUDIT_LOG, (event, limit) => {
26079
26358
  assertTrustedIpcSender(event);
26359
+ assertHumanVaultUnlocked();
26080
26360
  return readAuditLog(limit);
26081
26361
  });
26082
26362
  }
@@ -27212,6 +27492,10 @@ function registerHistoryHandlers() {
27212
27492
  assertTrustedIpcSender(event);
27213
27493
  return getState$1();
27214
27494
  });
27495
+ electron.ipcMain.handle(Channels.HISTORY_LIST, (event, offset, limit) => {
27496
+ assertTrustedIpcSender(event);
27497
+ return listEntries$2(offset, limit);
27498
+ });
27215
27499
  electron.ipcMain.handle(Channels.HISTORY_SEARCH, (event, query) => {
27216
27500
  assertTrustedIpcSender(event);
27217
27501
  return search(query);
@@ -27366,7 +27650,7 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
27366
27650
  electron.ipcMain.handle(Channels.PREMIUM_ACTIVATION_START, async (event, email) => {
27367
27651
  assertTrustedIpcSender(event);
27368
27652
  assertString(email, "email");
27369
- if (!isValidEmail(email)) {
27653
+ if (!isValidEmail$1(email)) {
27370
27654
  return errorResult("Invalid email format");
27371
27655
  }
27372
27656
  trackPremiumFunnel("activation_attempted");
@@ -27393,7 +27677,7 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
27393
27677
  assertString(email, "email");
27394
27678
  assertString(code, "code");
27395
27679
  assertString(challengeToken, "challengeToken");
27396
- if (!isValidEmail(email)) {
27680
+ if (!isValidEmail$1(email)) {
27397
27681
  return errorResult("Invalid email format", {
27398
27682
  state: getPremiumState()
27399
27683
  });
@@ -27597,6 +27881,47 @@ function registerCodexHandlers() {
27597
27881
  return { ok: true };
27598
27882
  });
27599
27883
  }
27884
+ const SUPPORT_API = process.env.VESSEL_SUPPORT_API || process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
27885
+ const MAX_FEEDBACK_MESSAGE_LENGTH = 5e3;
27886
+ const FEEDBACK_REQUEST_TIMEOUT_MS = 15e3;
27887
+ function isValidEmail(email) {
27888
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
27889
+ }
27890
+ async function submitFeedback(payload) {
27891
+ const email = payload.email.trim().toLowerCase();
27892
+ const message = payload.message.trim();
27893
+ if (!isValidEmail(email)) {
27894
+ return errorResult("Enter a valid reply email.");
27895
+ }
27896
+ if (!message) {
27897
+ return errorResult("Write a feedback message before sending.");
27898
+ }
27899
+ if (message.length > MAX_FEEDBACK_MESSAGE_LENGTH) {
27900
+ return errorResult(
27901
+ `Feedback must be ${MAX_FEEDBACK_MESSAGE_LENGTH.toLocaleString()} characters or less.`
27902
+ );
27903
+ }
27904
+ try {
27905
+ const signal = AbortSignal.timeout(FEEDBACK_REQUEST_TIMEOUT_MS);
27906
+ const res = await fetch(`${SUPPORT_API}/feedback`, {
27907
+ method: "POST",
27908
+ headers: { "Content-Type": "application/json" },
27909
+ signal,
27910
+ body: JSON.stringify({
27911
+ email,
27912
+ message,
27913
+ source: payload.source
27914
+ })
27915
+ });
27916
+ const data = await res.json().catch(() => ({}));
27917
+ if (!res.ok) {
27918
+ return errorResult(data.error || `HTTP ${res.status}`);
27919
+ }
27920
+ return okResult();
27921
+ } catch (error) {
27922
+ return errorResult(getErrorMessage(error, "Failed to send feedback."));
27923
+ }
27924
+ }
27600
27925
  const filePath = () => path$1.join(electron.app.getPath("userData"), "vessel-permissions.json");
27601
27926
  const ALLOWED_PERMISSION_TYPES = /* @__PURE__ */ new Set([
27602
27927
  "clipboard-read",
@@ -28110,7 +28435,7 @@ function registerIpcHandlers(windowState, runtime2) {
28110
28435
  () => sendToRendererViews(Channels.AI_STREAM_END, "completed"),
28111
28436
  tabManager,
28112
28437
  runtime2,
28113
- history,
28438
+ compactProviderHistory(history),
28114
28439
  researchOrchestrator
28115
28440
  );
28116
28441
  } catch (err) {
@@ -28262,6 +28587,12 @@ function registerIpcHandlers(windowState, runtime2) {
28262
28587
  requireTrusted(event);
28263
28588
  return regenerateMcpAuthToken();
28264
28589
  });
28590
+ electron.ipcMain.handle(Channels.SUPPORT_SUBMIT_FEEDBACK, async (event, email, message) => {
28591
+ requireTrusted(event);
28592
+ assertString(email, "email");
28593
+ assertString(message, "message");
28594
+ return submitFeedback({ email, message, source: "settings_account" });
28595
+ });
28265
28596
  electron.ipcMain.handle(Channels.SETTINGS_SET, async (event, key2, value) => {
28266
28597
  requireTrusted(event);
28267
28598
  assertString(key2, "key");
@@ -29903,7 +30234,7 @@ async function bootstrap() {
29903
30234
  );
29904
30235
  }
29905
30236
  };
29906
- const windowState = createMainWindow((tabs, activeId) => {
30237
+ const windowState = createMainWindow((tabs, activeId, meta) => {
29907
30238
  windowState.chromeView.webContents.send(
29908
30239
  Channels.TAB_STATE_UPDATE,
29909
30240
  tabs,
@@ -29911,7 +30242,9 @@ async function bootstrap() {
29911
30242
  );
29912
30243
  void syncActiveHighlightCount(windowState);
29913
30244
  layoutViews(windowState);
29914
- runtime?.onTabStateChanged();
30245
+ if (meta.persistSession) {
30246
+ runtime?.onTabStateChanged();
30247
+ }
29915
30248
  });
29916
30249
  let didRevealMainWindow = false;
29917
30250
  const revealMainWindow = () => {