@quanta-intellect/vessel-browser 0.1.94 → 0.1.97

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
@@ -15,6 +15,7 @@ const http$1 = require("node:http");
15
15
  const os = require("node:os");
16
16
  const mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
17
17
  const streamableHttp_js = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
18
+ const promises = require("fs/promises");
18
19
  function getEnvFlag(name) {
19
20
  const globalProcess = typeof globalThis === "object" && "process" in globalThis ? globalThis.process : void 0;
20
21
  return globalProcess?.env?.[name];
@@ -79,7 +80,7 @@ const defaults = {
79
80
  const SAVE_DEBOUNCE_MS$6 = 150;
80
81
  const CHAT_PROVIDER_SECRET_FILENAME = "vessel-chat-provider-secret";
81
82
  const CODEX_TOKENS_FILENAME = "vessel-codex-tokens";
82
- const logger$n = createLogger("Settings");
83
+ const logger$p = createLogger("Settings");
83
84
  const SETTABLE_KEYS = new Set(Object.keys(defaults));
84
85
  let settings = null;
85
86
  let settingsIssues = [];
@@ -276,9 +277,10 @@ function persistNow() {
276
277
  return fs.promises.mkdir(path.dirname(getSettingsPath()), { recursive: true }).then(
277
278
  () => fs.promises.writeFile(
278
279
  getSettingsPath(),
279
- JSON.stringify(buildPersistedSettings(settings), null, 2)
280
+ JSON.stringify(buildPersistedSettings(settings), null, 2),
281
+ { encoding: "utf-8", mode: 384 }
280
282
  )
281
- ).catch((err) => logger$n.error("Failed to save settings:", err));
283
+ ).then(() => fs.promises.chmod(getSettingsPath(), 384).catch(() => void 0)).catch((err) => logger$p.error("Failed to save settings:", err));
282
284
  }
283
285
  function saveSettings() {
284
286
  saveDirty = true;
@@ -407,7 +409,7 @@ function loadTrustedAppURL(wc, url) {
407
409
  }
408
410
  const MAX_CUSTOM_HISTORY = 50;
409
411
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
410
- const logger$m = createLogger("Tab");
412
+ const logger$o = createLogger("Tab");
411
413
  const sessionCertExceptions = /* @__PURE__ */ new WeakMap();
412
414
  const sessionsWithVerifyProc = /* @__PURE__ */ new WeakSet();
413
415
  const CERT_VERIFY_TRUST = 0;
@@ -473,7 +475,7 @@ class Tab {
473
475
  guardedLoadURL(url, options) {
474
476
  const blockReason = this.getNavigationBlockReason(url);
475
477
  if (blockReason) {
476
- logger$m.warn(blockReason);
478
+ logger$o.warn(blockReason);
477
479
  return blockReason;
478
480
  }
479
481
  void this.view.webContents.loadURL(url, options);
@@ -557,7 +559,7 @@ class Tab {
557
559
  wc.setWindowOpenHandler(({ url, disposition }) => {
558
560
  const error = this.getNavigationBlockReason(url);
559
561
  if (error) {
560
- logger$m.warn(error);
562
+ logger$o.warn(error);
561
563
  return { action: "deny" };
562
564
  }
563
565
  this.onOpenUrl?.({
@@ -571,7 +573,7 @@ class Tab {
571
573
  const error = this.getNavigationBlockReason(url);
572
574
  if (!error) return;
573
575
  event.preventDefault();
574
- logger$m.warn(`${context}: ${error}`);
576
+ logger$o.warn(`${context}: ${error}`);
575
577
  };
576
578
  wc.on("will-navigate", (event, url) => {
577
579
  blockNavigation(event, url, "Blocked top-level navigation");
@@ -655,7 +657,7 @@ class Tab {
655
657
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 999px; }
656
658
  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
657
659
  ::-webkit-scrollbar-corner { background: transparent; }
658
- `).catch((err) => logger$m.warn("Failed to inject scrollbar CSS:", err));
660
+ `).catch((err) => logger$o.warn("Failed to inject scrollbar CSS:", err));
659
661
  });
660
662
  wc.on("page-favicon-updated", (_, favicons) => {
661
663
  this._state.favicon = favicons[0] || "";
@@ -691,7 +693,7 @@ class Tab {
691
693
  ).then((highlightedText) => {
692
694
  this.buildContextMenu(wc, params, highlightedText.trim());
693
695
  }).catch((err) => {
694
- logger$m.warn("Failed to inspect highlighted text for context menu:", err);
696
+ logger$o.warn("Failed to inspect highlighted text for context menu:", err);
695
697
  this.buildContextMenu(wc, params, "");
696
698
  });
697
699
  });
@@ -892,7 +894,7 @@ class Tab {
892
894
  "document.documentElement.outerHTML"
893
895
  );
894
896
  } catch (err) {
895
- logger$m.warn("Failed to retrieve page source:", err);
897
+ logger$o.warn("Failed to retrieve page source:", err);
896
898
  return;
897
899
  }
898
900
  const escaped = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1020,7 +1022,7 @@ class Tab {
1020
1022
  document.addEventListener('mouseup', window.__vesselHighlightHandler);
1021
1023
  }
1022
1024
  })()
1023
- `).catch((err) => logger$m.warn("Failed to inject highlight listener:", err));
1025
+ `).catch((err) => logger$o.warn("Failed to inject highlight listener:", err));
1024
1026
  } else {
1025
1027
  void wc.executeJavaScript(`
1026
1028
  (function() {
@@ -1031,7 +1033,7 @@ class Tab {
1031
1033
  delete window.__vesselHighlightHandler;
1032
1034
  }
1033
1035
  })()
1034
- `).catch((err) => logger$m.warn("Failed to remove highlight listener:", err));
1036
+ `).catch((err) => logger$o.warn("Failed to remove highlight listener:", err));
1035
1037
  }
1036
1038
  }
1037
1039
  get webContentsId() {
@@ -1068,7 +1070,7 @@ const SEARCH_ENGINE_PRESETS = {
1068
1070
  ecosia: { label: "Ecosia", url: "https://www.ecosia.org/search?q=" },
1069
1071
  kagi: { label: "Kagi", url: "https://kagi.com/search?q=" }
1070
1072
  };
1071
- const logger$l = createLogger("JsonPersistence");
1073
+ const logger$n = createLogger("JsonPersistence");
1072
1074
  function canUseSafeStorage() {
1073
1075
  try {
1074
1076
  return electron.safeStorage.isEncryptionAvailable();
@@ -1133,7 +1135,7 @@ function createDebouncedJsonPersistence({
1133
1135
  data,
1134
1136
  typeof data === "string" ? { encoding: "utf-8", mode: 384 } : { mode: 384 }
1135
1137
  )
1136
- ).catch((err) => logger$l.error(`Failed to save ${logLabel}:`, err));
1138
+ ).then(() => fs.promises.chmod(filePath2, 384).catch(() => void 0)).catch((err) => logger$n.error(`Failed to save ${logLabel}:`, err));
1137
1139
  };
1138
1140
  const schedule = () => {
1139
1141
  saveDirty2 = true;
@@ -2814,7 +2816,7 @@ function destroySession(tabId) {
2814
2816
  sessions.delete(tabId);
2815
2817
  }
2816
2818
  }
2817
- const logger$k = createLogger("TabManager");
2819
+ const logger$m = createLogger("TabManager");
2818
2820
  function sanitizePdfFilename(title) {
2819
2821
  const clean = title.replace(/[<>:"/\\|?*\x00-\x1f]/g, " ").replace(/\s+/g, " ").trim();
2820
2822
  const base = (clean || "Vessel Page").replace(/\.pdf$/i, "");
@@ -3213,7 +3215,7 @@ class TabManager {
3213
3215
  }));
3214
3216
  if (entries.length > 0) {
3215
3217
  void highlightBatchOnPage(wc, entries).catch(
3216
- (err) => logger$k.warn("Failed to batch highlight:", err)
3218
+ (err) => logger$m.warn("Failed to batch highlight:", err)
3217
3219
  );
3218
3220
  }
3219
3221
  }
@@ -3235,12 +3237,12 @@ class TabManager {
3235
3237
  const result = await captureSelectionHighlight(wc);
3236
3238
  if (result.success && result.text) {
3237
3239
  await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(
3238
- (err) => logger$k.warn("Failed to capture highlight:", err)
3240
+ (err) => logger$m.warn("Failed to capture highlight:", err)
3239
3241
  );
3240
3242
  }
3241
3243
  this.highlightCaptureCallback?.(result);
3242
3244
  } catch (err) {
3243
- logger$k.warn("Failed to capture highlight from page:", err);
3245
+ logger$m.warn("Failed to capture highlight from page:", err);
3244
3246
  this.highlightCaptureCallback?.({
3245
3247
  success: false,
3246
3248
  message: "Could not capture selection"
@@ -3265,7 +3267,7 @@ class TabManager {
3265
3267
  void this.removeHighlightMarksForText(wc, text);
3266
3268
  }
3267
3269
  } catch (err) {
3268
- logger$k.warn("Failed to remove highlight from matching tab:", err);
3270
+ logger$m.warn("Failed to remove highlight from matching tab:", err);
3269
3271
  }
3270
3272
  }
3271
3273
  this.highlightCaptureCallback?.({
@@ -3296,12 +3298,12 @@ class TabManager {
3296
3298
  void 0,
3297
3299
  color
3298
3300
  ).catch(
3299
- (err) => logger$k.warn("Failed to update highlight color:", err)
3301
+ (err) => logger$m.warn("Failed to update highlight color:", err)
3300
3302
  );
3301
3303
  });
3302
3304
  }
3303
3305
  } catch (err) {
3304
- logger$k.warn("Failed to iterate highlights for color change:", err);
3306
+ logger$m.warn("Failed to iterate highlights for color change:", err);
3305
3307
  }
3306
3308
  }
3307
3309
  this.highlightCaptureCallback?.({
@@ -3342,7 +3344,7 @@ class TabManager {
3342
3344
  });
3343
3345
  })()`
3344
3346
  ).catch(
3345
- (err) => logger$k.warn("Failed to remove highlight marks:", err)
3347
+ (err) => logger$m.warn("Failed to remove highlight marks:", err)
3346
3348
  );
3347
3349
  }
3348
3350
  broadcastState() {
@@ -3372,6 +3374,7 @@ const Channels = {
3372
3374
  AI_STREAM_CHUNK: "ai:stream-chunk",
3373
3375
  AI_STREAM_END: "ai:stream-end",
3374
3376
  AI_STREAM_IDLE: "ai:stream-idle",
3377
+ AI_RESEARCH_CLARIFICATION: "ai:research-clarification",
3375
3378
  AUTOMATION_ACTIVITY_START: "automation:activity-start",
3376
3379
  AUTOMATION_ACTIVITY_CHUNK: "automation:activity-chunk",
3377
3380
  AUTOMATION_ACTIVITY_END: "automation:activity-end",
@@ -3552,6 +3555,16 @@ const Channels = {
3552
3555
  CLEAR_BROWSING_DATA_OPEN: "browsing-data:open",
3553
3556
  // Picture-in-Picture
3554
3557
  TAB_TOGGLE_PIP: "tab:toggle-pip",
3558
+ // Research Desk
3559
+ RESEARCH_STATE_GET: "research:state-get",
3560
+ RESEARCH_STATE_UPDATE: "research:state-update",
3561
+ RESEARCH_START_BRIEF: "research:start-brief",
3562
+ RESEARCH_CONFIRM_BRIEF: "research:confirm-brief",
3563
+ RESEARCH_APPROVE_OBJECTIVES: "research:approve-objectives",
3564
+ RESEARCH_SET_MODE: "research:set-mode",
3565
+ RESEARCH_SET_TRACES: "research:set-traces",
3566
+ RESEARCH_CANCEL: "research:cancel",
3567
+ RESEARCH_EXPORT_REPORT: "research:export-report",
3555
3568
  // Codex OAuth
3556
3569
  CODEX_START_AUTH: "codex:start-auth",
3557
3570
  CODEX_CANCEL_AUTH: "codex:cancel-auth",
@@ -4521,7 +4534,7 @@ function errorResult(error, value) {
4521
4534
  function getErrorMessage(error, fallback = "Unknown error") {
4522
4535
  return error instanceof Error && error.message ? error.message : fallback;
4523
4536
  }
4524
- const logger$j = createLogger("Premium");
4537
+ const logger$l = createLogger("Premium");
4525
4538
  const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
4526
4539
  const FREE_TOOL_ITERATION_LIMIT = 50;
4527
4540
  const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
@@ -4561,7 +4574,10 @@ const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
4561
4574
  "vault_totp",
4562
4575
  "human_vault_list",
4563
4576
  "human_vault_fill",
4564
- "human_vault_remove"
4577
+ "human_vault_remove",
4578
+ "research_confirm_brief",
4579
+ "research_approve_objectives",
4580
+ "research_export_report"
4565
4581
  ]);
4566
4582
  function isPremium() {
4567
4583
  const { premium } = loadSettings();
@@ -4637,7 +4653,7 @@ async function verifySubscription$1(identifier) {
4637
4653
  });
4638
4654
  if (!res.ok) {
4639
4655
  const detail = await readApiErrorDetail(res);
4640
- logger$j.warn(
4656
+ logger$l.warn(
4641
4657
  "Verification API returned a non-OK status:",
4642
4658
  res.status,
4643
4659
  detail
@@ -4656,7 +4672,7 @@ async function verifySubscription$1(identifier) {
4656
4672
  setSetting("premium", updated);
4657
4673
  return updated;
4658
4674
  } catch (err) {
4659
- logger$j.warn("Verification failed:", err);
4675
+ logger$l.warn("Verification failed:", err);
4660
4676
  return current;
4661
4677
  }
4662
4678
  }
@@ -4760,6 +4776,21 @@ const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
4760
4776
  const BATCH_INTERVAL_MS = 6e4;
4761
4777
  const MAX_BATCH_SIZE = 50;
4762
4778
  const SENSITIVE_PROPERTY_RE = /url|uri|query|prompt|content|text|token|secret|key|password|credential|email|domain/i;
4779
+ const SENSITIVE_STRING_VALUE_RE = /https?:\/\/|www\.|[^\s@]+@[^\s@]+\.[^\s@]+|bearer\s+\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.|(?:sk|pk|rk|gh[pousr]|xox[baprs])-[-_A-Za-z0-9]{12,}/i;
4780
+ const EMPTY_PROPERTY_ALLOWLIST = /* @__PURE__ */ new Set();
4781
+ const TELEMETRY_PROPERTY_ALLOWLIST = {
4782
+ app_launched: /* @__PURE__ */ new Set(["electron_version", "chrome_version"]),
4783
+ app_session_ended: /* @__PURE__ */ new Set(["duration_minutes"]),
4784
+ tool_called: /* @__PURE__ */ new Set(["tool_name", "page_type"]),
4785
+ provider_configured: /* @__PURE__ */ new Set(["provider_id"]),
4786
+ page_type_detected: /* @__PURE__ */ new Set(["page_type"]),
4787
+ setting_changed: /* @__PURE__ */ new Set(["setting_key"]),
4788
+ approval_mode_changed: /* @__PURE__ */ new Set(["mode"]),
4789
+ bookmark_action: /* @__PURE__ */ new Set(["action"]),
4790
+ vault_action: /* @__PURE__ */ new Set(["action"]),
4791
+ extraction_failed: /* @__PURE__ */ new Set(["reason"]),
4792
+ premium_funnel: /* @__PURE__ */ new Set(["step", "status", "reason"])
4793
+ };
4763
4794
  function getDeviceIdPath() {
4764
4795
  return path.join(electron.app.getPath("userData"), ".vessel-device-id");
4765
4796
  }
@@ -4775,7 +4806,8 @@ function getDeviceId() {
4775
4806
  deviceId = crypto$1.randomUUID();
4776
4807
  try {
4777
4808
  fs.mkdirSync(path.dirname(idPath), { recursive: true });
4778
- fs.writeFileSync(idPath, deviceId, "utf-8");
4809
+ fs.writeFileSync(idPath, deviceId, { encoding: "utf-8", mode: 384 });
4810
+ fs.chmodSync(idPath, 384);
4779
4811
  } catch {
4780
4812
  }
4781
4813
  return deviceId;
@@ -4788,11 +4820,15 @@ function isEnabled() {
4788
4820
  if (process.env.VESSEL_DEV === "1") return false;
4789
4821
  return loadSettings().telemetryEnabled !== false;
4790
4822
  }
4791
- function sanitizeTelemetryProperties(properties) {
4823
+ function sanitizeTelemetryProperties(properties, allowedKeys) {
4792
4824
  const safe = {};
4793
4825
  for (const [key2, value] of Object.entries(properties)) {
4794
- if (SENSITIVE_PROPERTY_RE.test(key2)) continue;
4826
+ if (allowedKeys && !allowedKeys.has(key2)) continue;
4827
+ if (!allowedKeys?.has(key2) && SENSITIVE_PROPERTY_RE.test(key2)) continue;
4795
4828
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
4829
+ if (typeof value === "string" && SENSITIVE_STRING_VALUE_RE.test(value)) {
4830
+ continue;
4831
+ }
4796
4832
  safe[key2] = typeof value === "string" ? value.slice(0, 120) : value;
4797
4833
  }
4798
4834
  }
@@ -4800,10 +4836,11 @@ function sanitizeTelemetryProperties(properties) {
4800
4836
  }
4801
4837
  function trackEvent(event, properties = {}) {
4802
4838
  if (!isEnabled()) return;
4839
+ const allowedKeys = TELEMETRY_PROPERTY_ALLOWLIST[event] ?? EMPTY_PROPERTY_ALLOWLIST;
4803
4840
  eventQueue.push({
4804
4841
  event,
4805
4842
  properties: {
4806
- ...sanitizeTelemetryProperties(properties),
4843
+ ...sanitizeTelemetryProperties(properties, allowedKeys),
4807
4844
  premium_status: isPremium() ? "premium" : "free",
4808
4845
  app_version: electron.app.getVersion(),
4809
4846
  platform: process.platform,
@@ -5240,7 +5277,7 @@ const EXTRACT_TIMEOUT_MAX_MS = 2e4;
5240
5277
  const MUTATION_CAPTURE_INTERVAL_MS = 5e3;
5241
5278
  const MUTATION_SETTLE_AFTER_MS = 1500;
5242
5279
  const AGENT_STREAM_IDLE_TIMEOUT_MS = 3e4;
5243
- const logger$i = createLogger("Extractor");
5280
+ const logger$k = createLogger("Extractor");
5244
5281
  const EMPTY_PAGE_CONTENT = {
5245
5282
  title: "",
5246
5283
  content: "",
@@ -5966,7 +6003,8 @@ async function waitForDomReady(webContents, timeoutMs = DEFAULT_PAGE_SCRIPT_TIME
5966
6003
  (function() {
5967
6004
  return document.readyState || "";
5968
6005
  })()
5969
- `
6006
+ `,
6007
+ { label: "ready-state" }
5970
6008
  );
5971
6009
  if (readyState === "interactive" || readyState === "complete") {
5972
6010
  return;
@@ -5974,7 +6012,7 @@ async function waitForDomReady(webContents, timeoutMs = DEFAULT_PAGE_SCRIPT_TIME
5974
6012
  await delay(75);
5975
6013
  }
5976
6014
  }
5977
- async function executeScript(webContents, script) {
6015
+ async function executeScript(webContents, script, options = {}) {
5978
6016
  if (webContents.isDestroyed()) {
5979
6017
  return null;
5980
6018
  }
@@ -5990,7 +6028,15 @@ async function executeScript(webContents, script) {
5990
6028
  })
5991
6029
  ]);
5992
6030
  } catch (err) {
5993
- logger$i.warn("Failed to execute page script:", err);
6031
+ const label = options.label ? ` (${options.label})` : "";
6032
+ const url = webContents.getURL() || "unknown URL";
6033
+ const message = err instanceof Error ? err.message : String(err);
6034
+ const detail = `Failed to execute page script${label} on ${url}: ${message}`;
6035
+ if (options.warnOnFailure) {
6036
+ logger$k.warn(detail);
6037
+ } else {
6038
+ logger$k.debug(detail);
6039
+ }
5994
6040
  return null;
5995
6041
  } finally {
5996
6042
  if (timer) {
@@ -6087,7 +6133,8 @@ async function estimateExtractionTimeout(webContents) {
6087
6133
  try {
6088
6134
  const elementCount = await executeScript(
6089
6135
  webContents,
6090
- `(function() { try { return document.querySelectorAll('*').length; } catch { return 0; } })()`
6136
+ `(function() { try { return document.querySelectorAll('*').length; } catch { return 0; } })()`,
6137
+ { label: "element-count" }
6091
6138
  );
6092
6139
  if (typeof elementCount === "number" && elementCount > 5e3) {
6093
6140
  const extra = Math.min(
@@ -6097,15 +6144,19 @@ async function estimateExtractionTimeout(webContents) {
6097
6144
  return EXTRACT_TIMEOUT_BASE_MS + extra;
6098
6145
  }
6099
6146
  } catch (err) {
6100
- logger$i.warn("Failed to estimate extraction timeout, using base timeout:", err);
6147
+ logger$k.warn("Failed to estimate extraction timeout, using base timeout:", err);
6101
6148
  }
6102
6149
  return EXTRACT_TIMEOUT_BASE_MS;
6103
6150
  }
6104
6151
  async function extractContentInner(webContents) {
6105
6152
  await waitForDomReady(webContents);
6106
6153
  const [preloadResult, directResult] = await Promise.all([
6107
- executeScript(webContents, PRELOAD_EXTRACTION_SCRIPT),
6108
- executeScript(webContents, DIRECT_EXTRACTION_SCRIPT)
6154
+ executeScript(webContents, PRELOAD_EXTRACTION_SCRIPT, {
6155
+ label: "preload-extraction"
6156
+ }),
6157
+ executeScript(webContents, DIRECT_EXTRACTION_SCRIPT, {
6158
+ label: "direct-extraction"
6159
+ })
6109
6160
  ]);
6110
6161
  return mergePageContent(
6111
6162
  [preloadResult, directResult],
@@ -6985,8 +7036,9 @@ function isClickReadLoop(names) {
6985
7036
  }
6986
7037
  return clickReadPairs >= 2;
6987
7038
  }
7039
+ const TERMINAL_TOOL_RESULT = "__VESSEL_TERMINAL_TOOL_RESULT__";
6988
7040
  const ANTHROPIC_MAX_TOKENS = 4096;
6989
- function isRecord(value) {
7041
+ function isRecord$1(value) {
6990
7042
  return value !== null && typeof value === "object" && !Array.isArray(value);
6991
7043
  }
6992
7044
  function anthropicModelLikelySupportsThinking(model) {
@@ -7110,7 +7162,7 @@ class AnthropicProvider {
7110
7162
  } else if (event.type === "content_block_stop" && currentToolUse) {
7111
7163
  try {
7112
7164
  const input = JSON.parse(currentToolUse.inputJson || "{}");
7113
- if (!isRecord(input)) {
7165
+ if (!isRecord$1(input)) {
7114
7166
  throw new Error("Tool input must be a JSON object");
7115
7167
  }
7116
7168
  toolUseBlocks.push({
@@ -7188,6 +7240,9 @@ class AnthropicProvider {
7188
7240
  const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
7189
7241
  result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
7190
7242
  }
7243
+ if (result === TERMINAL_TOOL_RESULT) {
7244
+ return;
7245
+ }
7191
7246
  let parsedRich = null;
7192
7247
  try {
7193
7248
  const parsed = JSON.parse(result);
@@ -7456,7 +7511,7 @@ const MAX_MCP_NAV_CONTENT_LENGTH = 3e4;
7456
7511
  const MAX_AGENT_DEBUG_CONTENT_LENGTH = 2e4;
7457
7512
  const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
7458
7513
  const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
7459
- const logger$h = createLogger("OpenAIProvider");
7514
+ const logger$j = createLogger("OpenAIProvider");
7460
7515
  function shouldDebugAgentLoop() {
7461
7516
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
7462
7517
  return value === "1" || value === "true";
@@ -7981,9 +8036,9 @@ function resolveToolCallName(rawName, args, availableToolNames) {
7981
8036
  function logAgentLoopDebug(payload) {
7982
8037
  if (!shouldDebugAgentLoop()) return;
7983
8038
  try {
7984
- logger$h.info(`[agent-debug] ${JSON.stringify(payload)}`);
8039
+ logger$j.info(`[agent-debug] ${JSON.stringify(payload)}`);
7985
8040
  } catch (err) {
7986
- logger$h.warn("Failed to serialize debug payload:", err);
8041
+ logger$j.warn("Failed to serialize debug payload:", err);
7987
8042
  }
7988
8043
  }
7989
8044
  function recoverTextEncodedToolCalls(text, availableToolNames) {
@@ -8454,6 +8509,9 @@ class OpenAICompatProvider {
8454
8509
  const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
8455
8510
  result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
8456
8511
  }
8512
+ if (result === TERMINAL_TOOL_RESULT) {
8513
+ return;
8514
+ }
8457
8515
  let toolContent = result;
8458
8516
  try {
8459
8517
  const parsed = JSON.parse(result);
@@ -8523,12 +8581,15 @@ async function openExternalAllowlisted(url, rule) {
8523
8581
  if (!schemes.includes(parsed.protocol)) {
8524
8582
  throw new Error(`Blocked external URL scheme: ${parsed.protocol}`);
8525
8583
  }
8584
+ if (parsed.username || parsed.password) {
8585
+ throw new Error("Blocked external URL with embedded credentials");
8586
+ }
8526
8587
  if (rule.hosts && !rule.hosts.includes(parsed.hostname)) {
8527
8588
  throw new Error(`Blocked external URL host: ${parsed.hostname}`);
8528
8589
  }
8529
8590
  await electron.shell.openExternal(parsed.toString());
8530
8591
  }
8531
- const logger$g = createLogger("CodexOAuth");
8592
+ const logger$i = createLogger("CodexOAuth");
8532
8593
  const ISSUER = "https://auth.openai.com";
8533
8594
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
8534
8595
  const SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
@@ -8593,54 +8654,6 @@ function parseTokenExpiry(accessToken) {
8593
8654
  }
8594
8655
  return Date.now() + 36e5;
8595
8656
  }
8596
- async function exchangeIdTokenForApiKey(idToken) {
8597
- const body = new URLSearchParams({
8598
- grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
8599
- client_id: CLIENT_ID,
8600
- requested_token: "openai-api-key",
8601
- subject_token: idToken,
8602
- subject_token_type: "urn:ietf:params:oauth:token-type:id_token"
8603
- });
8604
- const response = await fetch(`${ISSUER}/oauth/token`, {
8605
- method: "POST",
8606
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
8607
- body: body.toString()
8608
- });
8609
- if (!response.ok) {
8610
- let errorMsg = `OpenAI API token exchange failed: ${response.status}`;
8611
- try {
8612
- const err = await response.json();
8613
- if (typeof err.error_description === "string") {
8614
- errorMsg = err.error_description;
8615
- } else if (typeof err.error === "string") {
8616
- errorMsg = err.error;
8617
- }
8618
- } catch {
8619
- }
8620
- throw new Error(errorMsg);
8621
- }
8622
- const data = await response.json();
8623
- if (!data.access_token) {
8624
- throw new Error("OpenAI API token exchange did not return an access token");
8625
- }
8626
- return data.access_token;
8627
- }
8628
- async function ensureCodexApiKey(tokens) {
8629
- if (tokens.apiKey) return tokens;
8630
- if (!tokens.idToken) return tokens;
8631
- try {
8632
- return {
8633
- ...tokens,
8634
- apiKey: await exchangeIdTokenForApiKey(tokens.idToken)
8635
- };
8636
- } catch (err) {
8637
- logger$g.warn(
8638
- "Codex API-key token exchange failed; continuing with ChatGPT OAuth tokens:",
8639
- err
8640
- );
8641
- return tokens;
8642
- }
8643
- }
8644
8657
  async function exchangeCodeForTokens(code, redirectUri, codeVerifier) {
8645
8658
  const body = new URLSearchParams({
8646
8659
  grant_type: "authorization_code",
@@ -8678,7 +8691,7 @@ async function exchangeCodeForTokens(code, redirectUri, codeVerifier) {
8678
8691
  accountId: claims?.accountId || "",
8679
8692
  accountEmail: claims?.email
8680
8693
  };
8681
- return ensureCodexApiKey(tokens);
8694
+ return tokens;
8682
8695
  }
8683
8696
  async function refreshAccessToken(tokens) {
8684
8697
  const body = new URLSearchParams({
@@ -8714,7 +8727,7 @@ async function refreshAccessToken(tokens) {
8714
8727
  accountId: claims?.accountId || tokens.accountId || "",
8715
8728
  accountEmail: claims?.email || tokens.accountEmail
8716
8729
  };
8717
- return ensureCodexApiKey(refreshedTokens);
8730
+ return refreshedTokens;
8718
8731
  }
8719
8732
  function startServer(port, pkce, expectedState, resolve, reject) {
8720
8733
  const server = http.createServer(async (req, res) => {
@@ -8825,7 +8838,7 @@ async function startCodexOAuth(onStatus) {
8825
8838
  try {
8826
8839
  onStatus(status, error);
8827
8840
  } catch {
8828
- logger$g.warn("Codex OAuth status callback failed — window may be closed");
8841
+ logger$i.warn("Codex OAuth status callback failed — window may be closed");
8829
8842
  }
8830
8843
  };
8831
8844
  const wrappedResolve = (tokens) => {
@@ -8865,7 +8878,7 @@ async function startCodexOAuth(onStatus) {
8865
8878
  const authUrl = buildAuthorizeUrl(port, pkce, state2);
8866
8879
  safeOnStatus("waiting");
8867
8880
  openExternalAllowlisted(authUrl, { hosts: ["auth.openai.com"] }).catch((err) => {
8868
- logger$g.warn("Failed to open browser, user will need the URL:", err);
8881
+ logger$i.warn("Failed to open browser, user will need the URL:", err);
8869
8882
  });
8870
8883
  }).catch(wrappedReject);
8871
8884
  });
@@ -8877,20 +8890,68 @@ function cancelCodexOAuth() {
8877
8890
  try {
8878
8891
  activeFlow.onStatus("idle");
8879
8892
  } catch {
8880
- logger$g.warn("Codex OAuth cancel status callback failed — window may be closed");
8893
+ logger$i.warn("Codex OAuth cancel status callback failed — window may be closed");
8881
8894
  }
8882
8895
  activeFlow = null;
8883
8896
  }
8884
- const logger$f = createLogger("CodexProvider");
8897
+ const logger$h = createLogger("CodexProvider");
8885
8898
  const REFRESH_WINDOW_MS = 5 * 60 * 1e3;
8886
8899
  const CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api/codex";
8887
8900
  const CODEX_CLIENT_VERSION = "0.129.0";
8901
+ function isRecord(value) {
8902
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8903
+ }
8904
+ async function createCodexFunctionCallOutput(functionCall, availableToolNames, onChunk, onToolCall) {
8905
+ const callId = functionCall.call_id || functionCall.id || "";
8906
+ const name = functionCall.name || "";
8907
+ if (!callId) {
8908
+ return {
8909
+ type: "function_call_output",
8910
+ call_id: callId,
8911
+ output: "Error: Function call was missing a call_id. Please retry the tool call."
8912
+ };
8913
+ }
8914
+ if (!name || !availableToolNames.has(name)) {
8915
+ onChunk(`
8916
+ <<tool:${name || "unknown"}:⚠ unsupported>>
8917
+ `);
8918
+ return {
8919
+ type: "function_call_output",
8920
+ call_id: callId,
8921
+ output: `Error: Unsupported tool${name ? `: ${name}` : ""}. Use one of the provided tools.`
8922
+ };
8923
+ }
8924
+ let args;
8925
+ try {
8926
+ const parsed = JSON.parse(functionCall.arguments || "{}");
8927
+ if (!isRecord(parsed)) throw new Error("Tool arguments must be a JSON object");
8928
+ args = parsed;
8929
+ } catch {
8930
+ onChunk(`
8931
+ <<tool:${name}:⚠ invalid args>>
8932
+ `);
8933
+ return {
8934
+ type: "function_call_output",
8935
+ call_id: callId,
8936
+ output: "Error: Invalid JSON in tool arguments. Please retry with a valid JSON object."
8937
+ };
8938
+ }
8939
+ const output = await onToolCall(name, args);
8940
+ if (output === TERMINAL_TOOL_RESULT) {
8941
+ return { terminal: true };
8942
+ }
8943
+ return {
8944
+ type: "function_call_output",
8945
+ call_id: callId,
8946
+ output
8947
+ };
8948
+ }
8888
8949
  class CodexProvider {
8889
8950
  agentToolProfile;
8890
8951
  tokens;
8891
8952
  model;
8892
8953
  abortController = null;
8893
- constructor(tokens, model, _baseUrl) {
8954
+ constructor(tokens, model) {
8894
8955
  this.tokens = tokens;
8895
8956
  this.model = model;
8896
8957
  this.agentToolProfile = "default";
@@ -8898,7 +8959,7 @@ class CodexProvider {
8898
8959
  async ensureFreshTokens() {
8899
8960
  if (Date.now() < this.tokens.expiresAt - REFRESH_WINDOW_MS) return;
8900
8961
  try {
8901
- logger$f.info("Refreshing Codex access token");
8962
+ logger$h.info("Refreshing Codex access token");
8902
8963
  const fresh = await refreshAccessToken(this.tokens);
8903
8964
  this.tokens = fresh;
8904
8965
  writeStoredCodexTokens(fresh);
@@ -8909,7 +8970,7 @@ class CodexProvider {
8909
8970
  );
8910
8971
  }
8911
8972
  }
8912
- backendHeaders() {
8973
+ backendHeaders(turnState) {
8913
8974
  const headers = {
8914
8975
  Authorization: `Bearer ${this.tokens.accessToken}`,
8915
8976
  "Content-Type": "application/json",
@@ -8920,6 +8981,9 @@ class CodexProvider {
8920
8981
  if (this.tokens.accountId) {
8921
8982
  headers["ChatGPT-Account-ID"] = this.tokens.accountId;
8922
8983
  }
8984
+ if (turnState) {
8985
+ headers["x-codex-turn-state"] = turnState;
8986
+ }
8923
8987
  return headers;
8924
8988
  }
8925
8989
  buildInput(userMessage, history) {
@@ -8928,7 +8992,7 @@ class CodexProvider {
8928
8992
  input.push({
8929
8993
  type: "message",
8930
8994
  role: msg.role,
8931
- content: [{ type: "input_text", text: msg.content }]
8995
+ content: [{ type: msg.role === "assistant" ? "output_text" : "input_text", text: msg.content }]
8932
8996
  });
8933
8997
  }
8934
8998
  input.push({
@@ -8938,7 +9002,7 @@ class CodexProvider {
8938
9002
  });
8939
9003
  return input;
8940
9004
  }
8941
- handleStreamEvent(raw, onChunk, emittedTextFromDelta) {
9005
+ handleStreamEvent(raw, onChunk, acc) {
8942
9006
  if (!raw.trim() || raw.trim() === "[DONE]") return;
8943
9007
  let event;
8944
9008
  try {
@@ -8947,13 +9011,28 @@ class CodexProvider {
8947
9011
  return;
8948
9012
  }
8949
9013
  if (event.type === "response.output_text.delta" && event.delta) {
8950
- emittedTextFromDelta.value = true;
9014
+ acc.emittedTextFromDelta = true;
9015
+ acc.text += event.delta;
8951
9016
  onChunk(event.delta);
8952
9017
  return;
8953
9018
  }
8954
- if (event.type === "response.output_item.done" && !emittedTextFromDelta.value) {
8955
- const text = event.item?.content?.filter((item) => item.type === "output_text" && item.text).map((item) => item.text).join("");
8956
- if (text) onChunk(text);
9019
+ if (event.type === "response.function_call_arguments.delta" && event.delta) {
9020
+ const key2 = event.call_id || event.item_id || "";
9021
+ if (key2) {
9022
+ acc.functionCallArgs.set(key2, (acc.functionCallArgs.get(key2) || "") + event.delta);
9023
+ }
9024
+ return;
9025
+ }
9026
+ if (event.type === "response.output_item.done" && event.item) {
9027
+ const item = event.item;
9028
+ if (item.type === "function_call") {
9029
+ const key2 = item.call_id || item.id || "";
9030
+ const args = acc.functionCallArgs.get(key2) || item.arguments || "";
9031
+ acc.functionCallArgs.delete(key2);
9032
+ acc.items.push({ ...item, arguments: args });
9033
+ } else if (item.type === "message") {
9034
+ acc.items.push(item);
9035
+ }
8957
9036
  return;
8958
9037
  }
8959
9038
  if (event.type === "response.failed") {
@@ -8962,18 +9041,12 @@ class CodexProvider {
8962
9041
  throw new Error(message);
8963
9042
  }
8964
9043
  }
8965
- async streamCodexResponse(systemPrompt, userMessage, onChunk, history) {
9044
+ async streamCodexResponse(requestBody, onChunk, turnState) {
8966
9045
  const response = await fetch(`${CODEX_BACKEND_BASE_URL}/responses`, {
8967
9046
  method: "POST",
8968
- headers: this.backendHeaders(),
9047
+ headers: this.backendHeaders(turnState),
8969
9048
  signal: this.abortController?.signal,
8970
- body: JSON.stringify({
8971
- model: this.model,
8972
- instructions: systemPrompt,
8973
- input: this.buildInput(userMessage, history),
8974
- stream: true,
8975
- store: false
8976
- })
9049
+ body: JSON.stringify(requestBody)
8977
9050
  });
8978
9051
  if (!response.ok) {
8979
9052
  const text = await response.text().catch(() => "");
@@ -8984,37 +9057,57 @@ class CodexProvider {
8984
9057
  if (!response.body) {
8985
9058
  throw new Error("Codex backend returned an empty response stream");
8986
9059
  }
9060
+ const newTurnState = response.headers.get("x-codex-turn-state") || null;
8987
9061
  const reader = response.body.getReader();
8988
- const decoder = new TextDecoder();
8989
- let buffer = "";
8990
- const emittedTextFromDelta = { value: false };
8991
- while (true) {
8992
- const { value, done } = await reader.read();
8993
- if (done) break;
8994
- buffer += decoder.decode(value, { stream: true });
8995
- let separatorIndex;
8996
- while ((separatorIndex = buffer.indexOf("\n\n")) !== -1) {
8997
- const block = buffer.slice(0, separatorIndex);
8998
- buffer = buffer.slice(separatorIndex + 2);
8999
- const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
9000
- this.handleStreamEvent(data, onChunk, emittedTextFromDelta);
9001
- }
9002
- }
9003
- const trailing = buffer.trim();
9004
- if (trailing) {
9005
- const data = trailing.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
9006
- this.handleStreamEvent(data, onChunk, emittedTextFromDelta);
9062
+ try {
9063
+ const decoder = new TextDecoder();
9064
+ let buffer = "";
9065
+ const acc = {
9066
+ text: "",
9067
+ items: [],
9068
+ emittedTextFromDelta: false,
9069
+ functionCallArgs: /* @__PURE__ */ new Map()
9070
+ };
9071
+ while (true) {
9072
+ const { value, done } = await reader.read();
9073
+ if (done) break;
9074
+ buffer += decoder.decode(value, { stream: true });
9075
+ let separatorIndex;
9076
+ while ((separatorIndex = buffer.indexOf("\n\n")) !== -1) {
9077
+ const block = buffer.slice(0, separatorIndex);
9078
+ buffer = buffer.slice(separatorIndex + 2);
9079
+ const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
9080
+ this.handleStreamEvent(data, onChunk, acc);
9081
+ }
9082
+ }
9083
+ const trailing = buffer.trim();
9084
+ if (trailing) {
9085
+ const data = trailing.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
9086
+ this.handleStreamEvent(data, onChunk, acc);
9087
+ }
9088
+ return { text: acc.text, items: acc.items, turnState: newTurnState };
9089
+ } finally {
9090
+ reader.releaseLock();
9007
9091
  }
9008
9092
  }
9009
9093
  async streamQuery(systemPrompt, userMessage, onChunk, onEnd, history) {
9010
9094
  await this.ensureFreshTokens();
9011
9095
  this.abortController = new AbortController();
9012
9096
  try {
9013
- await this.streamCodexResponse(systemPrompt, userMessage, onChunk, history);
9097
+ await this.streamCodexResponse(
9098
+ {
9099
+ model: this.model,
9100
+ instructions: systemPrompt,
9101
+ input: this.buildInput(userMessage, history),
9102
+ stream: true,
9103
+ store: false
9104
+ },
9105
+ onChunk
9106
+ );
9014
9107
  } catch (err) {
9015
9108
  if (err.name !== "AbortError") {
9016
9109
  const msg = err instanceof Error ? err.message : String(err);
9017
- logger$f.error("Codex streamQuery error:", err);
9110
+ logger$h.error("Codex streamQuery error:", err);
9018
9111
  onChunk(`
9019
9112
 
9020
9113
  [Error: ${msg}]`);
@@ -9024,15 +9117,65 @@ class CodexProvider {
9024
9117
  onEnd();
9025
9118
  }
9026
9119
  }
9027
- async streamAgentQuery(systemPrompt, userMessage, _tools, onChunk, _onToolCall, onEnd, history) {
9120
+ async streamAgentQuery(systemPrompt, userMessage, tools, onChunk, onToolCall, onEnd, history) {
9028
9121
  await this.ensureFreshTokens();
9029
9122
  this.abortController = new AbortController();
9123
+ const maxIterations = getEffectiveMaxIterations();
9124
+ const availableToolNames = new Set(tools.map((tool) => tool.name));
9125
+ let iterationsUsed = 0;
9126
+ const convertedTools = tools.map((tool) => ({
9127
+ type: "function",
9128
+ name: tool.name,
9129
+ description: tool.description || "",
9130
+ parameters: tool.input_schema
9131
+ }));
9132
+ let currentInput = this.buildInput(userMessage, history);
9133
+ let turnState = null;
9030
9134
  try {
9031
- await this.streamCodexResponse(systemPrompt, userMessage, onChunk, history);
9135
+ for (let i = 0; i < maxIterations; i++) {
9136
+ iterationsUsed = i + 1;
9137
+ const result = await this.streamCodexResponse(
9138
+ {
9139
+ model: this.model,
9140
+ instructions: systemPrompt,
9141
+ input: currentInput,
9142
+ tools: convertedTools,
9143
+ stream: true,
9144
+ store: false
9145
+ },
9146
+ onChunk,
9147
+ turnState || void 0
9148
+ );
9149
+ turnState = result.turnState || turnState;
9150
+ const functionCalls = result.items.filter(
9151
+ (item) => item.type === "function_call"
9152
+ );
9153
+ if (functionCalls.length === 0) {
9154
+ break;
9155
+ }
9156
+ currentInput = [];
9157
+ for (const fc of functionCalls) {
9158
+ const output = await createCodexFunctionCallOutput(
9159
+ fc,
9160
+ availableToolNames,
9161
+ onChunk,
9162
+ onToolCall
9163
+ );
9164
+ if ("terminal" in output) {
9165
+ return;
9166
+ }
9167
+ currentInput.push(output);
9168
+ }
9169
+ }
9170
+ if (iterationsUsed >= maxIterations) {
9171
+ onChunk(`
9172
+
9173
+ [Reached maximum tool call limit (${maxIterations} steps). You can adjust this in Settings → Max Tool Iterations, or continue by sending another message.]`);
9174
+ }
9032
9175
  } catch (err) {
9033
9176
  if (err.name !== "AbortError") {
9034
9177
  const msg = err instanceof Error ? err.message : String(err);
9035
- logger$f.error("Codex streamAgentQuery error:", err);
9178
+ logger$h.error("Codex streamAgentQuery error:", err);
9036
9179
  onChunk(`
9037
9180
 
9038
9181
  [Error: ${msg}]`);
@@ -9112,11 +9255,11 @@ function buildLlamaCppCtxWarning(ctxSize) {
9112
9255
  }
9113
9256
  async function fetchCodexBackendModels(tokens) {
9114
9257
  const url = new URL("https://chatgpt.com/backend-api/codex/models");
9115
- url.searchParams.set("client_version", "0.129.0");
9258
+ url.searchParams.set("client_version", CODEX_CLIENT_VERSION);
9116
9259
  const headers = {
9117
9260
  Authorization: `Bearer ${tokens.accessToken}`,
9118
9261
  originator: "codex_cli_rs",
9119
- "User-Agent": "codex_cli_rs/0.129.0 Vessel"
9262
+ "User-Agent": `codex_cli_rs/${CODEX_CLIENT_VERSION} Vessel`
9120
9263
  };
9121
9264
  if (tokens.accountId) {
9122
9265
  headers["ChatGPT-Account-ID"] = tokens.accountId;
@@ -9219,12 +9362,12 @@ function createProvider(config) {
9219
9362
  "OpenAI Codex requires authentication. Open settings to connect your ChatGPT account."
9220
9363
  );
9221
9364
  }
9222
- return new CodexProvider(tokens, normalized.model, normalized.baseUrl);
9365
+ return new CodexProvider(tokens, normalized.model);
9223
9366
  }
9224
9367
  return new OpenAICompatProvider(normalized);
9225
9368
  }
9226
9369
  const require$1 = node_module.createRequire(require("url").pathToFileURL(__filename).href);
9227
- const logger$e = createLogger("DevTrace");
9370
+ const logger$g = createLogger("DevTrace");
9228
9371
  let cachedFactory;
9229
9372
  function createNoopTraceSession() {
9230
9373
  return {
@@ -9257,7 +9400,7 @@ function loadLocalFactory() {
9257
9400
  return cachedFactory;
9258
9401
  }
9259
9402
  } catch (err) {
9260
- logger$e.warn("Failed to load local trace logger:", err);
9403
+ logger$g.warn("Failed to load local trace logger:", err);
9261
9404
  }
9262
9405
  }
9263
9406
  return cachedFactory;
@@ -13376,7 +13519,7 @@ function formatDeadLinkMessage(label, result) {
13376
13519
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
13377
13520
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
13378
13521
  }
13379
- const logger$d = createLogger("Screenshot");
13522
+ const logger$f = createLogger("Screenshot");
13380
13523
  const SCREENSHOT_RETRY_COUNT = 3;
13381
13524
  const SCREENSHOT_RETRY_BASE_DELAY_MS = 120;
13382
13525
  async function captureScreenshot(wc) {
@@ -13398,7 +13541,7 @@ async function captureScreenshot(wc) {
13398
13541
  }
13399
13542
  }
13400
13543
  } catch (err) {
13401
- logger$d.debug(
13544
+ logger$f.debug(
13402
13545
  `capturePage attempt ${attempt + 1} failed; retrying if attempts remain.`,
13403
13546
  getErrorMessage(err)
13404
13547
  );
@@ -14272,7 +14415,15 @@ function buildHuggingFaceSearchShortcut(currentUrl, rawQuery) {
14272
14415
  appliedFilters
14273
14416
  };
14274
14417
  }
14275
- const logger$c = createLogger("PageActions");
14418
+ class TabMutex {
14419
+ queue = Promise.resolve();
14420
+ enqueue(fn) {
14421
+ return new Promise((resolve, reject) => {
14422
+ this.queue = this.queue.then(fn).then(resolve, reject);
14423
+ });
14424
+ }
14425
+ }
14426
+ const logger$e = createLogger("PageActions");
14276
14427
  function getBookmarkMetadataFromArgs(args) {
14277
14428
  return normalizeBookmarkMetadata({
14278
14429
  intent: args.intent ?? args.intent,
@@ -14458,7 +14609,7 @@ async function executePageScript(wc, script, options) {
14458
14609
  return result;
14459
14610
  } catch (err) {
14460
14611
  const label = options?.label ? ` (${options.label})` : "";
14461
- logger$c.warn(`Failed to execute page script${label}:`, err);
14612
+ logger$e.warn(`Failed to execute page script${label}:`, err);
14462
14613
  return null;
14463
14614
  } finally {
14464
14615
  if (timer) {
@@ -14559,7 +14710,7 @@ Search results snapshot:
14559
14710
  ${truncated}`;
14560
14711
  }
14561
14712
  } catch (err) {
14562
- logger$c.warn("Failed to build post-search summary, falling back to nav summary:", err);
14713
+ logger$e.warn("Failed to build post-search summary, falling back to nav summary:", err);
14563
14714
  }
14564
14715
  const fallback = await getPostNavSummary(wc);
14565
14716
  return fallback ? `${fallback}
@@ -14582,7 +14733,7 @@ Page snapshot after navigation:
14582
14733
  ${truncated}`;
14583
14734
  }
14584
14735
  } catch (err) {
14585
- logger$c.warn("Failed to build post-click navigation summary:", err);
14736
+ logger$e.warn("Failed to build post-click navigation summary:", err);
14586
14737
  }
14587
14738
  return "";
14588
14739
  }
@@ -15076,7 +15227,7 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15076
15227
  }
15077
15228
  }
15078
15229
  } catch (err) {
15079
- logger$c.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
15230
+ logger$e.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
15080
15231
  }
15081
15232
  if (snapshot2.url && snapshot2.url !== wc.getURL()) {
15082
15233
  try {
@@ -15085,7 +15236,7 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15085
15236
  await waitForLoad(wc, 3e3);
15086
15237
  return;
15087
15238
  } catch (err) {
15088
- logger$c.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
15239
+ logger$e.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
15089
15240
  }
15090
15241
  }
15091
15242
  if (snapshot2.url) {
@@ -15093,7 +15244,7 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15093
15244
  await wc.reload();
15094
15245
  await waitForLoad(wc, 3e3);
15095
15246
  } catch (err) {
15096
- logger$c.warn("Failed to restore locale via page reload:", err);
15247
+ logger$e.warn("Failed to restore locale via page reload:", err);
15097
15248
  }
15098
15249
  }
15099
15250
  }
@@ -15445,7 +15596,7 @@ ${postActivationOverlayHint}`;
15445
15596
  return `${clickText} -> ${hrefFallbackUrl} (recovered via href fallback)`;
15446
15597
  }
15447
15598
  } catch (err) {
15448
- logger$c.warn("Failed href fallback after click, returning generic click result:", err);
15599
+ logger$e.warn("Failed href fallback after click, returning generic click result:", err);
15449
15600
  }
15450
15601
  }
15451
15602
  }
@@ -15490,7 +15641,7 @@ async function tryAutoDismissCartDialog(wc) {
15490
15641
  return result;
15491
15642
  }
15492
15643
  } catch (err) {
15493
- logger$c.warn("Failed to auto-dismiss cart dialog, falling back to dialog actions:", err);
15644
+ logger$e.warn("Failed to auto-dismiss cart dialog, falling back to dialog actions:", err);
15494
15645
  }
15495
15646
  return null;
15496
15647
  }
@@ -17434,6 +17585,19 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
17434
17585
  ]);
17435
17586
  async function executeAction(name, args, ctx) {
17436
17587
  name = normalizeToolAlias(name);
17588
+ if (ctx.tabId && ctx._tabMutex) {
17589
+ return ctx._tabMutex.enqueue(async () => {
17590
+ const prevActiveId = ctx.tabManager.getActiveTabId();
17591
+ if (prevActiveId !== ctx.tabId) ctx.tabManager.switchTab(ctx.tabId);
17592
+ try {
17593
+ return await executeAction(name, args, { ...ctx, tabId: void 0, _tabMutex: void 0 });
17594
+ } finally {
17595
+ if (prevActiveId && prevActiveId !== ctx.tabId) {
17596
+ ctx.tabManager.switchTab(prevActiveId);
17597
+ }
17598
+ }
17599
+ });
17600
+ }
17437
17601
  if (!KNOWN_TOOLS.has(name)) {
17438
17602
  for (const known of KNOWN_TOOLS) {
17439
17603
  if (name.startsWith(known) && name.length > known.length) {
@@ -17760,7 +17924,7 @@ async function executeAction(name, args, ctx) {
17760
17924
  )
17761
17925
  ]);
17762
17926
  } catch (err) {
17763
- logger$c.warn("Failed to extract content for read_page, falling back to lighter recovery:", err);
17927
+ logger$e.warn("Failed to extract content for read_page, falling back to lighter recovery:", err);
17764
17928
  content = null;
17765
17929
  }
17766
17930
  if (!content || content.content.length === 0) {
@@ -17777,12 +17941,12 @@ async function executeAction(name, args, ctx) {
17777
17941
  new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
17778
17942
  ]);
17779
17943
  } catch (err) {
17780
- logger$c.warn("Failed to re-extract content after iframe consent dismissal:", err);
17944
+ logger$e.warn("Failed to re-extract content after iframe consent dismissal:", err);
17781
17945
  content = null;
17782
17946
  }
17783
17947
  }
17784
17948
  } catch (err) {
17785
- logger$c.warn("Failed iframe consent dismissal during read_page recovery:", err);
17949
+ logger$e.warn("Failed iframe consent dismissal during read_page recovery:", err);
17786
17950
  }
17787
17951
  }
17788
17952
  if (content && content.content.length > 0) {
@@ -18195,7 +18359,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
18195
18359
  try {
18196
18360
  page = await extractContent(wc);
18197
18361
  } catch (err) {
18198
- logger$c.warn("Failed to extract content for suggest:", err);
18362
+ logger$e.warn("Failed to extract content for suggest:", err);
18199
18363
  return "Could not read page. Try navigate to a working URL.";
18200
18364
  }
18201
18365
  const suggestions = [];
@@ -18583,7 +18747,80 @@ WARNING: You have clicked ${clickStreakCount} elements on this page without veri
18583
18747
  }
18584
18748
  return formattedResult + await getPostActionState$1(ctx, name) + clickNavSummary + streakWarning + flowCtx;
18585
18749
  }
18586
- async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history) {
18750
+ function buildOrchestratorSystemPrompt() {
18751
+ return `You are the Research Captain of Vessel. You orchestrate deep research on behalf of the user.
18752
+
18753
+ YOUR ROLE:
18754
+ You are accountable for the final Research Report. The report has YOUR name on it. You do not blindly accept sub-agent findings — you review, challenge, and demand more when needed. You are the captain, and the sub-agents are your crew.
18755
+
18756
+ CORE PRINCIPLES:
18757
+ - You OWN the research question end-to-end. If the answer is insufficient, you dig deeper.
18758
+ - Every factual claim in your final report MUST be backed by a specific source URL and extracted quote. No citation = the claim does not survive synthesis.
18759
+ - You are authoritative but honest. Flag contradictions and gaps explicitly. Never invent to fill a hole.
18760
+
18761
+ BRIEF PHASE:
18762
+ Your first job is to interview the user. Ask one question at a time, and for EVERY question you MUST provide 2–6 concrete answer choices as a bullet list so the user can click instead of typing. Cover:
18763
+ - What exactly do they want to know?
18764
+ - How deep? How many sources?
18765
+ - Who is the report for? Technical or layperson?
18766
+ - Any domains to prefer or avoid?
18767
+ - What does a good answer look like?
18768
+
18769
+ If the user's question is vague, switch into EXPLORATION MODE: proactively suggest 2–3 concrete research angles they might be interested in. Help them discover what they actually want to know.
18770
+
18771
+ Never ask a bare question without listed options. Every assistant turn must end with a question and concrete answer choices.
18772
+
18773
+ You CANNOT navigate or use tools during the brief. The brief is dialogue only. When you are confident you have enough context, summarize what you heard and ask the user to confirm before moving to planning.
18774
+
18775
+ PLANNING PHASE:
18776
+ After the brief is confirmed, produce a structured Research Objectives document with 2–5 independent threads. Each thread gets a specific question, suggested search queries, and a source budget. Present this as a clear, structured card for the user to review, edit, or approve.
18777
+
18778
+ EXECUTION PHASE:
18779
+ Sub-agents run in parallel, each handling one thread. You monitor their progress. If a thread stalls or produces thin findings, rebalance — reassign effort, ask the sub-agent to dig deeper, or spawn a replacement.
18780
+
18781
+ SYNTHESIS PHASE:
18782
+ Before writing the report, self-audit: "Do I have enough to answer the research question? Am I confident in every claim?" If not, request more from sub-agents.
18783
+
18784
+ Write the report with:
18785
+ - An executive summary
18786
+ - One section per thread with sourced claims
18787
+ - Explicit contradictions and gaps
18788
+ - A numbered source index
18789
+
18790
+ Never use emojis. Be concise. Be precise.`;
18791
+ }
18792
+ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd, tabManager, runtime2, history, researchOrchestrator) {
18793
+ if (researchOrchestrator) {
18794
+ const researchState = researchOrchestrator.getState();
18795
+ if (researchState.phase === "briefing" || researchState.phase === "planning") {
18796
+ const isPlanning = researchState.phase === "planning";
18797
+ const phaseInstruction = isPlanning ? "\n\nNow produce the Research Objectives based on the brief conversation above. Output them as a JSON object with researchQuestion, threads (array of {label, question, searchQueries, sourceBudget}), audience, reportOutline, and totalSourceBudget fields." : "\n\nContinue the briefing interview. You MUST end every assistant turn with one concise question AND 2–6 concrete answer choices formatted as bullet points (e.g. `- Option one`). Never ask a bare question without listed options. Each option should read like something the user could click as their answer. Do not browse, plan the report, or write a prose preamble.";
18798
+ let fullResponse = "";
18799
+ const wrappedOnChunk = (text) => {
18800
+ fullResponse += text;
18801
+ if (text) onChunk(text);
18802
+ };
18803
+ const wrappedOnEnd = () => {
18804
+ if (isPlanning) {
18805
+ const parsed = researchOrchestrator.parseAndSetObjectives(fullResponse);
18806
+ if (!parsed) {
18807
+ onChunk(
18808
+ "\n\n[Failed to parse objectives. Please try confirming the brief again or refine your research question.]"
18809
+ );
18810
+ }
18811
+ }
18812
+ onEnd();
18813
+ };
18814
+ await provider.streamQuery(
18815
+ buildOrchestratorSystemPrompt() + phaseInstruction,
18816
+ query,
18817
+ wrappedOnChunk,
18818
+ wrappedOnEnd,
18819
+ history
18820
+ );
18821
+ return;
18822
+ }
18823
+ }
18587
18824
  const lowerQuery = query.toLowerCase().trim();
18588
18825
  const isSummarize = lowerQuery.startsWith("summarize") || lowerQuery.startsWith("tldr") || lowerQuery === "summary";
18589
18826
  if (provider.streamAgentQuery && tabManager && activeWebContents && runtime2) {
@@ -19520,10 +19757,20 @@ Exception: ${result.exceptionDetails}`);
19520
19757
  }
19521
19758
  );
19522
19759
  }
19523
- const logger$b = createLogger("VaultShared");
19760
+ const logger$d = createLogger("VaultShared");
19524
19761
  const ALGORITHM = "aes-256-gcm";
19525
19762
  const IV_LENGTH = 12;
19526
19763
  const AUTH_TAG_LENGTH = 16;
19764
+ const KEY_STORAGE_PREFIX = "base64:";
19765
+ function encodeEncryptionKeyForStorage(key2) {
19766
+ return `${KEY_STORAGE_PREFIX}${key2.toString("base64")}`;
19767
+ }
19768
+ function decodeEncryptionKeyFromStorage(value) {
19769
+ if (value.startsWith(KEY_STORAGE_PREFIX)) {
19770
+ return Buffer.from(value.slice(KEY_STORAGE_PREFIX.length), "base64");
19771
+ }
19772
+ return Buffer.from(value, "utf-8");
19773
+ }
19527
19774
  function assertSecretStorageAvailable(customMessage) {
19528
19775
  if (!electron.safeStorage.isEncryptionAvailable()) {
19529
19776
  throw new Error(
@@ -19536,11 +19783,17 @@ function getOrCreateEncryptionKey(keyFilename) {
19536
19783
  const keyPath = path$1.join(electron.app.getPath("userData"), keyFilename);
19537
19784
  if (fs$1.existsSync(keyPath)) {
19538
19785
  const encryptedKey = fs$1.readFileSync(keyPath);
19539
- return Buffer.from(electron.safeStorage.decryptString(encryptedKey), "utf-8");
19786
+ const key22 = decodeEncryptionKeyFromStorage(
19787
+ electron.safeStorage.decryptString(encryptedKey)
19788
+ );
19789
+ if (key22.length !== 32) {
19790
+ throw new Error("Stored vault encryption key has an invalid length.");
19791
+ }
19792
+ return key22;
19540
19793
  }
19541
19794
  const key2 = crypto$2.randomBytes(32);
19542
19795
  fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
19543
- const encrypted = electron.safeStorage.encryptString(key2.toString("utf-8"));
19796
+ const encrypted = electron.safeStorage.encryptString(encodeEncryptionKeyForStorage(key2));
19544
19797
  fs$1.writeFileSync(keyPath, encrypted, { mode: 384 });
19545
19798
  fs$1.chmodSync(keyPath, 384);
19546
19799
  return key2;
@@ -19598,7 +19851,7 @@ function createVaultIO(vaultFilename, encrypt2, decrypt2) {
19598
19851
  cachedEntries = JSON.parse(json);
19599
19852
  return cachedEntries;
19600
19853
  } catch (err) {
19601
- logger$b.error("Failed to load vault:", err);
19854
+ logger$d.error("Failed to load vault:", err);
19602
19855
  throw new Error("Could not unlock the vault. Check OS secret storage availability.");
19603
19856
  }
19604
19857
  }
@@ -19607,7 +19860,7 @@ function createVaultIO(vaultFilename, encrypt2, decrypt2) {
19607
19860
  const encrypted = encrypt2(json);
19608
19861
  const vaultPath = getVaultPath();
19609
19862
  fs$1.mkdirSync(path$1.dirname(vaultPath), { recursive: true });
19610
- fs$1.writeFileSync(vaultPath, encrypted);
19863
+ fs$1.writeFileSync(vaultPath, encrypted, { mode: 384 });
19611
19864
  fs$1.chmodSync(vaultPath, 384);
19612
19865
  cachedEntries = entries;
19613
19866
  }
@@ -19663,17 +19916,25 @@ function createAuditLog(filename, maxEntries) {
19663
19916
  try {
19664
19917
  const auditPath = getAuditPath2();
19665
19918
  fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
19666
- fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
19919
+ fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n", {
19920
+ encoding: "utf-8",
19921
+ mode: 384
19922
+ });
19923
+ fs$1.chmodSync(auditPath, 384);
19667
19924
  try {
19668
19925
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
19669
19926
  if (lines.length > maxEntries) {
19670
19927
  const trimmed = lines.slice(-maxEntries);
19671
- fs$1.writeFileSync(auditPath, trimmed.join("\n") + "\n");
19928
+ fs$1.writeFileSync(auditPath, trimmed.join("\n") + "\n", {
19929
+ encoding: "utf-8",
19930
+ mode: 384
19931
+ });
19932
+ fs$1.chmodSync(auditPath, 384);
19672
19933
  }
19673
19934
  } catch {
19674
19935
  }
19675
19936
  } catch (err) {
19676
- logger$b.error("Failed to write audit log:", err);
19937
+ logger$d.error("Failed to write audit log:", err);
19677
19938
  }
19678
19939
  }
19679
19940
  function readAuditLog2(limit = 100) {
@@ -19683,7 +19944,7 @@ function createAuditLog(filename, maxEntries) {
19683
19944
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
19684
19945
  return lines.slice(-Math.min(limit, maxEntries)).map((line) => JSON.parse(line)).reverse();
19685
19946
  } catch (err) {
19686
- logger$b.error("Failed to read audit log:", err);
19947
+ logger$d.error("Failed to read audit log:", err);
19687
19948
  return [];
19688
19949
  }
19689
19950
  }
@@ -19787,7 +20048,7 @@ async function requestConsent(request) {
19787
20048
  }
19788
20049
  const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
19789
20050
  const MAX_ENTRIES = 1e3;
19790
- const logger$a = createLogger("VaultAudit");
20051
+ const logger$c = createLogger("VaultAudit");
19791
20052
  function getAuditPath() {
19792
20053
  return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
19793
20054
  }
@@ -19795,9 +20056,13 @@ function appendAuditEntry(entry) {
19795
20056
  try {
19796
20057
  const auditPath = getAuditPath();
19797
20058
  fs$1.mkdirSync(path$1.dirname(auditPath), { recursive: true });
19798
- fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n");
20059
+ fs$1.appendFileSync(auditPath, JSON.stringify(entry) + "\n", {
20060
+ encoding: "utf-8",
20061
+ mode: 384
20062
+ });
20063
+ fs$1.chmodSync(auditPath, 384);
19799
20064
  } catch (err) {
19800
- logger$a.error("Failed to write audit log:", err);
20065
+ logger$c.error("Failed to write audit log:", err);
19801
20066
  }
19802
20067
  }
19803
20068
  function readAuditLog$1(limit = 100) {
@@ -19807,7 +20072,7 @@ function readAuditLog$1(limit = 100) {
19807
20072
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
19808
20073
  return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
19809
20074
  } catch (err) {
19810
- logger$a.error("Failed to read audit log:", err);
20075
+ logger$c.error("Failed to read audit log:", err);
19811
20076
  return [];
19812
20077
  }
19813
20078
  }
@@ -19975,7 +20240,7 @@ async function requestHumanVaultConsent(request) {
19975
20240
  }
19976
20241
  let httpServer = null;
19977
20242
  let mcpAuthToken = null;
19978
- const logger$9 = createLogger("MCP");
20243
+ const logger$b = createLogger("MCP");
19979
20244
  const MCP_AUTH_FILENAME = "mcp-auth.json";
19980
20245
  function getMcpAuthFilePath() {
19981
20246
  const configDir = process.env.VESSEL_CONFIG_DIR || path$1.join(
@@ -20012,7 +20277,7 @@ function writeMcpAuthFile(endpoint, token) {
20012
20277
  );
20013
20278
  fs$1.chmodSync(filePath2, 384);
20014
20279
  } catch (err) {
20015
- logger$9.warn("Failed to write auth file:", err);
20280
+ logger$b.warn("Failed to write auth file:", err);
20016
20281
  }
20017
20282
  }
20018
20283
  function clearMcpAuthFile() {
@@ -20038,7 +20303,7 @@ function clearMcpAuthFile() {
20038
20303
  );
20039
20304
  fs$1.chmodSync(filePath2, 384);
20040
20305
  } catch (err) {
20041
- logger$9.warn("Failed to clear auth file:", err);
20306
+ logger$b.warn("Failed to clear auth file:", err);
20042
20307
  }
20043
20308
  }
20044
20309
  function regenerateMcpAuthToken() {
@@ -20075,7 +20340,7 @@ function isDangerousMcpAction(name) {
20075
20340
  }
20076
20341
  function requiresExplicitMcpApproval(name, args) {
20077
20342
  if (name === "delete_session" || name === "close_tab" || name === "load_session") return true;
20078
- if (name === "remove_folder" && args.delete_contents === true) return true;
20343
+ if (name === "remove_bookmark_folder" && args.delete_contents === true) return true;
20079
20344
  return false;
20080
20345
  }
20081
20346
  function getActiveTabSummary(tabManager) {
@@ -20140,7 +20405,7 @@ async function getPostActionState(tabManager, name) {
20140
20405
  }
20141
20406
  }
20142
20407
  } catch (err) {
20143
- logger$9.warn("Failed to compute post-action state warning:", err);
20408
+ logger$b.warn("Failed to compute post-action state warning:", err);
20144
20409
  }
20145
20410
  return `${warning}
20146
20411
  [state: url=${wc.getURL()}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]`;
@@ -20243,7 +20508,7 @@ async function waitForConditionMcp(wc, text, selector, timeoutMs) {
20243
20508
  }
20244
20509
  })()
20245
20510
  `).catch((err) => {
20246
- logger$9.warn("Failed to gather wait_for timeout diagnostic:", err);
20511
+ logger$b.warn("Failed to gather wait_for timeout diagnostic:", err);
20247
20512
  return null;
20248
20513
  });
20249
20514
  if (typeof diagnostic === "string" && diagnostic.trim()) {
@@ -20330,7 +20595,7 @@ function registerTools(server, tabManager, runtime2) {
20330
20595
  const page = await extractContent(wc);
20331
20596
  pageType = detectPageType(page);
20332
20597
  } catch (err) {
20333
- logger$9.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
20598
+ logger$b.warn("Failed to detect page type for tool scoring, falling back to GENERAL:", err);
20334
20599
  }
20335
20600
  }
20336
20601
  const scored = TOOL_DEFINITIONS.map((def) => {
@@ -21728,7 +21993,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
21728
21993
  void 0,
21729
21994
  h.color
21730
21995
  ).catch(
21731
- (err) => logger$9.warn("Failed to restore highlight after removal:", err)
21996
+ (err) => logger$b.warn("Failed to restore highlight after removal:", err)
21732
21997
  );
21733
21998
  }
21734
21999
  }
@@ -22576,7 +22841,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
22576
22841
  try {
22577
22842
  page = await extractContent(wc);
22578
22843
  } catch (err) {
22579
- logger$9.warn("Failed to extract page while generating suggestions:", err);
22844
+ logger$b.warn("Failed to extract page while generating suggestions:", err);
22580
22845
  return asTextResponse(
22581
22846
  "Could not read page. Try navigate to a working URL."
22582
22847
  );
@@ -23185,7 +23450,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
23185
23450
  try {
23186
23451
  targetDomain = new URL(tab.state.url).hostname;
23187
23452
  } catch (err) {
23188
- logger$9.warn("Failed to parse active tab URL for vault_status:", err);
23453
+ logger$b.warn("Failed to parse active tab URL for vault_status:", err);
23189
23454
  return asErrorTextResponse("Could not parse active tab URL");
23190
23455
  }
23191
23456
  }
@@ -23251,7 +23516,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23251
23516
  try {
23252
23517
  hostname = new URL(tab.state.url).hostname;
23253
23518
  } catch (err) {
23254
- logger$9.warn("Failed to parse active tab URL for vault_login:", err);
23519
+ logger$b.warn("Failed to parse active tab URL for vault_login:", err);
23255
23520
  return asErrorTextResponse("Could not parse active tab URL");
23256
23521
  }
23257
23522
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -23345,7 +23610,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23345
23610
  try {
23346
23611
  hostname = new URL(tab.state.url).hostname;
23347
23612
  } catch (err) {
23348
- logger$9.warn("Failed to parse active tab URL for vault_totp:", err);
23613
+ logger$b.warn("Failed to parse active tab URL for vault_totp:", err);
23349
23614
  return asErrorTextResponse("Could not parse active tab URL");
23350
23615
  }
23351
23616
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -23685,7 +23950,7 @@ function startMcpServer(tabManager, runtime2, port) {
23685
23950
  await mcpServer.connect(transport);
23686
23951
  await transport.handleRequest(req, res);
23687
23952
  } catch (error) {
23688
- logger$9.error("Error handling request:", error);
23953
+ logger$b.error("Error handling request:", error);
23689
23954
  if (!res.headersSent) {
23690
23955
  res.writeHead(500, { "Content-Type": "application/json" });
23691
23956
  res.end(
@@ -23704,7 +23969,7 @@ function startMcpServer(tabManager, runtime2, port) {
23704
23969
  };
23705
23970
  server.once("error", (error) => {
23706
23971
  const message = error.code === "EADDRINUSE" ? `Port ${port} is already in use. MCP server not started.` : error.message;
23707
- logger$9.error("Server error:", error);
23972
+ logger$b.error("Server error:", error);
23708
23973
  clearMcpAuthFile();
23709
23974
  setMcpHealth({
23710
23975
  configuredPort: port,
@@ -23736,7 +24001,7 @@ function startMcpServer(tabManager, runtime2, port) {
23736
24001
  message: `MCP server listening on ${endpoint}.`
23737
24002
  });
23738
24003
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
23739
- logger$9.info(`Server listening on ${endpoint} (auth enabled)`);
24004
+ logger$b.info(`Server listening on ${endpoint} (auth enabled)`);
23740
24005
  }
23741
24006
  if (mcpAuthToken) {
23742
24007
  writeMcpAuthFile(endpoint, mcpAuthToken);
@@ -23775,7 +24040,7 @@ function stopMcpServer() {
23775
24040
  message: "MCP server is stopped."
23776
24041
  });
23777
24042
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
23778
- logger$9.info("Server stopped");
24043
+ logger$b.info("Server stopped");
23779
24044
  }
23780
24045
  resolve();
23781
24046
  });
@@ -23796,7 +24061,7 @@ const KIT_ID_UNSAFE_CHAR_PATTERN = /[\/\\\0]/;
23796
24061
  function isSafeAutomationKitId(id) {
23797
24062
  return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
23798
24063
  }
23799
- const logger$8 = createLogger("KitRegistry");
24064
+ const logger$a = createLogger("KitRegistry");
23800
24065
  function getUserKitsDir() {
23801
24066
  return path$1.join(electron.app.getPath("userData"), "kits");
23802
24067
  }
@@ -23834,10 +24099,10 @@ function getInstalledKits() {
23834
24099
  if (isValidKit(parsed)) {
23835
24100
  kits.push(parsed);
23836
24101
  } else {
23837
- logger$8.warn(`Skipping invalid kit file: ${file}`);
24102
+ logger$a.warn(`Skipping invalid kit file: ${file}`);
23838
24103
  }
23839
24104
  } catch (err) {
23840
- logger$8.warn(`Failed to read kit file: ${file}`, err);
24105
+ logger$a.warn(`Failed to read kit file: ${file}`, err);
23841
24106
  }
23842
24107
  }
23843
24108
  return kits;
@@ -23909,7 +24174,44 @@ function uninstallKit(id, scheduledKitIds) {
23909
24174
  return errorResult("Failed to remove the kit file.");
23910
24175
  }
23911
24176
  }
23912
- const logger$7 = createLogger("Scheduler");
24177
+ const trustedIpcSenderIds = /* @__PURE__ */ new Set();
24178
+ function registerTrustedIpcSender(wc) {
24179
+ trustedIpcSenderIds.add(wc.id);
24180
+ wc.once("destroyed", () => trustedIpcSenderIds.delete(wc.id));
24181
+ }
24182
+ function assertTrustedIpcSender(event) {
24183
+ if (!trustedIpcSenderIds.has(event.sender.id)) {
24184
+ throw new Error("Blocked IPC from untrusted renderer");
24185
+ }
24186
+ }
24187
+ function isManagedTabIpcSender(event, tabManager) {
24188
+ return Boolean(tabManager.findTabByWebContentsId(event.sender.id));
24189
+ }
24190
+ function assertString(value, name) {
24191
+ if (typeof value !== "string") throw new Error(`${name} must be a string`);
24192
+ }
24193
+ function assertOptionalString(value, name) {
24194
+ if (value !== void 0 && typeof value !== "string") {
24195
+ throw new Error(`${name} must be a string`);
24196
+ }
24197
+ }
24198
+ function assertNumber(value, name) {
24199
+ if (typeof value !== "number" || Number.isNaN(value)) {
24200
+ throw new Error(`${name} must be a number`);
24201
+ }
24202
+ }
24203
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
24204
+ function isValidEmail(value) {
24205
+ return EMAIL_RE.test(value.trim());
24206
+ }
24207
+ function getActiveTabInfo(tabManager) {
24208
+ const tab = tabManager.getActiveTab();
24209
+ if (!tab) return null;
24210
+ const wc = tab.view.webContents;
24211
+ if (wc.isDestroyed()) return null;
24212
+ return { tab, wc };
24213
+ }
24214
+ const logger$9 = createLogger("Scheduler");
23913
24215
  let jobs = [];
23914
24216
  let removeIdleListener = null;
23915
24217
  let broadcastFn = null;
@@ -23932,9 +24234,15 @@ function loadJobs() {
23932
24234
  }
23933
24235
  function saveJobs() {
23934
24236
  try {
23935
- fs$1.writeFileSync(getJobsPath(), JSON.stringify(jobs, null, 2), "utf-8");
24237
+ const jobsPath = getJobsPath();
24238
+ fs$1.mkdirSync(path$1.dirname(jobsPath), { recursive: true });
24239
+ fs$1.writeFileSync(jobsPath, JSON.stringify(jobs, null, 2), {
24240
+ encoding: "utf-8",
24241
+ mode: 384
24242
+ });
24243
+ fs$1.chmodSync(jobsPath, 384);
23936
24244
  } catch (err) {
23937
- logger$7.warn("Failed to save jobs:", err);
24245
+ logger$9.warn("Failed to save jobs:", err);
23938
24246
  }
23939
24247
  }
23940
24248
  function normalizeJob(job, now = /* @__PURE__ */ new Date()) {
@@ -24056,7 +24364,7 @@ async function fireJob(job, windowState, runtime2) {
24056
24364
  };
24057
24365
  startActivity();
24058
24366
  if (!settings2.chatProvider) {
24059
- logger$7.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
24367
+ logger$9.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
24060
24368
  appendActivity(
24061
24369
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
24062
24370
  );
@@ -24064,7 +24372,7 @@ async function fireJob(job, windowState, runtime2) {
24064
24372
  return;
24065
24373
  }
24066
24374
  if (process.env.VESSEL_DEBUG_SCHEDULER === "1" || process.env.VESSEL_DEBUG_SCHEDULER === "true") {
24067
- logger$7.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
24375
+ logger$9.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
24068
24376
  }
24069
24377
  try {
24070
24378
  const provider = createProvider(settings2.chatProvider);
@@ -24117,7 +24425,7 @@ function tick(windowState, runtime2) {
24117
24425
  saveJobs();
24118
24426
  broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
24119
24427
  void fireJob(job, windowState, runtime2).catch((err) => {
24120
- logger$7.warn("Unexpected error firing job:", err);
24428
+ logger$9.warn("Unexpected error firing job:", err);
24121
24429
  }).finally(fireNext);
24122
24430
  };
24123
24431
  fireNext();
@@ -24136,8 +24444,12 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24136
24444
  tick(windowState, runtime2);
24137
24445
  setInterval(() => tick(windowState, runtime2), 6e4);
24138
24446
  }, msToNextMinute);
24139
- electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, () => jobs);
24140
- electron.ipcMain.handle(Channels.SCHEDULE_CREATE, (_, rawJob) => {
24447
+ electron.ipcMain.handle(Channels.SCHEDULE_GET_ALL, (event) => {
24448
+ assertTrustedIpcSender(event);
24449
+ return jobs;
24450
+ });
24451
+ electron.ipcMain.handle(Channels.SCHEDULE_CREATE, (event, rawJob) => {
24452
+ assertTrustedIpcSender(event);
24141
24453
  if (!isValidJobData(rawJob)) {
24142
24454
  throw new Error(
24143
24455
  "Invalid job data. Required: kitId, kitName, kitIcon, renderedPrompt, schedule, enabled."
@@ -24154,7 +24466,8 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24154
24466
  sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
24155
24467
  return newJob;
24156
24468
  });
24157
- electron.ipcMain.handle(Channels.SCHEDULE_UPDATE, (_, id, updates) => {
24469
+ electron.ipcMain.handle(Channels.SCHEDULE_UPDATE, (event, id, updates) => {
24470
+ assertTrustedIpcSender(event);
24158
24471
  if (typeof id !== "string") throw new Error("id must be a string");
24159
24472
  const job = jobs.find((j) => j.id === id);
24160
24473
  if (!job) return null;
@@ -24180,7 +24493,8 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24180
24493
  sendToAll(Channels.SCHEDULE_JOBS_UPDATE, jobs);
24181
24494
  return job;
24182
24495
  });
24183
- electron.ipcMain.handle(Channels.SCHEDULE_DELETE, (_, id) => {
24496
+ electron.ipcMain.handle(Channels.SCHEDULE_DELETE, (event, id) => {
24497
+ assertTrustedIpcSender(event);
24184
24498
  if (typeof id !== "string") throw new Error("id must be a string");
24185
24499
  const before = jobs.length;
24186
24500
  jobs = jobs.filter((j) => j.id !== id);
@@ -24190,40 +24504,6 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24190
24504
  return true;
24191
24505
  });
24192
24506
  }
24193
- const trustedIpcSenderIds = /* @__PURE__ */ new Set();
24194
- function registerTrustedIpcSender(wc) {
24195
- trustedIpcSenderIds.add(wc.id);
24196
- wc.once("destroyed", () => trustedIpcSenderIds.delete(wc.id));
24197
- }
24198
- function assertTrustedIpcSender(event) {
24199
- if (!trustedIpcSenderIds.has(event.sender.id)) {
24200
- throw new Error("Blocked IPC from untrusted renderer");
24201
- }
24202
- }
24203
- function assertString(value, name) {
24204
- if (typeof value !== "string") throw new Error(`${name} must be a string`);
24205
- }
24206
- function assertOptionalString(value, name) {
24207
- if (value !== void 0 && typeof value !== "string") {
24208
- throw new Error(`${name} must be a string`);
24209
- }
24210
- }
24211
- function assertNumber(value, name) {
24212
- if (typeof value !== "number" || Number.isNaN(value)) {
24213
- throw new Error(`${name} must be a number`);
24214
- }
24215
- }
24216
- const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
24217
- function isValidEmail(value) {
24218
- return EMAIL_RE.test(value.trim());
24219
- }
24220
- function getActiveTabInfo(tabManager) {
24221
- const tab = tabManager.getActiveTab();
24222
- if (!tab) return null;
24223
- const wc = tab.view.webContents;
24224
- if (wc.isDestroyed()) return null;
24225
- return { tab, wc };
24226
- }
24227
24507
  const SAVE_DEBOUNCE_MS = 250;
24228
24508
  const PROFILE_FIELDS = [
24229
24509
  "label",
@@ -24563,24 +24843,29 @@ function sanitizeAutofillUpdates(value) {
24563
24843
  return updates;
24564
24844
  }
24565
24845
  function registerAutofillHandlers(windowState) {
24566
- electron.ipcMain.handle(Channels.AUTOFILL_LIST, () => {
24846
+ electron.ipcMain.handle(Channels.AUTOFILL_LIST, (event) => {
24847
+ assertTrustedIpcSender(event);
24567
24848
  return listProfiles();
24568
24849
  });
24569
24850
  electron.ipcMain.handle(
24570
24851
  Channels.AUTOFILL_ADD,
24571
- (_, profile) => {
24852
+ (event, profile) => {
24853
+ assertTrustedIpcSender(event);
24572
24854
  return addProfile(sanitizeAutofillProfile(profile));
24573
24855
  }
24574
24856
  );
24575
- electron.ipcMain.handle(Channels.AUTOFILL_UPDATE, (_, id, updates) => {
24857
+ electron.ipcMain.handle(Channels.AUTOFILL_UPDATE, (event, id, updates) => {
24858
+ assertTrustedIpcSender(event);
24576
24859
  assertString(id, "id");
24577
24860
  return updateProfile(id, sanitizeAutofillUpdates(updates));
24578
24861
  });
24579
- electron.ipcMain.handle(Channels.AUTOFILL_DELETE, (_, id) => {
24862
+ electron.ipcMain.handle(Channels.AUTOFILL_DELETE, (event, id) => {
24863
+ assertTrustedIpcSender(event);
24580
24864
  assertString(id, "id");
24581
24865
  return deleteProfile(id);
24582
24866
  });
24583
- electron.ipcMain.handle(Channels.AUTOFILL_FILL, async (_, profileId) => {
24867
+ electron.ipcMain.handle(Channels.AUTOFILL_FILL, async (event, profileId) => {
24868
+ assertTrustedIpcSender(event);
24584
24869
  assertString(profileId, "profileId");
24585
24870
  const profile = getProfile(profileId);
24586
24871
  if (!profile) throw new Error("Profile not found");
@@ -24650,27 +24935,800 @@ function registerPageDiffHandlers(windowState, sendToRendererViews) {
24650
24935
  electron.ipcMain.on(Channels.PAGE_DIFF_ACTIVITY, (event) => {
24651
24936
  const wc = event.sender;
24652
24937
  if (!wc || wc.isDestroyed()) return;
24938
+ if (!isManagedTabIpcSender(event, windowState.tabManager)) return;
24653
24939
  if (!allowPageEvent(wc.id)) return;
24654
24940
  notePageMutationActivity(wc, sendToRendererViews);
24655
24941
  });
24656
24942
  electron.ipcMain.on(Channels.PAGE_DIFF_DIRTY, (event) => {
24657
24943
  const wc = event.sender;
24658
24944
  if (!wc || wc.isDestroyed()) return;
24945
+ if (!isManagedTabIpcSender(event, windowState.tabManager)) return;
24659
24946
  if (!allowPageEvent(wc.id)) return;
24660
24947
  schedulePageSnapshotCapture(wc, sendToRendererViews);
24661
24948
  });
24662
24949
  }
24663
- function registerVaultHandlers() {
24664
- electron.ipcMain.handle(Channels.VAULT_LIST, (event) => {
24665
- assertTrustedIpcSender(event);
24666
- return listEntries$1();
24950
+ function renderReportAsMarkdown(report, traces) {
24951
+ const sections = [];
24952
+ sections.push(`# ${report.title}`);
24953
+ sections.push("");
24954
+ sections.push(`*Generated: ${report.generatedAt}*`);
24955
+ sections.push("");
24956
+ sections.push("## Executive Summary");
24957
+ sections.push(report.executiveSummary);
24958
+ sections.push("");
24959
+ for (const section of report.findingsByThread) {
24960
+ sections.push(`## ${section.threadLabel}`);
24961
+ sections.push(section.content);
24962
+ sections.push("");
24963
+ }
24964
+ if (report.contradictions.length > 0) {
24965
+ sections.push("## Contradictions & Discrepancies");
24966
+ for (const c of report.contradictions) {
24967
+ sections.push(`- **Claim:** ${c.claim}`);
24968
+ sections.push(` - Source A: [${c.sourceA.url}](${c.sourceA.url}) — "${c.sourceA.claim}"`);
24969
+ sections.push(` - Source B: [${c.sourceB.url}](${c.sourceB.url}) — "${c.sourceB.claim}"`);
24970
+ sections.push(` - **Resolution:** ${c.resolution}`);
24971
+ }
24972
+ sections.push("");
24973
+ }
24974
+ if (report.gaps.length > 0) {
24975
+ sections.push("## Gaps & Unanswered Questions");
24976
+ for (const gap of report.gaps) {
24977
+ sections.push(`- ${gap}`);
24978
+ }
24979
+ sections.push("");
24980
+ }
24981
+ sections.push("## Source Index");
24982
+ for (const source of report.sourceIndex) {
24983
+ sections.push(
24984
+ `${source.index}. [${source.title}](${source.url}) — accessed ${source.accessedAt}`
24985
+ );
24986
+ sections.push(` > "${source.supportingQuote}"`);
24987
+ }
24988
+ sections.push("");
24989
+ if (traces && traces.length > 0) {
24990
+ sections.push("---");
24991
+ sections.push("");
24992
+ sections.push("## Appendix: Agent Traces");
24993
+ for (const trace of traces) {
24994
+ sections.push(`### ${trace.threadLabel}`);
24995
+ sections.push(`Started: ${trace.startedAt} | Finished: ${trace.finishedAt}`);
24996
+ sections.push(`Tool calls: ${trace.toolCalls.length}`);
24997
+ if (trace.errors.length > 0) {
24998
+ sections.push(`Errors: ${trace.errors.length}`);
24999
+ for (const err of trace.errors) {
25000
+ sections.push(`- [${err.timestamp}] ${err.message}`);
25001
+ }
25002
+ }
25003
+ sections.push("");
25004
+ }
25005
+ }
25006
+ return sections.join("\n");
25007
+ }
25008
+ const logger$8 = createLogger("ResearchIPC");
25009
+ function registerResearchHandlers(getOrchestrator) {
25010
+ electron.ipcMain.handle(Channels.RESEARCH_STATE_GET, () => {
25011
+ return getOrchestrator().getState();
24667
25012
  });
24668
25013
  electron.ipcMain.handle(
24669
- Channels.VAULT_ADD,
24670
- (event, entry) => {
24671
- assertTrustedIpcSender(event);
24672
- if (!entry || typeof entry !== "object") {
24673
- throw new Error("Invalid vault entry");
25014
+ Channels.RESEARCH_START_BRIEF,
25015
+ async (_event, query) => {
25016
+ try {
25017
+ const trimmedQuery = query.trim();
25018
+ if (!trimmedQuery) {
25019
+ return { accepted: false, reason: "error" };
25020
+ }
25021
+ if (getOrchestrator().getState().phase !== "idle") {
25022
+ return { accepted: false, reason: "busy" };
25023
+ }
25024
+ await getOrchestrator().startBrief(trimmedQuery);
25025
+ return { accepted: true };
25026
+ } catch (err) {
25027
+ logger$8.error("RESEARCH_START_BRIEF failed", err);
25028
+ return { accepted: false, reason: "error" };
25029
+ }
25030
+ }
25031
+ );
25032
+ electron.ipcMain.handle(Channels.RESEARCH_CONFIRM_BRIEF, () => {
25033
+ try {
25034
+ if (isToolGated("research_confirm_brief")) {
25035
+ return { accepted: false, reason: "premium" };
25036
+ }
25037
+ const orchestrator = getOrchestrator();
25038
+ if (orchestrator.getState().phase !== "briefing") {
25039
+ return { accepted: false, reason: "error" };
25040
+ }
25041
+ orchestrator.confirmBrief();
25042
+ return { accepted: true };
25043
+ } catch (err) {
25044
+ logger$8.error("RESEARCH_CONFIRM_BRIEF failed", err);
25045
+ return { accepted: false, reason: "error" };
25046
+ }
25047
+ });
25048
+ electron.ipcMain.handle(
25049
+ Channels.RESEARCH_APPROVE_OBJECTIVES,
25050
+ (_event, options) => {
25051
+ try {
25052
+ if (isToolGated("research_approve_objectives")) {
25053
+ return { accepted: false, reason: "premium" };
25054
+ }
25055
+ const orchestrator = getOrchestrator();
25056
+ const state2 = orchestrator.getState();
25057
+ if (state2.phase !== "awaiting_approval" || !state2.objectives) {
25058
+ return { accepted: false, reason: "error" };
25059
+ }
25060
+ orchestrator.approveObjectives(
25061
+ options.supervisionMode,
25062
+ options.includeTraces
25063
+ );
25064
+ orchestrator.executeSubAgents().catch((err) => {
25065
+ logger$8.error("Background sub-agent execution failed", err);
25066
+ });
25067
+ return { accepted: true };
25068
+ } catch (err) {
25069
+ logger$8.error("RESEARCH_APPROVE_OBJECTIVES failed", err);
25070
+ return { accepted: false, reason: "error" };
25071
+ }
25072
+ }
25073
+ );
25074
+ electron.ipcMain.handle(
25075
+ Channels.RESEARCH_SET_MODE,
25076
+ (_event, mode) => {
25077
+ getOrchestrator().setSupervisionMode(mode);
25078
+ }
25079
+ );
25080
+ electron.ipcMain.handle(
25081
+ Channels.RESEARCH_SET_TRACES,
25082
+ (_event, include) => {
25083
+ getOrchestrator().setIncludeTraces(include);
25084
+ }
25085
+ );
25086
+ electron.ipcMain.handle(Channels.RESEARCH_CANCEL, () => {
25087
+ getOrchestrator().cancel();
25088
+ });
25089
+ electron.ipcMain.handle(Channels.RESEARCH_EXPORT_REPORT, async () => {
25090
+ try {
25091
+ if (isToolGated("research_export_report")) {
25092
+ return { accepted: false, reason: "premium" };
25093
+ }
25094
+ const state2 = getOrchestrator().getState();
25095
+ if (!state2.report) {
25096
+ return { accepted: false, reason: "error", error: "No report to export" };
25097
+ }
25098
+ const markdown = renderReportAsMarkdown(
25099
+ state2.report,
25100
+ state2.includeTraces ? state2.subAgentTraces : void 0
25101
+ );
25102
+ const { filePath: filePath2, canceled } = await electron.dialog.showSaveDialog({
25103
+ title: "Export Research Report",
25104
+ defaultPath: `${state2.report.title.replace(/[^a-zA-Z0-9 _-]/g, "")}.md`,
25105
+ filters: [
25106
+ { name: "Markdown", extensions: ["md"] },
25107
+ { name: "All Files", extensions: ["*"] }
25108
+ ]
25109
+ });
25110
+ if (canceled || !filePath2) {
25111
+ return { accepted: false, reason: "cancelled" };
25112
+ }
25113
+ await promises.writeFile(filePath2, markdown, "utf-8");
25114
+ return { accepted: true, savedPath: filePath2 };
25115
+ } catch (err) {
25116
+ logger$8.error("RESEARCH_EXPORT_REPORT failed", err);
25117
+ return { accepted: false, reason: "error" };
25118
+ }
25119
+ });
25120
+ }
25121
+ function buildSubAgentSystemPrompt(thread) {
25122
+ const domainBlock = thread.blockedDomains.length > 0 ? `
25123
+ BLOCKED DOMAINS (never visit): ${thread.blockedDomains.join(", ")}` : "";
25124
+ const domainPref = thread.preferredDomains.length > 0 ? `
25125
+ PREFERRED DOMAINS: ${thread.preferredDomains.join(", ")}` : "";
25126
+ return `You are a Vessel research sub-agent assigned to a specific thread.
25127
+
25128
+ YOUR MISSION: ${thread.question}
25129
+
25130
+ SEARCH QUERIES TO START WITH:
25131
+ ${thread.searchQueries.map((q) => `- ${q}`).join("\n")}${domainPref}${domainBlock}
25132
+
25133
+ SOURCE BUDGET: You may visit up to ${thread.sourceBudget} sources. Do not exceed this unless the captain explicitly increases it.
25134
+
25135
+ RULES:
25136
+ 1. Every finding you report MUST include the source URL and the verbatim extracted quote that supports it.
25137
+ 2. Never fabricate. If you cannot find an answer, say so.
25138
+ 3. Stay on your thread. Do not wander into other research angles.
25139
+ 4. Report findings incrementally. After visiting each source, report what you found.
25140
+ 5. If a page is behind a paywall or requires login, note it and move on.
25141
+ 6. Prefer primary sources over secondary commentary.
25142
+ 7. Do not use emojis.
25143
+
25144
+ When done, report a summary of your execution: pages visited, useful sources found, discarded sources, any errors.`;
25145
+ }
25146
+ function buildSynthesisPrompt(objectives, findings) {
25147
+ const findingsBlock = findings.map(
25148
+ (f) => `
25149
+ ### Thread: ${f.threadLabel}
25150
+ Question: ${f.threadQuestion}
25151
+ Execution: ${f.executionSummary}
25152
+
25153
+ Claims:
25154
+ ${f.claims.map(
25155
+ (c, i) => `${i + 1}. ${c.claim}
25156
+ Source: ${c.sourceUrl}
25157
+ Title: ${c.sourceTitle}
25158
+ Accessed: ${c.extractedAt}
25159
+ Quote: "${c.extractedQuote}"
25160
+ Relevance: ${c.relevanceNote}`
25161
+ ).join("\n")}
25162
+
25163
+ ${f.discardedSources.length > 0 ? `Discarded sources:
25164
+ ${f.discardedSources.map((d) => `- ${d.url}: ${d.reason}`).join("\n")}` : ""}`
25165
+ ).join("\n\n---\n");
25166
+ return `Synthesize the following research findings into a structured JSON Research Report.
25167
+
25168
+ RESEARCH QUESTION: ${objectives.researchQuestion}
25169
+ AUDIENCE: ${objectives.audience}
25170
+ EXPECTED OUTLINE:
25171
+ ${objectives.reportOutline.map((s) => `- ${s}`).join("\n")}
25172
+
25173
+ FINDINGS:
25174
+ ${findingsBlock}
25175
+
25176
+ Return ONLY valid JSON — no markdown, no code fences, no commentary. The JSON object must have these exact fields:
25177
+
25178
+ {
25179
+ "title": "Report title (string)",
25180
+ "executiveSummary": "2-3 paragraph answer to the research question",
25181
+ "findingsByThread": [
25182
+ { "threadLabel": "Label", "content": "Section content with claims and numbered citations [1], [2], etc." }
25183
+ ],
25184
+ "contradictions": [
25185
+ {
25186
+ "claim": "The disputed claim",
25187
+ "sourceA": { "url": "https://...", "claim": "What source A says" },
25188
+ "sourceB": { "url": "https://...", "claim": "What source B says" },
25189
+ "resolution": "How to resolve the contradiction (or why it cannot be resolved)"
25190
+ }
25191
+ ],
25192
+ "gaps": ["Gap or unanswered question 1", "Gap 2"],
25193
+ "sourceIndex": [
25194
+ {
25195
+ "index": 1,
25196
+ "url": "https://...",
25197
+ "title": "Page title",
25198
+ "accessedAt": "ISO timestamp from claim metadata",
25199
+ "supportingQuote": "Verbatim quote from the claim"
25200
+ }
25201
+ ]
25202
+ }
25203
+
25204
+ RULES:
25205
+ 1. Every factual claim MUST cite its source using the numbered index format [1], [2], etc.
25206
+ 2. The sourceIndex numbers must correspond to the [n] citations in the text.
25207
+ 3. Do not invent anything. Only use claims from the findings above.
25208
+ 4. Omit empty arrays entirely (contradictions, gaps) — do not include "contradictions": [] if there are none.
25209
+ 5. Do not use emojis.`;
25210
+ }
25211
+ const logger$7 = createLogger("ResearchOrchestrator");
25212
+ const MAX_THREADS = 5;
25213
+ function clone$1(value) {
25214
+ return structuredClone(value);
25215
+ }
25216
+ class ResearchOrchestrator {
25217
+ constructor(provider, tabManager, runtime2) {
25218
+ this.provider = provider;
25219
+ this.tabManager = tabManager;
25220
+ this.runtime = runtime2;
25221
+ this.state = this.initialState();
25222
+ }
25223
+ provider;
25224
+ tabManager;
25225
+ runtime;
25226
+ state;
25227
+ updateListener = null;
25228
+ // ── state access ──────────────────────────────────────────────
25229
+ initialState() {
25230
+ return {
25231
+ phase: "idle",
25232
+ supervisionMode: "interactive",
25233
+ includeTraces: false,
25234
+ objectives: null,
25235
+ threads: [],
25236
+ threadFindings: [],
25237
+ report: null,
25238
+ subAgentTraces: [],
25239
+ error: null,
25240
+ startedAt: null,
25241
+ originalQuery: null
25242
+ };
25243
+ }
25244
+ getState() {
25245
+ return clone$1(this.state);
25246
+ }
25247
+ setUpdateListener(listener) {
25248
+ this.updateListener = listener;
25249
+ if (listener) listener(this.getState());
25250
+ }
25251
+ emit() {
25252
+ if (this.updateListener) this.updateListener(this.getState());
25253
+ }
25254
+ setPhase(phase) {
25255
+ this.state.phase = phase;
25256
+ this.emit();
25257
+ }
25258
+ // ── supervision / config ──────────────────────────────────────
25259
+ setSupervisionMode(mode) {
25260
+ this.state.supervisionMode = mode;
25261
+ this.emit();
25262
+ }
25263
+ setIncludeTraces(include) {
25264
+ this.state.includeTraces = include;
25265
+ this.emit();
25266
+ }
25267
+ cancel() {
25268
+ this.state = this.initialState();
25269
+ this.emit();
25270
+ }
25271
+ /**
25272
+ * Swap the AI provider used by this orchestrator.
25273
+ * Safe to call while research is in progress — running sub-agents
25274
+ * pick up the new provider on their next LLM call.
25275
+ */
25276
+ setProvider(provider) {
25277
+ this.provider = provider;
25278
+ }
25279
+ getProvider() {
25280
+ if (!this.provider) {
25281
+ throw new Error("Chat provider not configured - required for Research Desk");
25282
+ }
25283
+ return this.provider;
25284
+ }
25285
+ // ── phase: idle → briefing ────────────────────────────────────
25286
+ async startBrief(userQuery) {
25287
+ const query = userQuery.trim();
25288
+ if (!query) {
25289
+ logger$7.warn("Ignoring empty Research Desk query");
25290
+ return;
25291
+ }
25292
+ if (this.state.phase !== "idle") {
25293
+ logger$7.warn("Research already in progress, ignoring startBrief");
25294
+ return;
25295
+ }
25296
+ this.state = this.initialState();
25297
+ this.state.originalQuery = query;
25298
+ this.state.startedAt = (/* @__PURE__ */ new Date()).toISOString();
25299
+ this.setPhase("briefing");
25300
+ logger$7.info(`Brief started for query: ${query.slice(0, 120)}`);
25301
+ }
25302
+ // ── phase: briefing → planning ─────────────────────────────────
25303
+ confirmBrief() {
25304
+ if (this.state.phase !== "briefing") {
25305
+ logger$7.warn("Not in briefing phase, ignoring confirmBrief");
25306
+ return;
25307
+ }
25308
+ this.setPhase("planning");
25309
+ }
25310
+ // ── phase: planning → awaiting_approval ────────────────────────
25311
+ setObjectives(objectives) {
25312
+ if (this.state.phase !== "planning") {
25313
+ logger$7.warn("Not in planning phase, ignoring setObjectives");
25314
+ return;
25315
+ }
25316
+ this.state.objectives = objectives;
25317
+ this.state.threads = objectives.threads.slice(0, MAX_THREADS);
25318
+ this.setPhase("awaiting_approval");
25319
+ }
25320
+ /**
25321
+ * Parse a planning-phase LLM response into ResearchObjectives.
25322
+ * Expects JSON (optionally wrapped in ```json fences).
25323
+ * Returns true if parsing succeeded and objectives were set.
25324
+ */
25325
+ parseAndSetObjectives(text) {
25326
+ if (this.state.phase !== "planning") return false;
25327
+ let json = text;
25328
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
25329
+ if (fenceMatch) {
25330
+ json = fenceMatch[1].trim();
25331
+ } else {
25332
+ const objMatch = text.match(/\{[\s\S]*\}/);
25333
+ if (objMatch) json = objMatch[0];
25334
+ }
25335
+ try {
25336
+ const parsed = JSON.parse(json);
25337
+ if (typeof parsed.researchQuestion !== "string" || !parsed.researchQuestion.trim()) {
25338
+ logger$7.warn("Missing researchQuestion in objectives JSON");
25339
+ return false;
25340
+ }
25341
+ if (!Array.isArray(parsed.threads) || parsed.threads.length === 0) {
25342
+ logger$7.warn("Missing or empty threads array in objectives JSON");
25343
+ return false;
25344
+ }
25345
+ const threads = parsed.threads.map((t, i) => {
25346
+ const obj = t;
25347
+ const question = String(obj.question || "").trim();
25348
+ const searchQueries = Array.isArray(obj.searchQueries) ? obj.searchQueries.map((q) => String(q).trim()).filter(Boolean) : [];
25349
+ const sourceBudget = typeof obj.sourceBudget === "number" && Number.isFinite(obj.sourceBudget) ? Math.max(1, Math.floor(obj.sourceBudget)) : 5;
25350
+ return {
25351
+ label: String(obj.label || `Thread ${i + 1}`),
25352
+ question,
25353
+ searchQueries,
25354
+ preferredDomains: Array.isArray(obj.preferredDomains) ? obj.preferredDomains.map((d) => String(d).trim()).filter(Boolean) : [],
25355
+ blockedDomains: Array.isArray(obj.blockedDomains) ? obj.blockedDomains.map((d) => String(d).trim()).filter(Boolean) : [],
25356
+ sourceBudget
25357
+ };
25358
+ }).filter((thread) => thread.question && thread.searchQueries.length > 0).slice(0, MAX_THREADS);
25359
+ if (threads.length === 0) {
25360
+ logger$7.warn("Objectives JSON did not contain any valid research threads");
25361
+ return false;
25362
+ }
25363
+ const objectives = {
25364
+ researchQuestion: String(parsed.researchQuestion).trim(),
25365
+ threads,
25366
+ audience: String(parsed.audience || "general").trim(),
25367
+ reportOutline: Array.isArray(parsed.reportOutline) ? parsed.reportOutline.map((s) => String(s).trim()).filter(Boolean) : [],
25368
+ totalSourceBudget: threads.reduce((sum, t) => sum + t.sourceBudget, 0)
25369
+ };
25370
+ this.setObjectives(objectives);
25371
+ logger$7.info(`Parsed ${objectives.threads.length} threads from objectives`);
25372
+ return true;
25373
+ } catch (err) {
25374
+ logger$7.warn("Failed to parse objectives JSON", err);
25375
+ return false;
25376
+ }
25377
+ }
25378
+ // ── phase: awaiting_approval → executing ───────────────────────
25379
+ approveObjectives(mode, includeTraces) {
25380
+ if (this.state.phase !== "awaiting_approval") {
25381
+ logger$7.warn("Not awaiting approval, ignoring approveObjectives");
25382
+ return;
25383
+ }
25384
+ if (mode) this.state.supervisionMode = mode;
25385
+ if (includeTraces !== void 0) this.state.includeTraces = includeTraces;
25386
+ this.setPhase("executing");
25387
+ }
25388
+ // ── phase: executing → synthesizing ────────────────────────────
25389
+ async executeSubAgents() {
25390
+ if (this.state.phase !== "executing" || !this.state.objectives) return;
25391
+ const tabMutex = new TabMutex();
25392
+ const results = await Promise.all(
25393
+ this.state.threads.map((thread) => {
25394
+ if (this.state.phase !== "executing") return null;
25395
+ return this.runSubAgent(thread, tabMutex).catch((err) => {
25396
+ logger$7.error(`Sub-agent "${thread.label}" failed`, err);
25397
+ return {
25398
+ threadLabel: thread.label,
25399
+ threadQuestion: thread.question,
25400
+ claims: [],
25401
+ discardedSources: [],
25402
+ executionSummary: `Failed: ${String(err)}`
25403
+ };
25404
+ });
25405
+ })
25406
+ );
25407
+ if (this.state.phase !== "executing") return;
25408
+ this.state.threadFindings = results.filter((f) => f !== null);
25409
+ this.setPhase("synthesizing");
25410
+ try {
25411
+ await this.synthesizeReport();
25412
+ } catch (err) {
25413
+ logger$7.error("Auto-synthesis failed", err);
25414
+ this.state.error = `Synthesis failed: ${String(err)}`;
25415
+ this.setPhase("delivered");
25416
+ }
25417
+ }
25418
+ // ── sub-agent loop ─────────────────────────────────────────────
25419
+ async runSubAgent(thread, tabMutex) {
25420
+ const trace = {
25421
+ threadLabel: thread.label,
25422
+ toolCalls: [],
25423
+ errors: [],
25424
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
25425
+ finishedAt: ""
25426
+ };
25427
+ const tabId = this.tabManager.createTab();
25428
+ let sourcesConsumed = 0;
25429
+ if (tabId) this.tabManager.switchTab(tabId);
25430
+ const discardedSources = [];
25431
+ let transcript = "";
25432
+ try {
25433
+ const provider = this.getProvider();
25434
+ if (!provider.streamAgentQuery) {
25435
+ throw new Error("Provider does not support agent tool loops");
25436
+ }
25437
+ const systemPrompt = buildSubAgentSystemPrompt(thread);
25438
+ const userMessage = `Begin researching: ${thread.question}
25439
+
25440
+ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25441
+ const actionCtx = {
25442
+ tabManager: this.tabManager,
25443
+ runtime: this.runtime,
25444
+ toolProfile: provider.agentToolProfile,
25445
+ tabId: tabId ?? void 0,
25446
+ _tabMutex: tabMutex
25447
+ };
25448
+ await provider.streamAgentQuery(
25449
+ systemPrompt,
25450
+ userMessage,
25451
+ AGENT_TOOLS,
25452
+ (chunk) => {
25453
+ transcript += chunk;
25454
+ },
25455
+ async (name, args) => {
25456
+ const t0 = Date.now();
25457
+ if (this.state.phase !== "executing") {
25458
+ const msg = "Research cancelled — stopping.";
25459
+ return msg;
25460
+ }
25461
+ if (name === "navigate" || name === "search") {
25462
+ sourcesConsumed++;
25463
+ if (sourcesConsumed > thread.sourceBudget) {
25464
+ const msg = `Source budget (${thread.sourceBudget}) exceeded. Summarize findings and stop.`;
25465
+ trace.toolCalls.push({
25466
+ tool: name,
25467
+ args,
25468
+ result: msg,
25469
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25470
+ durationMs: 0
25471
+ });
25472
+ return msg;
25473
+ }
25474
+ }
25475
+ try {
25476
+ const output = await executeAction(name, args, actionCtx);
25477
+ trace.toolCalls.push({
25478
+ tool: name,
25479
+ args,
25480
+ result: output,
25481
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25482
+ durationMs: Date.now() - t0
25483
+ });
25484
+ return output;
25485
+ } catch (err) {
25486
+ const msg = err instanceof Error ? err.message : String(err);
25487
+ if (msg.includes("paywall") || msg.includes("login required") || msg.includes("403")) {
25488
+ discardedSources.push({
25489
+ url: String(args.url || ""),
25490
+ title: String(args.url || "unknown"),
25491
+ reason: msg
25492
+ });
25493
+ }
25494
+ trace.errors.push({
25495
+ message: msg,
25496
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
25497
+ });
25498
+ trace.toolCalls.push({
25499
+ tool: name,
25500
+ args,
25501
+ result: `Error: ${msg}`,
25502
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25503
+ durationMs: Date.now() - t0
25504
+ });
25505
+ return `Error: ${msg}`;
25506
+ }
25507
+ },
25508
+ () => {
25509
+ }
25510
+ );
25511
+ } catch (err) {
25512
+ trace.errors.push({
25513
+ message: String(err),
25514
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
25515
+ });
25516
+ } finally {
25517
+ trace.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
25518
+ if (tabId) {
25519
+ try {
25520
+ this.tabManager.closeTab(tabId);
25521
+ } catch (err) {
25522
+ logger$7.warn(`Failed to close sub-agent tab ${tabId}`, err);
25523
+ }
25524
+ }
25525
+ }
25526
+ let claims = [];
25527
+ if (this.state.phase === "executing") {
25528
+ try {
25529
+ claims = await this.extractClaimsFromTranscript(thread, transcript);
25530
+ } catch (err) {
25531
+ logger$7.warn(`Claim extraction failed for "${thread.label}"`, err);
25532
+ }
25533
+ }
25534
+ if (this.state.phase === "executing" && this.state.includeTraces) {
25535
+ this.state.subAgentTraces.push(trace);
25536
+ }
25537
+ const pagesVisited = trace.toolCalls.filter(
25538
+ (t) => ["navigate", "read_page", "search"].includes(t.tool)
25539
+ ).length;
25540
+ return {
25541
+ threadLabel: thread.label,
25542
+ threadQuestion: thread.question,
25543
+ claims,
25544
+ discardedSources,
25545
+ executionSummary: `Visited ${pagesVisited} pages (${trace.toolCalls.length} tool calls, ${sourcesConsumed} sources). ${claims.length} claims extracted. ${discardedSources.length} sources discarded.${trace.errors.length > 0 ? ` ${trace.errors.length} errors.` : ""}`
25546
+ };
25547
+ }
25548
+ /**
25549
+ * Extract structured claims from the sub-agent's research transcript.
25550
+ * Makes a follow-up LLM call asking it to parse claims with source URLs and quotes.
25551
+ */
25552
+ async extractClaimsFromTranscript(thread, transcript) {
25553
+ if (!transcript.trim()) return [];
25554
+ const prompt = `You are a claim extractor. Given a research transcript, extract every factual claim along with its source URL and the exact supporting quote from the page.
25555
+
25556
+ CRITICAL RULES:
25557
+ - Only extract claims that are explicitly supported by a source URL AND a verbatim quote in the transcript.
25558
+ - If a claim has no source URL or no extracted quote, do NOT include it.
25559
+ - Do not fabricate claims. Only use what is explicitly stated in the transcript.
25560
+ - Return ONLY valid JSON — a JSON array of claim objects.
25561
+
25562
+ Each claim object must have these fields:
25563
+ - claim: the factual claim text
25564
+ - sourceUrl: the URL of the source page
25565
+ - sourceTitle: the title of the source page (or "Unknown" if not mentioned)
25566
+ - extractedQuote: the verbatim quote from the page that supports this claim
25567
+ - relevanceNote: a one-sentence note on why this claim matters to the research question
25568
+
25569
+ Return format:
25570
+ \`\`\`json
25571
+ [{"claim": "...", "sourceUrl": "...", "sourceTitle": "...", "extractedQuote": "...", "relevanceNote": "..."}]
25572
+ \`\`\`
25573
+
25574
+ RESEARCH QUESTION: ${thread.question}
25575
+ THREAD LABEL: ${thread.label}
25576
+
25577
+ TRANSCRIPT:
25578
+ ${transcript.slice(0, 32e3)}`;
25579
+ let response = "";
25580
+ await this.getProvider().streamQuery(
25581
+ prompt,
25582
+ "Extract the claims.",
25583
+ (chunk) => {
25584
+ response += chunk;
25585
+ },
25586
+ () => {
25587
+ }
25588
+ );
25589
+ let json = response;
25590
+ const fenceMatch = response.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
25591
+ if (fenceMatch) json = fenceMatch[1].trim();
25592
+ else {
25593
+ const arrMatch = response.match(/\[[\s\S]*\]/);
25594
+ if (arrMatch) json = arrMatch[0];
25595
+ }
25596
+ try {
25597
+ const raw = JSON.parse(json);
25598
+ if (!Array.isArray(raw)) return [];
25599
+ return raw.map((item) => {
25600
+ const c = item;
25601
+ return {
25602
+ claim: String(c.claim || "").trim(),
25603
+ sourceUrl: String(c.sourceUrl || "").trim(),
25604
+ sourceTitle: String(c.sourceTitle || c.sourceUrl || "Unknown").trim(),
25605
+ extractedQuote: String(c.extractedQuote || "").trim(),
25606
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
25607
+ threadLabel: thread.label,
25608
+ relevanceNote: String(c.relevanceNote || "").trim()
25609
+ };
25610
+ }).filter(
25611
+ (claim) => claim.claim && claim.sourceUrl && claim.extractedQuote
25612
+ );
25613
+ } catch {
25614
+ logger$7.warn(`Failed to parse claims JSON for "${thread.label}"`);
25615
+ return [];
25616
+ }
25617
+ }
25618
+ // ── phase: synthesizing → delivered ───────────────────────────
25619
+ async synthesizeReport() {
25620
+ if (this.state.phase !== "synthesizing" || !this.state.objectives) {
25621
+ return null;
25622
+ }
25623
+ const objectives = this.state.objectives;
25624
+ const findings = this.state.threadFindings;
25625
+ const synthesisPrompt = buildSynthesisPrompt(objectives, findings);
25626
+ let response = "";
25627
+ await this.getProvider().streamQuery(
25628
+ synthesisPrompt,
25629
+ "Return ONLY the JSON object now.",
25630
+ (chunk) => {
25631
+ response += chunk;
25632
+ },
25633
+ () => {
25634
+ }
25635
+ );
25636
+ const report = this.parseReportFromJson(response, objectives);
25637
+ this.setReport(report);
25638
+ this.setPhase("delivered");
25639
+ return report;
25640
+ }
25641
+ /**
25642
+ * Parse the LLM's JSON synthesis response into a structured ResearchReport.
25643
+ * Handles both bare JSON and JSON wrapped in markdown fences.
25644
+ */
25645
+ parseReportFromJson(text, objectives) {
25646
+ let json = text.trim();
25647
+ const fenceMatch = json.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
25648
+ if (fenceMatch) json = fenceMatch[1].trim();
25649
+ const objMatch = json.match(/\{[\s\S]*\}/);
25650
+ if (objMatch) json = objMatch[0];
25651
+ try {
25652
+ const parsed = JSON.parse(json);
25653
+ return {
25654
+ title: String(parsed.title || objectives.researchQuestion).trim(),
25655
+ executiveSummary: String(parsed.executiveSummary || "").trim(),
25656
+ findingsByThread: Array.isArray(parsed.findingsByThread) ? parsed.findingsByThread.map((s) => {
25657
+ const obj = s;
25658
+ return {
25659
+ threadLabel: String(obj.threadLabel || "").trim(),
25660
+ content: String(obj.content || "").trim()
25661
+ };
25662
+ }) : [],
25663
+ contradictions: Array.isArray(parsed.contradictions) ? parsed.contradictions.map((c) => {
25664
+ const obj = c;
25665
+ const sourceA = obj.sourceA ?? {};
25666
+ const sourceB = obj.sourceB ?? {};
25667
+ return {
25668
+ claim: String(obj.claim || "").trim(),
25669
+ sourceA: {
25670
+ url: String(sourceA.url || "").trim(),
25671
+ claim: String(sourceA.claim || "").trim()
25672
+ },
25673
+ sourceB: {
25674
+ url: String(sourceB.url || "").trim(),
25675
+ claim: String(sourceB.claim || "").trim()
25676
+ },
25677
+ resolution: String(obj.resolution || "").trim()
25678
+ };
25679
+ }).filter(
25680
+ (c) => c.claim && c.sourceA.url && c.sourceB.url && c.resolution
25681
+ ) : [],
25682
+ gaps: Array.isArray(parsed.gaps) ? parsed.gaps.map((g) => String(g).trim()).filter(Boolean) : [],
25683
+ sourceIndex: Array.isArray(parsed.sourceIndex) ? parsed.sourceIndex.map((s) => {
25684
+ const obj = s;
25685
+ return {
25686
+ index: typeof obj.index === "number" ? obj.index : parseInt(String(obj.index), 10) || 0,
25687
+ url: String(obj.url || "").trim(),
25688
+ title: String(obj.title || "").trim(),
25689
+ accessedAt: String(obj.accessedAt || "").trim(),
25690
+ supportingQuote: String(obj.supportingQuote || "").trim()
25691
+ };
25692
+ }).filter((s) => s.url && s.title) : [],
25693
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25694
+ objectives
25695
+ };
25696
+ } catch (err) {
25697
+ logger$7.warn("Failed to parse synthesis JSON, using minimal report", err);
25698
+ return {
25699
+ title: objectives.researchQuestion,
25700
+ executiveSummary: `Report generation failed: ${String(err)}`,
25701
+ findingsByThread: [],
25702
+ contradictions: [],
25703
+ gaps: ["Report generation failed — JSON parsing error"],
25704
+ sourceIndex: [],
25705
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25706
+ objectives
25707
+ };
25708
+ }
25709
+ }
25710
+ // ── report management ──────────────────────────────────────────
25711
+ setReport(report) {
25712
+ this.state.report = report;
25713
+ this.emit();
25714
+ }
25715
+ // ── reset ──────────────────────────────────────────────────────
25716
+ reset() {
25717
+ this.state = this.initialState();
25718
+ this.emit();
25719
+ }
25720
+ }
25721
+ function registerVaultHandlers() {
25722
+ electron.ipcMain.handle(Channels.VAULT_LIST, (event) => {
25723
+ assertTrustedIpcSender(event);
25724
+ return listEntries$1();
25725
+ });
25726
+ electron.ipcMain.handle(
25727
+ Channels.VAULT_ADD,
25728
+ (event, entry) => {
25729
+ assertTrustedIpcSender(event);
25730
+ if (!entry || typeof entry !== "object") {
25731
+ throw new Error("Invalid vault entry");
24674
25732
  }
24675
25733
  assertString(entry.label, "label");
24676
25734
  assertString(entry.domainPattern, "domainPattern");
@@ -25802,19 +26860,22 @@ function getSafeBookmarkExportName(name) {
25802
26860
  return safeName || "folder";
25803
26861
  }
25804
26862
  function registerBookmarkHandlers() {
25805
- electron.ipcMain.handle(Channels.BOOKMARKS_GET, () => {
26863
+ electron.ipcMain.handle(Channels.BOOKMARKS_GET, (event) => {
26864
+ assertTrustedIpcSender(event);
25806
26865
  return getState();
25807
26866
  });
25808
26867
  electron.ipcMain.handle(
25809
26868
  Channels.FOLDER_CREATE,
25810
- (_, name, summary) => {
26869
+ (event, name, summary) => {
26870
+ assertTrustedIpcSender(event);
25811
26871
  trackBookmarkAction("folder_create");
25812
26872
  return createFolderWithSummary(name, summary);
25813
26873
  }
25814
26874
  );
25815
26875
  electron.ipcMain.handle(
25816
26876
  Channels.BOOKMARK_SAVE,
25817
- (_, url, title, folderId, note, intent, expectedContent, keyFields, agentHints) => {
26877
+ (event, url, title, folderId, note, intent, expectedContent, keyFields, agentHints) => {
26878
+ assertTrustedIpcSender(event);
25818
26879
  trackBookmarkAction("save");
25819
26880
  const result = saveBookmarkWithPolicy(url, title, folderId, note, {
25820
26881
  onDuplicate: "update",
@@ -25835,12 +26896,14 @@ function registerBookmarkHandlers() {
25835
26896
  );
25836
26897
  electron.ipcMain.handle(
25837
26898
  Channels.BOOKMARK_UPDATE,
25838
- (_, id, updates) => {
26899
+ (event, id, updates) => {
26900
+ assertTrustedIpcSender(event);
25839
26901
  trackBookmarkAction("save");
25840
26902
  return updateBookmark(id, updates);
25841
26903
  }
25842
26904
  );
25843
- electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (_, id) => {
26905
+ electron.ipcMain.handle(Channels.BOOKMARK_REMOVE, (event, id) => {
26906
+ assertTrustedIpcSender(event);
25844
26907
  trackBookmarkAction("remove");
25845
26908
  return removeBookmark(id);
25846
26909
  });
@@ -25933,22 +26996,26 @@ function registerBookmarkHandlers() {
25933
26996
  trackBookmarkAction("import");
25934
26997
  return importBookmarksFromJson(content);
25935
26998
  });
25936
- electron.ipcMain.handle(Channels.FOLDER_REMOVE, (_, id, deleteContents) => {
26999
+ electron.ipcMain.handle(Channels.FOLDER_REMOVE, (event, id, deleteContents) => {
27000
+ assertTrustedIpcSender(event);
25937
27001
  trackBookmarkAction("folder_remove");
25938
27002
  return removeFolder(id, deleteContents ?? false);
25939
27003
  });
25940
27004
  electron.ipcMain.handle(
25941
27005
  Channels.FOLDER_RENAME,
25942
- (_, id, newName, summary) => {
27006
+ (event, id, newName, summary) => {
27007
+ assertTrustedIpcSender(event);
25943
27008
  return renameFolder(id, newName, summary);
25944
27009
  }
25945
27010
  );
25946
27011
  }
25947
27012
  function registerHistoryHandlers() {
25948
- electron.ipcMain.handle(Channels.HISTORY_GET, () => {
27013
+ electron.ipcMain.handle(Channels.HISTORY_GET, (event) => {
27014
+ assertTrustedIpcSender(event);
25949
27015
  return getState$1();
25950
27016
  });
25951
- electron.ipcMain.handle(Channels.HISTORY_SEARCH, (_, query) => {
27017
+ electron.ipcMain.handle(Channels.HISTORY_SEARCH, (event, query) => {
27018
+ assertTrustedIpcSender(event);
25952
27019
  return search(query);
25953
27020
  });
25954
27021
  electron.ipcMain.handle(Channels.HISTORY_CLEAR, (event) => {
@@ -26073,10 +27140,12 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
26073
27140
  void handleUrl(currentUrl);
26074
27141
  }
26075
27142
  };
26076
- electron.ipcMain.handle(Channels.PREMIUM_GET_STATE, () => {
27143
+ electron.ipcMain.handle(Channels.PREMIUM_GET_STATE, (event) => {
27144
+ assertTrustedIpcSender(event);
26077
27145
  return getPremiumState();
26078
27146
  });
26079
- electron.ipcMain.handle(Channels.PREMIUM_ACTIVATION_START, async (_, email) => {
27147
+ electron.ipcMain.handle(Channels.PREMIUM_ACTIVATION_START, async (event, email) => {
27148
+ assertTrustedIpcSender(event);
26080
27149
  assertString(email, "email");
26081
27150
  if (!isValidEmail(email)) {
26082
27151
  return errorResult("Invalid email format");
@@ -26090,7 +27159,8 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
26090
27159
  });
26091
27160
  electron.ipcMain.handle(
26092
27161
  Channels.PREMIUM_ACTIVATION_VERIFY,
26093
- async (_, email, code, challengeToken) => {
27162
+ async (event, email, code, challengeToken) => {
27163
+ assertTrustedIpcSender(event);
26094
27164
  assertString(email, "email");
26095
27165
  assertString(code, "code");
26096
27166
  assertString(challengeToken, "challengeToken");
@@ -26112,7 +27182,8 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
26112
27182
  return result;
26113
27183
  }
26114
27184
  );
26115
- electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (_, email) => {
27185
+ electron.ipcMain.handle(Channels.PREMIUM_CHECKOUT, async (event, email) => {
27186
+ assertTrustedIpcSender(event);
26116
27187
  trackPremiumFunnel("checkout_clicked");
26117
27188
  const result = await getCheckoutUrl(email);
26118
27189
  if (result.ok && result.url) {
@@ -26121,19 +27192,22 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
26121
27192
  }
26122
27193
  return result;
26123
27194
  });
26124
- electron.ipcMain.handle(Channels.PREMIUM_RESET, () => {
27195
+ electron.ipcMain.handle(Channels.PREMIUM_RESET, (event) => {
27196
+ assertTrustedIpcSender(event);
26125
27197
  trackPremiumFunnel("reset");
26126
27198
  const state2 = resetPremium();
26127
27199
  sendToRendererViews(Channels.PREMIUM_UPDATE, state2);
26128
27200
  return state2;
26129
27201
  });
26130
- electron.ipcMain.handle(Channels.PREMIUM_TRACK_CONTEXT, (_, step) => {
27202
+ electron.ipcMain.handle(Channels.PREMIUM_TRACK_CONTEXT, (event, step) => {
27203
+ assertTrustedIpcSender(event);
26131
27204
  assertString(step, "step");
26132
27205
  if (PREMIUM_TRACKABLE_STEPS.includes(step)) {
26133
27206
  trackPremiumFunnel(step);
26134
27207
  }
26135
27208
  });
26136
- electron.ipcMain.handle(Channels.PREMIUM_PORTAL, async () => {
27209
+ electron.ipcMain.handle(Channels.PREMIUM_PORTAL, async (event) => {
27210
+ assertTrustedIpcSender(event);
26137
27211
  trackPremiumFunnel("portal_opened");
26138
27212
  const result = await getPortalUrl();
26139
27213
  if (result.ok && result.url) {
@@ -26143,18 +27217,22 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
26143
27217
  });
26144
27218
  }
26145
27219
  function registerSessionHandlers(tabManager) {
26146
- electron.ipcMain.handle(Channels.SESSION_LIST, () => {
27220
+ electron.ipcMain.handle(Channels.SESSION_LIST, (event) => {
27221
+ assertTrustedIpcSender(event);
26147
27222
  return listNamedSessions();
26148
27223
  });
26149
- electron.ipcMain.handle(Channels.SESSION_SAVE, async (_, name) => {
27224
+ electron.ipcMain.handle(Channels.SESSION_SAVE, async (event, name) => {
27225
+ assertTrustedIpcSender(event);
26150
27226
  assertString(name, "name");
26151
27227
  return await saveNamedSession(tabManager, name);
26152
27228
  });
26153
- electron.ipcMain.handle(Channels.SESSION_LOAD, async (_, name) => {
27229
+ electron.ipcMain.handle(Channels.SESSION_LOAD, async (event, name) => {
27230
+ assertTrustedIpcSender(event);
26154
27231
  assertString(name, "name");
26155
27232
  return await loadNamedSession(tabManager, name);
26156
27233
  });
26157
- electron.ipcMain.handle(Channels.SESSION_DELETE, (_, name) => {
27234
+ electron.ipcMain.handle(Channels.SESSION_DELETE, (event, name) => {
27235
+ assertTrustedIpcSender(event);
26158
27236
  assertString(name, "name");
26159
27237
  return deleteNamedSession(name);
26160
27238
  });
@@ -26312,6 +27390,10 @@ function snapshot() {
26312
27390
  function emit() {
26313
27391
  broadcaster?.(Channels.PERMISSIONS_GET, snapshot());
26314
27392
  }
27393
+ function getDecision(origin, permission) {
27394
+ const existing = records.find((r) => r.origin === origin && r.permission === permission);
27395
+ return existing?.decision ?? sessionDecisions.get(key(origin, permission)) ?? null;
27396
+ }
26315
27397
  function save(origin, permission, decision) {
26316
27398
  const k = key(origin, permission);
26317
27399
  const existing = records.find((r) => key(r.origin, r.permission) === k);
@@ -26344,6 +27426,12 @@ function setPermissionBroadcaster(fn) {
26344
27426
  broadcaster = fn;
26345
27427
  }
26346
27428
  function installPermissionHandler() {
27429
+ electron.session.defaultSession.setPermissionCheckHandler((webContents, permission, requestingOrigin) => {
27430
+ if (!ALLOWED_PERMISSION_TYPES.has(permission)) return false;
27431
+ const origin = parseOrigin(requestingOrigin || webContents.getURL());
27432
+ if (!origin) return false;
27433
+ return getDecision(origin, permission) === "allow";
27434
+ });
26347
27435
  electron.session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => {
26348
27436
  if (!ALLOWED_PERMISSION_TYPES.has(permission)) {
26349
27437
  callback(false);
@@ -26355,19 +27443,21 @@ function installPermissionHandler() {
26355
27443
  return;
26356
27444
  }
26357
27445
  const k = key(origin, permission);
26358
- const existing = records.find((r) => r.origin === origin && r.permission === permission);
26359
- if (existing) {
26360
- callback(existing.decision === "allow");
26361
- return;
26362
- }
26363
- const sessionDecision = sessionDecisions.get(k);
26364
- if (sessionDecision) {
26365
- callback(sessionDecision === "allow");
27446
+ const decision = getDecision(origin, permission);
27447
+ if (decision) {
27448
+ callback(decision === "allow");
26366
27449
  return;
26367
27450
  }
26368
27451
  const result = electron.dialog.showMessageBoxSync({
26369
27452
  type: "question",
26370
- buttons: ["Deny", "Allow Once", "Allow Until Quit", "Always Allow"],
27453
+ buttons: [
27454
+ "Deny Once",
27455
+ "Deny Until Quit",
27456
+ "Always Deny",
27457
+ "Allow Once",
27458
+ "Allow Until Quit",
27459
+ "Always Allow"
27460
+ ],
26371
27461
  defaultId: 0,
26372
27462
  cancelId: 0,
26373
27463
  title: "Site permission request",
@@ -26375,20 +27465,29 @@ function installPermissionHandler() {
26375
27465
  detail: "Temporary choices are safer for camera, microphone, location, and clipboard access. Persistent choices can be cleared in Settings > Privacy."
26376
27466
  });
26377
27467
  if (result === 1) {
26378
- callback(true);
27468
+ sessionDecisions.set(k, "deny");
27469
+ callback(false);
26379
27470
  return;
26380
27471
  }
26381
27472
  if (result === 2) {
27473
+ save(origin, permission, "deny");
27474
+ callback(false);
27475
+ return;
27476
+ }
27477
+ if (result === 3) {
27478
+ callback(true);
27479
+ return;
27480
+ }
27481
+ if (result === 4) {
26382
27482
  sessionDecisions.set(k, "allow");
26383
27483
  callback(true);
26384
27484
  return;
26385
27485
  }
26386
- if (result === 3) {
27486
+ if (result === 5) {
26387
27487
  save(origin, permission, "allow");
26388
27488
  callback(true);
26389
27489
  return;
26390
27490
  }
26391
- save(origin, permission, "deny");
26392
27491
  callback(false);
26393
27492
  });
26394
27493
  }
@@ -26483,6 +27582,18 @@ function registerIpcHandlers(windowState, runtime2) {
26483
27582
  const requireTrusted = (event) => {
26484
27583
  assertTrustedIpcSender(event);
26485
27584
  };
27585
+ let researchOrchestrator = null;
27586
+ const getResearchOrchestrator = () => {
27587
+ if (!researchOrchestrator) {
27588
+ const settings2 = loadSettings();
27589
+ const provider = settings2.chatProvider ? createProvider(settings2.chatProvider) : null;
27590
+ researchOrchestrator = new ResearchOrchestrator(provider, tabManager, runtime2);
27591
+ researchOrchestrator.setUpdateListener((state2) => {
27592
+ sendToRendererViews(Channels.RESEARCH_STATE_UPDATE, state2);
27593
+ });
27594
+ }
27595
+ return researchOrchestrator;
27596
+ };
26486
27597
  electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, (event) => {
26487
27598
  requireTrusted(event);
26488
27599
  createPrivateWindow();
@@ -26491,7 +27602,10 @@ function registerIpcHandlers(windowState, runtime2) {
26491
27602
  requireTrusted(event);
26492
27603
  createSecondaryWindow();
26493
27604
  });
26494
- electron.ipcMain.handle(Channels.IS_PRIVATE_MODE, () => false);
27605
+ electron.ipcMain.handle(Channels.IS_PRIVATE_MODE, (event) => {
27606
+ requireTrusted(event);
27607
+ return false;
27608
+ });
26495
27609
  let sidebarResizeRecoveryTimer = null;
26496
27610
  let sidebarResizeActive = false;
26497
27611
  let runtimeUpdateTimer = null;
@@ -26572,37 +27686,45 @@ function registerIpcHandlers(windowState, runtime2) {
26572
27686
  onAIStreamIdle(() => {
26573
27687
  sendToRendererViews(Channels.AI_STREAM_IDLE);
26574
27688
  });
26575
- electron.ipcMain.handle(Channels.TAB_CREATE, (_, url) => {
27689
+ electron.ipcMain.handle(Channels.TAB_CREATE, (event, url) => {
27690
+ requireTrusted(event);
26576
27691
  const id = tabManager.createTab(url || loadSettings().defaultUrl);
26577
27692
  layoutViews(windowState);
26578
27693
  return id;
26579
27694
  });
26580
- electron.ipcMain.handle(Channels.TAB_CLOSE, (_, id) => {
27695
+ electron.ipcMain.handle(Channels.TAB_CLOSE, (event, id) => {
27696
+ requireTrusted(event);
26581
27697
  tabManager.closeTab(id);
26582
27698
  layoutViews(windowState);
26583
27699
  });
26584
- electron.ipcMain.handle(Channels.TAB_SWITCH, (_, id) => {
27700
+ electron.ipcMain.handle(Channels.TAB_SWITCH, (event, id) => {
27701
+ requireTrusted(event);
26585
27702
  tabManager.switchTab(id);
26586
27703
  layoutViews(windowState);
26587
27704
  });
26588
27705
  electron.ipcMain.handle(
26589
27706
  Channels.TAB_NAVIGATE,
26590
- (_, id, url, postBody) => {
27707
+ (event, id, url, postBody) => {
27708
+ requireTrusted(event);
26591
27709
  assertString(id, "tabId");
26592
27710
  assertString(url, "url");
26593
27711
  return tabManager.navigateTab(id, url, postBody);
26594
27712
  }
26595
27713
  );
26596
- electron.ipcMain.handle(Channels.TAB_BACK, (_, id) => {
27714
+ electron.ipcMain.handle(Channels.TAB_BACK, (event, id) => {
27715
+ requireTrusted(event);
26597
27716
  tabManager.goBack(id);
26598
27717
  });
26599
- electron.ipcMain.handle(Channels.TAB_FORWARD, (_, id) => {
27718
+ electron.ipcMain.handle(Channels.TAB_FORWARD, (event, id) => {
27719
+ requireTrusted(event);
26600
27720
  tabManager.goForward(id);
26601
27721
  });
26602
- electron.ipcMain.handle(Channels.TAB_RELOAD, (_, id) => {
27722
+ electron.ipcMain.handle(Channels.TAB_RELOAD, (event, id) => {
27723
+ requireTrusted(event);
26603
27724
  tabManager.reloadTab(id);
26604
27725
  });
26605
- electron.ipcMain.handle(Channels.TAB_TOGGLE_AD_BLOCK, (_, id) => {
27726
+ electron.ipcMain.handle(Channels.TAB_TOGGLE_AD_BLOCK, (event, id) => {
27727
+ requireTrusted(event);
26606
27728
  assertString(id, "id");
26607
27729
  const tab = tabManager.getTab(id);
26608
27730
  if (!tab) return null;
@@ -26610,87 +27732,108 @@ function registerIpcHandlers(windowState, runtime2) {
26610
27732
  tab.setAdBlockingEnabled(newState);
26611
27733
  return newState;
26612
27734
  });
26613
- electron.ipcMain.handle(Channels.TAB_ZOOM_IN, (_, id) => {
27735
+ electron.ipcMain.handle(Channels.TAB_ZOOM_IN, (event, id) => {
27736
+ requireTrusted(event);
26614
27737
  assertString(id, "id");
26615
27738
  tabManager.zoomIn(id);
26616
27739
  });
26617
- electron.ipcMain.handle(Channels.TAB_ZOOM_OUT, (_, id) => {
27740
+ electron.ipcMain.handle(Channels.TAB_ZOOM_OUT, (event, id) => {
27741
+ requireTrusted(event);
26618
27742
  assertString(id, "id");
26619
27743
  tabManager.zoomOut(id);
26620
27744
  });
26621
- electron.ipcMain.handle(Channels.TAB_ZOOM_RESET, (_, id) => {
27745
+ electron.ipcMain.handle(Channels.TAB_ZOOM_RESET, (event, id) => {
27746
+ requireTrusted(event);
26622
27747
  assertString(id, "id");
26623
27748
  tabManager.zoomReset(id);
26624
27749
  });
26625
- electron.ipcMain.handle(Channels.TAB_REOPEN_CLOSED, () => {
27750
+ electron.ipcMain.handle(Channels.TAB_REOPEN_CLOSED, (event) => {
27751
+ requireTrusted(event);
26626
27752
  const id = tabManager.reopenClosedTab();
26627
27753
  if (id) layoutViews(windowState);
26628
27754
  return id;
26629
27755
  });
26630
- electron.ipcMain.handle(Channels.TAB_DUPLICATE, (_, id) => {
27756
+ electron.ipcMain.handle(Channels.TAB_DUPLICATE, (event, id) => {
27757
+ requireTrusted(event);
26631
27758
  assertString(id, "id");
26632
27759
  const newId = tabManager.duplicateTab(id);
26633
27760
  if (newId) layoutViews(windowState);
26634
27761
  return newId;
26635
27762
  });
26636
- electron.ipcMain.handle(Channels.TAB_PIN, (_, id) => {
27763
+ electron.ipcMain.handle(Channels.TAB_PIN, (event, id) => {
27764
+ requireTrusted(event);
26637
27765
  assertString(id, "id");
26638
27766
  tabManager.pinTab(id);
26639
27767
  });
26640
- electron.ipcMain.handle(Channels.TAB_UNPIN, (_, id) => {
27768
+ electron.ipcMain.handle(Channels.TAB_UNPIN, (event, id) => {
27769
+ requireTrusted(event);
26641
27770
  assertString(id, "id");
26642
27771
  tabManager.unpinTab(id);
26643
27772
  });
26644
- electron.ipcMain.handle(Channels.TAB_GROUP_CREATE, (_, id) => {
27773
+ electron.ipcMain.handle(Channels.TAB_GROUP_CREATE, (event, id) => {
27774
+ requireTrusted(event);
26645
27775
  assertString(id, "id");
26646
27776
  return tabManager.createGroupFromTab(id);
26647
27777
  });
26648
- electron.ipcMain.handle(Channels.TAB_GROUP_ADD_TAB, (_, id, groupId) => {
27778
+ electron.ipcMain.handle(Channels.TAB_GROUP_ADD_TAB, (event, id, groupId) => {
27779
+ requireTrusted(event);
26649
27780
  assertString(id, "id");
26650
27781
  assertString(groupId, "groupId");
26651
27782
  tabManager.assignTabToGroup(id, groupId);
26652
27783
  });
26653
- electron.ipcMain.handle(Channels.TAB_GROUP_REMOVE_TAB, (_, id) => {
27784
+ electron.ipcMain.handle(Channels.TAB_GROUP_REMOVE_TAB, (event, id) => {
27785
+ requireTrusted(event);
26654
27786
  assertString(id, "id");
26655
27787
  tabManager.removeTabFromGroup(id);
26656
27788
  });
26657
- electron.ipcMain.handle(Channels.TAB_GROUP_TOGGLE_COLLAPSED, (_, groupId) => {
27789
+ electron.ipcMain.handle(Channels.TAB_GROUP_TOGGLE_COLLAPSED, (event, groupId) => {
27790
+ requireTrusted(event);
26658
27791
  assertString(groupId, "groupId");
26659
27792
  return tabManager.toggleGroupCollapsed(groupId);
26660
27793
  });
26661
27794
  electron.ipcMain.handle(
26662
27795
  Channels.TAB_GROUP_SET_COLOR,
26663
- (_, groupId, color) => {
27796
+ (event, groupId, color) => {
27797
+ requireTrusted(event);
26664
27798
  assertString(groupId, "groupId");
26665
27799
  assertString(color, "color");
26666
27800
  tabManager.setGroupColor(groupId, color);
26667
27801
  }
26668
27802
  );
26669
- electron.ipcMain.handle(Channels.TAB_TOGGLE_MUTE, (_, id) => {
27803
+ electron.ipcMain.handle(Channels.TAB_TOGGLE_MUTE, (event, id) => {
27804
+ requireTrusted(event);
26670
27805
  assertString(id, "id");
26671
27806
  return tabManager.toggleMuted(id);
26672
27807
  });
26673
- electron.ipcMain.handle(Channels.TAB_PRINT, (_, id) => {
27808
+ electron.ipcMain.handle(Channels.TAB_PRINT, (event, id) => {
27809
+ requireTrusted(event);
26674
27810
  assertString(id, "id");
26675
27811
  tabManager.printTab(id);
26676
27812
  });
26677
- electron.ipcMain.handle(Channels.TAB_PRINT_TO_PDF, (_, id) => {
27813
+ electron.ipcMain.handle(Channels.TAB_PRINT_TO_PDF, (event, id) => {
27814
+ requireTrusted(event);
26678
27815
  assertString(id, "id");
26679
27816
  return tabManager.saveTabAsPdf(id);
26680
27817
  });
26681
- electron.ipcMain.on(Channels.TAB_CONTEXT_MENU, (_event, id) => {
27818
+ electron.ipcMain.on(Channels.TAB_CONTEXT_MENU, (event, id) => {
27819
+ requireTrusted(event);
26682
27820
  assertString(id, "id");
26683
27821
  showTabContextMenu(tabManager, id, mainWindow, () => layoutViews(windowState));
26684
27822
  });
26685
- electron.ipcMain.on(Channels.TAB_GROUP_CONTEXT_MENU, (_event, groupId) => {
27823
+ electron.ipcMain.on(Channels.TAB_GROUP_CONTEXT_MENU, (event, groupId) => {
27824
+ requireTrusted(event);
26686
27825
  assertString(groupId, "groupId");
26687
27826
  showGroupContextMenu(tabManager, groupId, mainWindow);
26688
27827
  });
26689
- electron.ipcMain.handle(Channels.TAB_STATE_GET, () => ({
26690
- tabs: tabManager.getAllStates(),
26691
- activeId: tabManager.getActiveTabId() || ""
26692
- }));
26693
- electron.ipcMain.handle(Channels.AI_QUERY, async (_, query, history) => {
27828
+ electron.ipcMain.handle(Channels.TAB_STATE_GET, (event) => {
27829
+ requireTrusted(event);
27830
+ return {
27831
+ tabs: tabManager.getAllStates(),
27832
+ activeId: tabManager.getActiveTabId() || ""
27833
+ };
27834
+ });
27835
+ electron.ipcMain.handle(Channels.AI_QUERY, async (event, query, history) => {
27836
+ requireTrusted(event);
26694
27837
  const settings2 = loadSettings();
26695
27838
  const chatConfig = settings2.chatProvider;
26696
27839
  if (!chatConfig) {
@@ -26719,7 +27862,8 @@ function registerIpcHandlers(windowState, runtime2) {
26719
27862
  () => sendToRendererViews(Channels.AI_STREAM_END, "completed"),
26720
27863
  tabManager,
26721
27864
  runtime2,
26722
- history
27865
+ history,
27866
+ researchOrchestrator
26723
27867
  );
26724
27868
  } catch (err) {
26725
27869
  const msg = err instanceof Error ? err.message : "Unknown error";
@@ -26733,10 +27877,12 @@ function registerIpcHandlers(windowState, runtime2) {
26733
27877
  })();
26734
27878
  return { accepted: true };
26735
27879
  });
26736
- electron.ipcMain.handle(Channels.AI_CANCEL, () => {
27880
+ electron.ipcMain.handle(Channels.AI_CANCEL, (event) => {
27881
+ requireTrusted(event);
26737
27882
  activeChatProvider?.cancel();
26738
27883
  });
26739
- electron.ipcMain.handle(Channels.AI_FETCH_MODELS, async (_, config) => {
27884
+ electron.ipcMain.handle(Channels.AI_FETCH_MODELS, async (event, config) => {
27885
+ requireTrusted(event);
26740
27886
  try {
26741
27887
  if (!config || typeof config !== "object" || !("id" in config)) {
26742
27888
  return errorResult("Invalid provider configuration", { models: [] });
@@ -26748,12 +27894,14 @@ function registerIpcHandlers(windowState, runtime2) {
26748
27894
  return errorResult(getErrorMessage(err), { models: [] });
26749
27895
  }
26750
27896
  });
26751
- electron.ipcMain.handle(Channels.CONTENT_EXTRACT, async () => {
27897
+ electron.ipcMain.handle(Channels.CONTENT_EXTRACT, async (event) => {
27898
+ requireTrusted(event);
26752
27899
  const activeTab = tabManager.getActiveTab();
26753
27900
  if (!activeTab) return null;
26754
27901
  return extractContent(activeTab.view.webContents);
26755
27902
  });
26756
- electron.ipcMain.handle(Channels.READER_MODE_TOGGLE, async () => {
27903
+ electron.ipcMain.handle(Channels.READER_MODE_TOGGLE, async (event) => {
27904
+ requireTrusted(event);
26757
27905
  const activeTab = tabManager.getActiveTab();
26758
27906
  if (!activeTab) return;
26759
27907
  if (activeTab.state.isReaderMode) {
@@ -26773,7 +27921,8 @@ function registerIpcHandlers(windowState, runtime2) {
26773
27921
  );
26774
27922
  }
26775
27923
  });
26776
- electron.ipcMain.handle(Channels.SIDEBAR_TOGGLE, () => {
27924
+ electron.ipcMain.handle(Channels.SIDEBAR_TOGGLE, (event) => {
27925
+ requireTrusted(event);
26777
27926
  windowState.uiState.sidebarOpen = !windowState.uiState.sidebarOpen;
26778
27927
  layoutViews(windowState);
26779
27928
  return {
@@ -26781,7 +27930,8 @@ function registerIpcHandlers(windowState, runtime2) {
26781
27930
  width: windowState.uiState.sidebarWidth
26782
27931
  };
26783
27932
  });
26784
- electron.ipcMain.handle(Channels.SIDEBAR_NAVIGATE, (_, tab) => {
27933
+ electron.ipcMain.handle(Channels.SIDEBAR_NAVIGATE, (event, tab) => {
27934
+ requireTrusted(event);
26785
27935
  assertString(tab, "tab");
26786
27936
  if (!windowState.uiState.sidebarOpen) {
26787
27937
  windowState.uiState.sidebarOpen = true;
@@ -26795,7 +27945,8 @@ function registerIpcHandlers(windowState, runtime2) {
26795
27945
  width: windowState.uiState.sidebarWidth
26796
27946
  };
26797
27947
  });
26798
- electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_START, () => {
27948
+ electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_START, (event) => {
27949
+ requireTrusted(event);
26799
27950
  sidebarResizeActive = true;
26800
27951
  clearSidebarResizeRecoveryTimer();
26801
27952
  const [width, height] = windowState.mainWindow.getContentSize();
@@ -26810,14 +27961,16 @@ function registerIpcHandlers(windowState, runtime2) {
26810
27961
  });
26811
27962
  scheduleSidebarResizeRecovery();
26812
27963
  });
26813
- electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (_, width) => {
27964
+ electron.ipcMain.handle(Channels.SIDEBAR_RESIZE, (event, width) => {
27965
+ requireTrusted(event);
26814
27966
  assertNumber(width, "width");
26815
27967
  const clamped = Math.max(240, Math.min(800, Math.round(width)));
26816
27968
  windowState.uiState.sidebarWidth = clamped;
26817
27969
  resizeSidebarViews(windowState);
26818
27970
  return clamped;
26819
27971
  });
26820
- electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_COMMIT, () => {
27972
+ electron.ipcMain.handle(Channels.SIDEBAR_RESIZE_COMMIT, (event) => {
27973
+ requireTrusted(event);
26821
27974
  sidebarResizeActive = false;
26822
27975
  clearSidebarResizeRecoveryTimer();
26823
27976
  setSetting("sidebarWidth", windowState.uiState.sidebarWidth);
@@ -26825,7 +27978,8 @@ function registerIpcHandlers(windowState, runtime2) {
26825
27978
  });
26826
27979
  electron.ipcMain.on(
26827
27980
  Channels.RENDERER_VIEW_READY,
26828
- (_event, view) => {
27981
+ (event, view) => {
27982
+ requireTrusted(event);
26829
27983
  if (view !== "sidebar") return;
26830
27984
  if (!windowState.uiState.sidebarOpen) {
26831
27985
  windowState.uiState.sidebarOpen = true;
@@ -26833,12 +27987,14 @@ function registerIpcHandlers(windowState, runtime2) {
26833
27987
  }
26834
27988
  }
26835
27989
  );
26836
- electron.ipcMain.handle(Channels.FOCUS_MODE_TOGGLE, () => {
27990
+ electron.ipcMain.handle(Channels.FOCUS_MODE_TOGGLE, (event) => {
27991
+ requireTrusted(event);
26837
27992
  windowState.uiState.focusMode = !windowState.uiState.focusMode;
26838
27993
  layoutViews(windowState);
26839
27994
  return windowState.uiState.focusMode;
26840
27995
  });
26841
- electron.ipcMain.handle(Channels.SETTINGS_VISIBILITY, (_, open) => {
27996
+ electron.ipcMain.handle(Channels.SETTINGS_VISIBILITY, (event, open) => {
27997
+ requireTrusted(event);
26842
27998
  windowState.uiState.settingsOpen = open;
26843
27999
  if (open) {
26844
28000
  windowState.uiState.sidebarOpen = false;
@@ -26846,10 +28002,14 @@ function registerIpcHandlers(windowState, runtime2) {
26846
28002
  layoutViews(windowState);
26847
28003
  return windowState.uiState.settingsOpen;
26848
28004
  });
26849
- electron.ipcMain.handle(Channels.SETTINGS_GET, () => {
28005
+ electron.ipcMain.handle(Channels.SETTINGS_GET, (event) => {
28006
+ requireTrusted(event);
26850
28007
  return getRendererSettings();
26851
28008
  });
26852
- electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
28009
+ electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, (event) => {
28010
+ requireTrusted(event);
28011
+ return getRuntimeHealth();
28012
+ });
26853
28013
  electron.ipcMain.handle(Channels.MCP_REGENERATE_TOKEN, (event) => {
26854
28014
  requireTrusted(event);
26855
28015
  return regenerateMcpAuthToken();
@@ -26870,11 +28030,20 @@ function registerIpcHandlers(windowState, runtime2) {
26870
28030
  await stopMcpServer();
26871
28031
  await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
26872
28032
  }
28033
+ if (key2 === "chatProvider" && researchOrchestrator) {
28034
+ try {
28035
+ researchOrchestrator.setProvider(createProvider(value));
28036
+ } catch {
28037
+ }
28038
+ }
26873
28039
  const rendererSettings = getRendererSettings();
26874
28040
  sendToRendererViews(Channels.SETTINGS_UPDATE, rendererSettings);
26875
28041
  return rendererSettings;
26876
28042
  });
26877
- electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime2.getState());
28043
+ electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, (event) => {
28044
+ requireTrusted(event);
28045
+ return runtime2.getState();
28046
+ });
26878
28047
  electron.ipcMain.handle(Channels.AGENT_PAUSE, (event) => {
26879
28048
  requireTrusted(event);
26880
28049
  return runtime2.pause();
@@ -26905,24 +28074,27 @@ function registerIpcHandlers(windowState, runtime2) {
26905
28074
  );
26906
28075
  electron.ipcMain.handle(
26907
28076
  Channels.AGENT_CHECKPOINT_CREATE,
26908
- (_, name, note) => runtime2.createCheckpoint(name, note)
26909
- );
26910
- electron.ipcMain.handle(
26911
- Channels.AGENT_CHECKPOINT_RESTORE,
26912
- (_, checkpointId) => runtime2.restoreCheckpoint(checkpointId)
26913
- );
26914
- electron.ipcMain.handle(
26915
- Channels.AGENT_CHECKPOINT_UPDATE_NOTE,
26916
- (_, checkpointId, note) => runtime2.updateCheckpointNote(checkpointId, note || "")
26917
- );
26918
- electron.ipcMain.handle(
26919
- Channels.AGENT_UNDO_LAST_ACTION,
26920
- () => runtime2.undoLastAction()
26921
- );
26922
- electron.ipcMain.handle(
26923
- Channels.AGENT_SESSION_CAPTURE,
26924
- (_, note) => runtime2.captureSession(note)
28077
+ (event, name, note) => {
28078
+ requireTrusted(event);
28079
+ return runtime2.createCheckpoint(name, note);
28080
+ }
26925
28081
  );
28082
+ electron.ipcMain.handle(Channels.AGENT_CHECKPOINT_RESTORE, (event, checkpointId) => {
28083
+ requireTrusted(event);
28084
+ return runtime2.restoreCheckpoint(checkpointId);
28085
+ });
28086
+ electron.ipcMain.handle(Channels.AGENT_CHECKPOINT_UPDATE_NOTE, (event, checkpointId, note) => {
28087
+ requireTrusted(event);
28088
+ return runtime2.updateCheckpointNote(checkpointId, note || "");
28089
+ });
28090
+ electron.ipcMain.handle(Channels.AGENT_UNDO_LAST_ACTION, (event) => {
28091
+ requireTrusted(event);
28092
+ return runtime2.undoLastAction();
28093
+ });
28094
+ electron.ipcMain.handle(Channels.AGENT_SESSION_CAPTURE, (event, note) => {
28095
+ requireTrusted(event);
28096
+ return runtime2.captureSession(note);
28097
+ });
26926
28098
  electron.ipcMain.handle(
26927
28099
  Channels.AGENT_SESSION_RESTORE,
26928
28100
  (event, snapshot2) => {
@@ -26931,7 +28103,8 @@ function registerIpcHandlers(windowState, runtime2) {
26931
28103
  }
26932
28104
  );
26933
28105
  registerBookmarkHandlers();
26934
- electron.ipcMain.handle(Channels.HIGHLIGHT_CAPTURE, async () => {
28106
+ electron.ipcMain.handle(Channels.HIGHLIGHT_CAPTURE, async (event) => {
28107
+ requireTrusted(event);
26935
28108
  try {
26936
28109
  const activeTab = tabManager.getActiveTab();
26937
28110
  if (!activeTab) {
@@ -26975,10 +28148,12 @@ function registerIpcHandlers(windowState, runtime2) {
26975
28148
  logger$4.warn("Failed to persist auto-highlight selection:", err);
26976
28149
  }
26977
28150
  });
26978
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_COUNT, () => {
28151
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_COUNT, (event) => {
28152
+ requireTrusted(event);
26979
28153
  return getActiveHighlightCountSafe();
26980
28154
  });
26981
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_SCROLL, (_, index) => {
28155
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_SCROLL, (event, index) => {
28156
+ requireTrusted(event);
26982
28157
  const info = getActiveTabInfo(tabManager);
26983
28158
  if (!info) return false;
26984
28159
  try {
@@ -26988,7 +28163,8 @@ function registerIpcHandlers(windowState, runtime2) {
26988
28163
  return false;
26989
28164
  }
26990
28165
  });
26991
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, async (_, index) => {
28166
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_REMOVE, async (event, index) => {
28167
+ requireTrusted(event);
26992
28168
  const info = getActiveTabInfo(tabManager);
26993
28169
  if (!info) return false;
26994
28170
  try {
@@ -27002,7 +28178,8 @@ function registerIpcHandlers(windowState, runtime2) {
27002
28178
  return false;
27003
28179
  }
27004
28180
  });
27005
- electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, async () => {
28181
+ electron.ipcMain.handle(Channels.HIGHLIGHT_NAV_CLEAR, async (event) => {
28182
+ requireTrusted(event);
27006
28183
  const info = getActiveTabInfo(tabManager);
27007
28184
  if (!info) return false;
27008
28185
  try {
@@ -27017,22 +28194,27 @@ function registerIpcHandlers(windowState, runtime2) {
27017
28194
  }
27018
28195
  });
27019
28196
  const findBridge = createFindInPageBridge(tabManager, chromeView);
27020
- electron.ipcMain.handle(Channels.FIND_IN_PAGE_START, (_, text, options) => {
28197
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_START, (event, text, options) => {
28198
+ requireTrusted(event);
27021
28199
  return findBridge.start(text, options);
27022
28200
  });
27023
- electron.ipcMain.handle(Channels.FIND_IN_PAGE_NEXT, (_, forward) => {
28201
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_NEXT, (event, forward) => {
28202
+ requireTrusted(event);
27024
28203
  return findBridge.next(forward);
27025
28204
  });
27026
- electron.ipcMain.handle(Channels.FIND_IN_PAGE_STOP, (_, action) => {
28205
+ electron.ipcMain.handle(Channels.FIND_IN_PAGE_STOP, (event, action) => {
28206
+ requireTrusted(event);
27027
28207
  findBridge.stop(action);
27028
28208
  });
27029
28209
  registerHistoryHandlers();
27030
- electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_TOGGLE, () => {
28210
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_TOGGLE, (event) => {
28211
+ requireTrusted(event);
27031
28212
  windowState.uiState.devtoolsPanelOpen = !windowState.uiState.devtoolsPanelOpen;
27032
28213
  layoutViews(windowState);
27033
28214
  return { open: windowState.uiState.devtoolsPanelOpen };
27034
28215
  });
27035
- electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE, (_, height) => {
28216
+ electron.ipcMain.handle(Channels.DEVTOOLS_PANEL_RESIZE, (event, height) => {
28217
+ requireTrusted(event);
27036
28218
  const clamped = Math.max(MIN_DEVTOOLS_PANEL, Math.min(MAX_DEVTOOLS_PANEL, Math.round(height)));
27037
28219
  windowState.uiState.devtoolsPanelHeight = clamped;
27038
28220
  layoutViews(windowState);
@@ -27045,7 +28227,8 @@ function registerIpcHandlers(windowState, runtime2) {
27045
28227
  registerHumanVaultHandlers();
27046
28228
  registerWindowControlHandlers(mainWindow);
27047
28229
  registerCodexHandlers();
27048
- electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, () => {
28230
+ electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, (event) => {
28231
+ requireTrusted(event);
27049
28232
  return getInstalledKits();
27050
28233
  });
27051
28234
  electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async (event) => {
@@ -27060,6 +28243,7 @@ function registerIpcHandlers(windowState, runtime2) {
27060
28243
  registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
27061
28244
  registerAutofillHandlers(windowState);
27062
28245
  registerPageDiffHandlers(windowState, sendToRendererViews);
28246
+ registerResearchHandlers(() => getResearchOrchestrator());
27063
28247
  electron.ipcMain.handle(Channels.CLEAR_BROWSING_DATA, async (event, options) => {
27064
28248
  requireTrusted(event);
27065
28249
  const { cache, cookies, history, localStorage: clearLs, timeRange } = options;
@@ -27078,7 +28262,10 @@ function registerIpcHandlers(windowState, runtime2) {
27078
28262
  });
27079
28263
  setDownloadBroadcaster(sendToRendererViews);
27080
28264
  setPermissionBroadcaster(sendToRendererViews);
27081
- electron.ipcMain.handle(Channels.DOWNLOADS_GET, () => listDownloads());
28265
+ electron.ipcMain.handle(Channels.DOWNLOADS_GET, (event) => {
28266
+ requireTrusted(event);
28267
+ return listDownloads();
28268
+ });
27082
28269
  electron.ipcMain.handle(Channels.DOWNLOADS_CLEAR, (event) => {
27083
28270
  requireTrusted(event);
27084
28271
  clearDownloads();
@@ -27092,7 +28279,10 @@ function registerIpcHandlers(windowState, runtime2) {
27092
28279
  requireTrusted(event);
27093
28280
  return showDownloadInFolder(id);
27094
28281
  });
27095
- electron.ipcMain.handle(Channels.PERMISSIONS_GET, () => listPermissions());
28282
+ electron.ipcMain.handle(Channels.PERMISSIONS_GET, (event) => {
28283
+ requireTrusted(event);
28284
+ return listPermissions();
28285
+ });
27096
28286
  electron.ipcMain.handle(Channels.PERMISSIONS_CLEAR, (event) => {
27097
28287
  requireTrusted(event);
27098
28288
  clearPermissions();
@@ -27103,12 +28293,16 @@ function registerIpcHandlers(windowState, runtime2) {
27103
28293
  clearPermissionsForOrigin(origin);
27104
28294
  return true;
27105
28295
  });
27106
- electron.ipcMain.handle(Channels.UPDATES_CHECK, () => checkForUpdates());
28296
+ electron.ipcMain.handle(Channels.UPDATES_CHECK, (event) => {
28297
+ requireTrusted(event);
28298
+ return checkForUpdates();
28299
+ });
27107
28300
  electron.ipcMain.handle(Channels.UPDATES_OPEN_DOWNLOAD, (event) => {
27108
28301
  requireTrusted(event);
27109
28302
  return openUpdateDownload();
27110
28303
  });
27111
- electron.ipcMain.handle(Channels.TAB_TOGGLE_PIP, async () => {
28304
+ electron.ipcMain.handle(Channels.TAB_TOGGLE_PIP, async (event) => {
28305
+ requireTrusted(event);
27112
28306
  return togglePictureInPicture(tabManager);
27113
28307
  });
27114
28308
  }
@@ -27496,6 +28690,10 @@ ${lines.join("\n")}
27496
28690
  }
27497
28691
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
27498
28692
  const PERSIST_DEBOUNCE_MS = 500;
28693
+ const INTERRUPTED_ACTION_STATUSES = /* @__PURE__ */ new Set([
28694
+ "running",
28695
+ "waiting-approval"
28696
+ ]);
27499
28697
  const logger$3 = createLogger("Runtime");
27500
28698
  function clone(value) {
27501
28699
  return JSON.parse(JSON.stringify(value));
@@ -27518,6 +28716,15 @@ function getRuntimeStatePath() {
27518
28716
  return path$1.join(electron.app.getPath("userData"), "vessel-agent-runtime.json");
27519
28717
  }
27520
28718
  function sanitizePersistence(persisted) {
28719
+ const recoveredAt = (/* @__PURE__ */ new Date()).toISOString();
28720
+ const actions = Array.isArray(persisted?.actions) ? persisted.actions.slice(-120).map(
28721
+ (action) => INTERRUPTED_ACTION_STATUSES.has(action.status) ? {
28722
+ ...action,
28723
+ status: "failed",
28724
+ finishedAt: action.finishedAt ?? recoveredAt,
28725
+ error: action.error ?? "Action was interrupted before the previous Vessel session ended."
28726
+ } : action
28727
+ ) : [];
27521
28728
  return {
27522
28729
  session: persisted?.session ?? null,
27523
28730
  supervisor: {
@@ -27526,7 +28733,7 @@ function sanitizePersistence(persisted) {
27526
28733
  pendingApprovals: [],
27527
28734
  lastError: persisted?.supervisor?.lastError
27528
28735
  },
27529
- actions: Array.isArray(persisted?.actions) ? persisted.actions.slice(-120) : [],
28736
+ actions,
27530
28737
  checkpoints: Array.isArray(persisted?.checkpoints) ? persisted.checkpoints.slice(-20) : [],
27531
28738
  transcript: [],
27532
28739
  mcpStatus: "stopped",