@quanta-intellect/vessel-browser 0.1.95 → 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 = [];
@@ -279,7 +280,7 @@ function persistNow() {
279
280
  JSON.stringify(buildPersistedSettings(settings), null, 2),
280
281
  { encoding: "utf-8", mode: 384 }
281
282
  )
282
- ).then(() => fs.promises.chmod(getSettingsPath(), 384).catch(() => void 0)).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));
283
284
  }
284
285
  function saveSettings() {
285
286
  saveDirty = true;
@@ -408,7 +409,7 @@ function loadTrustedAppURL(wc, url) {
408
409
  }
409
410
  const MAX_CUSTOM_HISTORY = 50;
410
411
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
411
- const logger$m = createLogger("Tab");
412
+ const logger$o = createLogger("Tab");
412
413
  const sessionCertExceptions = /* @__PURE__ */ new WeakMap();
413
414
  const sessionsWithVerifyProc = /* @__PURE__ */ new WeakSet();
414
415
  const CERT_VERIFY_TRUST = 0;
@@ -474,7 +475,7 @@ class Tab {
474
475
  guardedLoadURL(url, options) {
475
476
  const blockReason = this.getNavigationBlockReason(url);
476
477
  if (blockReason) {
477
- logger$m.warn(blockReason);
478
+ logger$o.warn(blockReason);
478
479
  return blockReason;
479
480
  }
480
481
  void this.view.webContents.loadURL(url, options);
@@ -558,7 +559,7 @@ class Tab {
558
559
  wc.setWindowOpenHandler(({ url, disposition }) => {
559
560
  const error = this.getNavigationBlockReason(url);
560
561
  if (error) {
561
- logger$m.warn(error);
562
+ logger$o.warn(error);
562
563
  return { action: "deny" };
563
564
  }
564
565
  this.onOpenUrl?.({
@@ -572,7 +573,7 @@ class Tab {
572
573
  const error = this.getNavigationBlockReason(url);
573
574
  if (!error) return;
574
575
  event.preventDefault();
575
- logger$m.warn(`${context}: ${error}`);
576
+ logger$o.warn(`${context}: ${error}`);
576
577
  };
577
578
  wc.on("will-navigate", (event, url) => {
578
579
  blockNavigation(event, url, "Blocked top-level navigation");
@@ -656,7 +657,7 @@ class Tab {
656
657
  ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 999px; }
657
658
  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); }
658
659
  ::-webkit-scrollbar-corner { background: transparent; }
659
- `).catch((err) => logger$m.warn("Failed to inject scrollbar CSS:", err));
660
+ `).catch((err) => logger$o.warn("Failed to inject scrollbar CSS:", err));
660
661
  });
661
662
  wc.on("page-favicon-updated", (_, favicons) => {
662
663
  this._state.favicon = favicons[0] || "";
@@ -692,7 +693,7 @@ class Tab {
692
693
  ).then((highlightedText) => {
693
694
  this.buildContextMenu(wc, params, highlightedText.trim());
694
695
  }).catch((err) => {
695
- 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);
696
697
  this.buildContextMenu(wc, params, "");
697
698
  });
698
699
  });
@@ -893,7 +894,7 @@ class Tab {
893
894
  "document.documentElement.outerHTML"
894
895
  );
895
896
  } catch (err) {
896
- logger$m.warn("Failed to retrieve page source:", err);
897
+ logger$o.warn("Failed to retrieve page source:", err);
897
898
  return;
898
899
  }
899
900
  const escaped = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -1021,7 +1022,7 @@ class Tab {
1021
1022
  document.addEventListener('mouseup', window.__vesselHighlightHandler);
1022
1023
  }
1023
1024
  })()
1024
- `).catch((err) => logger$m.warn("Failed to inject highlight listener:", err));
1025
+ `).catch((err) => logger$o.warn("Failed to inject highlight listener:", err));
1025
1026
  } else {
1026
1027
  void wc.executeJavaScript(`
1027
1028
  (function() {
@@ -1032,7 +1033,7 @@ class Tab {
1032
1033
  delete window.__vesselHighlightHandler;
1033
1034
  }
1034
1035
  })()
1035
- `).catch((err) => logger$m.warn("Failed to remove highlight listener:", err));
1036
+ `).catch((err) => logger$o.warn("Failed to remove highlight listener:", err));
1036
1037
  }
1037
1038
  }
1038
1039
  get webContentsId() {
@@ -1069,7 +1070,7 @@ const SEARCH_ENGINE_PRESETS = {
1069
1070
  ecosia: { label: "Ecosia", url: "https://www.ecosia.org/search?q=" },
1070
1071
  kagi: { label: "Kagi", url: "https://kagi.com/search?q=" }
1071
1072
  };
1072
- const logger$l = createLogger("JsonPersistence");
1073
+ const logger$n = createLogger("JsonPersistence");
1073
1074
  function canUseSafeStorage() {
1074
1075
  try {
1075
1076
  return electron.safeStorage.isEncryptionAvailable();
@@ -1134,7 +1135,7 @@ function createDebouncedJsonPersistence({
1134
1135
  data,
1135
1136
  typeof data === "string" ? { encoding: "utf-8", mode: 384 } : { mode: 384 }
1136
1137
  )
1137
- ).then(() => fs.promises.chmod(filePath2, 384).catch(() => void 0)).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));
1138
1139
  };
1139
1140
  const schedule = () => {
1140
1141
  saveDirty2 = true;
@@ -2815,7 +2816,7 @@ function destroySession(tabId) {
2815
2816
  sessions.delete(tabId);
2816
2817
  }
2817
2818
  }
2818
- const logger$k = createLogger("TabManager");
2819
+ const logger$m = createLogger("TabManager");
2819
2820
  function sanitizePdfFilename(title) {
2820
2821
  const clean = title.replace(/[<>:"/\\|?*\x00-\x1f]/g, " ").replace(/\s+/g, " ").trim();
2821
2822
  const base = (clean || "Vessel Page").replace(/\.pdf$/i, "");
@@ -3214,7 +3215,7 @@ class TabManager {
3214
3215
  }));
3215
3216
  if (entries.length > 0) {
3216
3217
  void highlightBatchOnPage(wc, entries).catch(
3217
- (err) => logger$k.warn("Failed to batch highlight:", err)
3218
+ (err) => logger$m.warn("Failed to batch highlight:", err)
3218
3219
  );
3219
3220
  }
3220
3221
  }
@@ -3236,12 +3237,12 @@ class TabManager {
3236
3237
  const result = await captureSelectionHighlight(wc);
3237
3238
  if (result.success && result.text) {
3238
3239
  await highlightOnPage(wc, null, result.text, void 0, void 0, "yellow").catch(
3239
- (err) => logger$k.warn("Failed to capture highlight:", err)
3240
+ (err) => logger$m.warn("Failed to capture highlight:", err)
3240
3241
  );
3241
3242
  }
3242
3243
  this.highlightCaptureCallback?.(result);
3243
3244
  } catch (err) {
3244
- logger$k.warn("Failed to capture highlight from page:", err);
3245
+ logger$m.warn("Failed to capture highlight from page:", err);
3245
3246
  this.highlightCaptureCallback?.({
3246
3247
  success: false,
3247
3248
  message: "Could not capture selection"
@@ -3266,7 +3267,7 @@ class TabManager {
3266
3267
  void this.removeHighlightMarksForText(wc, text);
3267
3268
  }
3268
3269
  } catch (err) {
3269
- logger$k.warn("Failed to remove highlight from matching tab:", err);
3270
+ logger$m.warn("Failed to remove highlight from matching tab:", err);
3270
3271
  }
3271
3272
  }
3272
3273
  this.highlightCaptureCallback?.({
@@ -3297,12 +3298,12 @@ class TabManager {
3297
3298
  void 0,
3298
3299
  color
3299
3300
  ).catch(
3300
- (err) => logger$k.warn("Failed to update highlight color:", err)
3301
+ (err) => logger$m.warn("Failed to update highlight color:", err)
3301
3302
  );
3302
3303
  });
3303
3304
  }
3304
3305
  } catch (err) {
3305
- logger$k.warn("Failed to iterate highlights for color change:", err);
3306
+ logger$m.warn("Failed to iterate highlights for color change:", err);
3306
3307
  }
3307
3308
  }
3308
3309
  this.highlightCaptureCallback?.({
@@ -3343,7 +3344,7 @@ class TabManager {
3343
3344
  });
3344
3345
  })()`
3345
3346
  ).catch(
3346
- (err) => logger$k.warn("Failed to remove highlight marks:", err)
3347
+ (err) => logger$m.warn("Failed to remove highlight marks:", err)
3347
3348
  );
3348
3349
  }
3349
3350
  broadcastState() {
@@ -3373,6 +3374,7 @@ const Channels = {
3373
3374
  AI_STREAM_CHUNK: "ai:stream-chunk",
3374
3375
  AI_STREAM_END: "ai:stream-end",
3375
3376
  AI_STREAM_IDLE: "ai:stream-idle",
3377
+ AI_RESEARCH_CLARIFICATION: "ai:research-clarification",
3376
3378
  AUTOMATION_ACTIVITY_START: "automation:activity-start",
3377
3379
  AUTOMATION_ACTIVITY_CHUNK: "automation:activity-chunk",
3378
3380
  AUTOMATION_ACTIVITY_END: "automation:activity-end",
@@ -3553,6 +3555,16 @@ const Channels = {
3553
3555
  CLEAR_BROWSING_DATA_OPEN: "browsing-data:open",
3554
3556
  // Picture-in-Picture
3555
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",
3556
3568
  // Codex OAuth
3557
3569
  CODEX_START_AUTH: "codex:start-auth",
3558
3570
  CODEX_CANCEL_AUTH: "codex:cancel-auth",
@@ -4522,7 +4534,7 @@ function errorResult(error, value) {
4522
4534
  function getErrorMessage(error, fallback = "Unknown error") {
4523
4535
  return error instanceof Error && error.message ? error.message : fallback;
4524
4536
  }
4525
- const logger$j = createLogger("Premium");
4537
+ const logger$l = createLogger("Premium");
4526
4538
  const VERIFICATION_API = process.env.VESSEL_PREMIUM_API || "https://vesselpremium.quantaintellect.com";
4527
4539
  const FREE_TOOL_ITERATION_LIMIT = 50;
4528
4540
  const REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
@@ -4562,7 +4574,10 @@ const PREMIUM_TOOLS = /* @__PURE__ */ new Set([
4562
4574
  "vault_totp",
4563
4575
  "human_vault_list",
4564
4576
  "human_vault_fill",
4565
- "human_vault_remove"
4577
+ "human_vault_remove",
4578
+ "research_confirm_brief",
4579
+ "research_approve_objectives",
4580
+ "research_export_report"
4566
4581
  ]);
4567
4582
  function isPremium() {
4568
4583
  const { premium } = loadSettings();
@@ -4638,7 +4653,7 @@ async function verifySubscription$1(identifier) {
4638
4653
  });
4639
4654
  if (!res.ok) {
4640
4655
  const detail = await readApiErrorDetail(res);
4641
- logger$j.warn(
4656
+ logger$l.warn(
4642
4657
  "Verification API returned a non-OK status:",
4643
4658
  res.status,
4644
4659
  detail
@@ -4657,7 +4672,7 @@ async function verifySubscription$1(identifier) {
4657
4672
  setSetting("premium", updated);
4658
4673
  return updated;
4659
4674
  } catch (err) {
4660
- logger$j.warn("Verification failed:", err);
4675
+ logger$l.warn("Verification failed:", err);
4661
4676
  return current;
4662
4677
  }
4663
4678
  }
@@ -5262,7 +5277,7 @@ const EXTRACT_TIMEOUT_MAX_MS = 2e4;
5262
5277
  const MUTATION_CAPTURE_INTERVAL_MS = 5e3;
5263
5278
  const MUTATION_SETTLE_AFTER_MS = 1500;
5264
5279
  const AGENT_STREAM_IDLE_TIMEOUT_MS = 3e4;
5265
- const logger$i = createLogger("Extractor");
5280
+ const logger$k = createLogger("Extractor");
5266
5281
  const EMPTY_PAGE_CONTENT = {
5267
5282
  title: "",
5268
5283
  content: "",
@@ -5988,7 +6003,8 @@ async function waitForDomReady(webContents, timeoutMs = DEFAULT_PAGE_SCRIPT_TIME
5988
6003
  (function() {
5989
6004
  return document.readyState || "";
5990
6005
  })()
5991
- `
6006
+ `,
6007
+ { label: "ready-state" }
5992
6008
  );
5993
6009
  if (readyState === "interactive" || readyState === "complete") {
5994
6010
  return;
@@ -5996,7 +6012,7 @@ async function waitForDomReady(webContents, timeoutMs = DEFAULT_PAGE_SCRIPT_TIME
5996
6012
  await delay(75);
5997
6013
  }
5998
6014
  }
5999
- async function executeScript(webContents, script) {
6015
+ async function executeScript(webContents, script, options = {}) {
6000
6016
  if (webContents.isDestroyed()) {
6001
6017
  return null;
6002
6018
  }
@@ -6012,7 +6028,15 @@ async function executeScript(webContents, script) {
6012
6028
  })
6013
6029
  ]);
6014
6030
  } catch (err) {
6015
- 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
+ }
6016
6040
  return null;
6017
6041
  } finally {
6018
6042
  if (timer) {
@@ -6109,7 +6133,8 @@ async function estimateExtractionTimeout(webContents) {
6109
6133
  try {
6110
6134
  const elementCount = await executeScript(
6111
6135
  webContents,
6112
- `(function() { try { return document.querySelectorAll('*').length; } catch { return 0; } })()`
6136
+ `(function() { try { return document.querySelectorAll('*').length; } catch { return 0; } })()`,
6137
+ { label: "element-count" }
6113
6138
  );
6114
6139
  if (typeof elementCount === "number" && elementCount > 5e3) {
6115
6140
  const extra = Math.min(
@@ -6119,15 +6144,19 @@ async function estimateExtractionTimeout(webContents) {
6119
6144
  return EXTRACT_TIMEOUT_BASE_MS + extra;
6120
6145
  }
6121
6146
  } catch (err) {
6122
- 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);
6123
6148
  }
6124
6149
  return EXTRACT_TIMEOUT_BASE_MS;
6125
6150
  }
6126
6151
  async function extractContentInner(webContents) {
6127
6152
  await waitForDomReady(webContents);
6128
6153
  const [preloadResult, directResult] = await Promise.all([
6129
- executeScript(webContents, PRELOAD_EXTRACTION_SCRIPT),
6130
- 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
+ })
6131
6160
  ]);
6132
6161
  return mergePageContent(
6133
6162
  [preloadResult, directResult],
@@ -7007,6 +7036,7 @@ function isClickReadLoop(names) {
7007
7036
  }
7008
7037
  return clickReadPairs >= 2;
7009
7038
  }
7039
+ const TERMINAL_TOOL_RESULT = "__VESSEL_TERMINAL_TOOL_RESULT__";
7010
7040
  const ANTHROPIC_MAX_TOKENS = 4096;
7011
7041
  function isRecord$1(value) {
7012
7042
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -7210,6 +7240,9 @@ class AnthropicProvider {
7210
7240
  const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
7211
7241
  result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
7212
7242
  }
7243
+ if (result === TERMINAL_TOOL_RESULT) {
7244
+ return;
7245
+ }
7213
7246
  let parsedRich = null;
7214
7247
  try {
7215
7248
  const parsed = JSON.parse(result);
@@ -7478,7 +7511,7 @@ const MAX_MCP_NAV_CONTENT_LENGTH = 3e4;
7478
7511
  const MAX_AGENT_DEBUG_CONTENT_LENGTH = 2e4;
7479
7512
  const LLAMA_CPP_MIN_CTX_TOKENS = 16384;
7480
7513
  const LLAMA_CPP_RECOMMENDED_CTX_TOKENS = 32768;
7481
- const logger$h = createLogger("OpenAIProvider");
7514
+ const logger$j = createLogger("OpenAIProvider");
7482
7515
  function shouldDebugAgentLoop() {
7483
7516
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
7484
7517
  return value === "1" || value === "true";
@@ -8003,9 +8036,9 @@ function resolveToolCallName(rawName, args, availableToolNames) {
8003
8036
  function logAgentLoopDebug(payload) {
8004
8037
  if (!shouldDebugAgentLoop()) return;
8005
8038
  try {
8006
- logger$h.info(`[agent-debug] ${JSON.stringify(payload)}`);
8039
+ logger$j.info(`[agent-debug] ${JSON.stringify(payload)}`);
8007
8040
  } catch (err) {
8008
- logger$h.warn("Failed to serialize debug payload:", err);
8041
+ logger$j.warn("Failed to serialize debug payload:", err);
8009
8042
  }
8010
8043
  }
8011
8044
  function recoverTextEncodedToolCalls(text, availableToolNames) {
@@ -8476,6 +8509,9 @@ class OpenAICompatProvider {
8476
8509
  const msg = toolErr instanceof Error ? toolErr.message : String(toolErr);
8477
8510
  result = `Error: Tool execution failed — ${msg}. Try a different approach or call read_page to refresh context.`;
8478
8511
  }
8512
+ if (result === TERMINAL_TOOL_RESULT) {
8513
+ return;
8514
+ }
8479
8515
  let toolContent = result;
8480
8516
  try {
8481
8517
  const parsed = JSON.parse(result);
@@ -8553,7 +8589,7 @@ async function openExternalAllowlisted(url, rule) {
8553
8589
  }
8554
8590
  await electron.shell.openExternal(parsed.toString());
8555
8591
  }
8556
- const logger$g = createLogger("CodexOAuth");
8592
+ const logger$i = createLogger("CodexOAuth");
8557
8593
  const ISSUER = "https://auth.openai.com";
8558
8594
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
8559
8595
  const SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
@@ -8802,7 +8838,7 @@ async function startCodexOAuth(onStatus) {
8802
8838
  try {
8803
8839
  onStatus(status, error);
8804
8840
  } catch {
8805
- 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");
8806
8842
  }
8807
8843
  };
8808
8844
  const wrappedResolve = (tokens) => {
@@ -8842,7 +8878,7 @@ async function startCodexOAuth(onStatus) {
8842
8878
  const authUrl = buildAuthorizeUrl(port, pkce, state2);
8843
8879
  safeOnStatus("waiting");
8844
8880
  openExternalAllowlisted(authUrl, { hosts: ["auth.openai.com"] }).catch((err) => {
8845
- 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);
8846
8882
  });
8847
8883
  }).catch(wrappedReject);
8848
8884
  });
@@ -8854,11 +8890,11 @@ function cancelCodexOAuth() {
8854
8890
  try {
8855
8891
  activeFlow.onStatus("idle");
8856
8892
  } catch {
8857
- 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");
8858
8894
  }
8859
8895
  activeFlow = null;
8860
8896
  }
8861
- const logger$f = createLogger("CodexProvider");
8897
+ const logger$h = createLogger("CodexProvider");
8862
8898
  const REFRESH_WINDOW_MS = 5 * 60 * 1e3;
8863
8899
  const CODEX_BACKEND_BASE_URL = "https://chatgpt.com/backend-api/codex";
8864
8900
  const CODEX_CLIENT_VERSION = "0.129.0";
@@ -8901,6 +8937,9 @@ async function createCodexFunctionCallOutput(functionCall, availableToolNames, o
8901
8937
  };
8902
8938
  }
8903
8939
  const output = await onToolCall(name, args);
8940
+ if (output === TERMINAL_TOOL_RESULT) {
8941
+ return { terminal: true };
8942
+ }
8904
8943
  return {
8905
8944
  type: "function_call_output",
8906
8945
  call_id: callId,
@@ -8920,7 +8959,7 @@ class CodexProvider {
8920
8959
  async ensureFreshTokens() {
8921
8960
  if (Date.now() < this.tokens.expiresAt - REFRESH_WINDOW_MS) return;
8922
8961
  try {
8923
- logger$f.info("Refreshing Codex access token");
8962
+ logger$h.info("Refreshing Codex access token");
8924
8963
  const fresh = await refreshAccessToken(this.tokens);
8925
8964
  this.tokens = fresh;
8926
8965
  writeStoredCodexTokens(fresh);
@@ -9068,7 +9107,7 @@ class CodexProvider {
9068
9107
  } catch (err) {
9069
9108
  if (err.name !== "AbortError") {
9070
9109
  const msg = err instanceof Error ? err.message : String(err);
9071
- logger$f.error("Codex streamQuery error:", err);
9110
+ logger$h.error("Codex streamQuery error:", err);
9072
9111
  onChunk(`
9073
9112
 
9074
9113
  [Error: ${msg}]`);
@@ -9116,14 +9155,16 @@ class CodexProvider {
9116
9155
  }
9117
9156
  currentInput = [];
9118
9157
  for (const fc of functionCalls) {
9119
- currentInput.push(
9120
- await createCodexFunctionCallOutput(
9121
- fc,
9122
- availableToolNames,
9123
- onChunk,
9124
- onToolCall
9125
- )
9158
+ const output = await createCodexFunctionCallOutput(
9159
+ fc,
9160
+ availableToolNames,
9161
+ onChunk,
9162
+ onToolCall
9126
9163
  );
9164
+ if ("terminal" in output) {
9165
+ return;
9166
+ }
9167
+ currentInput.push(output);
9127
9168
  }
9128
9169
  }
9129
9170
  if (iterationsUsed >= maxIterations) {
@@ -9134,7 +9175,7 @@ class CodexProvider {
9134
9175
  } catch (err) {
9135
9176
  if (err.name !== "AbortError") {
9136
9177
  const msg = err instanceof Error ? err.message : String(err);
9137
- logger$f.error("Codex streamAgentQuery error:", err);
9178
+ logger$h.error("Codex streamAgentQuery error:", err);
9138
9179
  onChunk(`
9139
9180
 
9140
9181
  [Error: ${msg}]`);
@@ -9326,7 +9367,7 @@ function createProvider(config) {
9326
9367
  return new OpenAICompatProvider(normalized);
9327
9368
  }
9328
9369
  const require$1 = node_module.createRequire(require("url").pathToFileURL(__filename).href);
9329
- const logger$e = createLogger("DevTrace");
9370
+ const logger$g = createLogger("DevTrace");
9330
9371
  let cachedFactory;
9331
9372
  function createNoopTraceSession() {
9332
9373
  return {
@@ -9359,7 +9400,7 @@ function loadLocalFactory() {
9359
9400
  return cachedFactory;
9360
9401
  }
9361
9402
  } catch (err) {
9362
- logger$e.warn("Failed to load local trace logger:", err);
9403
+ logger$g.warn("Failed to load local trace logger:", err);
9363
9404
  }
9364
9405
  }
9365
9406
  return cachedFactory;
@@ -13478,7 +13519,7 @@ function formatDeadLinkMessage(label, result) {
13478
13519
  const status = result.statusCode ? `HTTP ${result.statusCode}` : "dead link";
13479
13520
  return `Skipped stale link "${label}" because ${destination} returned ${status}. Try a different link or URL instead.`;
13480
13521
  }
13481
- const logger$d = createLogger("Screenshot");
13522
+ const logger$f = createLogger("Screenshot");
13482
13523
  const SCREENSHOT_RETRY_COUNT = 3;
13483
13524
  const SCREENSHOT_RETRY_BASE_DELAY_MS = 120;
13484
13525
  async function captureScreenshot(wc) {
@@ -13500,7 +13541,7 @@ async function captureScreenshot(wc) {
13500
13541
  }
13501
13542
  }
13502
13543
  } catch (err) {
13503
- logger$d.debug(
13544
+ logger$f.debug(
13504
13545
  `capturePage attempt ${attempt + 1} failed; retrying if attempts remain.`,
13505
13546
  getErrorMessage(err)
13506
13547
  );
@@ -14374,7 +14415,15 @@ function buildHuggingFaceSearchShortcut(currentUrl, rawQuery) {
14374
14415
  appliedFilters
14375
14416
  };
14376
14417
  }
14377
- 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");
14378
14427
  function getBookmarkMetadataFromArgs(args) {
14379
14428
  return normalizeBookmarkMetadata({
14380
14429
  intent: args.intent ?? args.intent,
@@ -14560,7 +14609,7 @@ async function executePageScript(wc, script, options) {
14560
14609
  return result;
14561
14610
  } catch (err) {
14562
14611
  const label = options?.label ? ` (${options.label})` : "";
14563
- logger$c.warn(`Failed to execute page script${label}:`, err);
14612
+ logger$e.warn(`Failed to execute page script${label}:`, err);
14564
14613
  return null;
14565
14614
  } finally {
14566
14615
  if (timer) {
@@ -14661,7 +14710,7 @@ Search results snapshot:
14661
14710
  ${truncated}`;
14662
14711
  }
14663
14712
  } catch (err) {
14664
- 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);
14665
14714
  }
14666
14715
  const fallback = await getPostNavSummary(wc);
14667
14716
  return fallback ? `${fallback}
@@ -14684,7 +14733,7 @@ Page snapshot after navigation:
14684
14733
  ${truncated}`;
14685
14734
  }
14686
14735
  } catch (err) {
14687
- logger$c.warn("Failed to build post-click navigation summary:", err);
14736
+ logger$e.warn("Failed to build post-click navigation summary:", err);
14688
14737
  }
14689
14738
  return "";
14690
14739
  }
@@ -15178,7 +15227,7 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15178
15227
  }
15179
15228
  }
15180
15229
  } catch (err) {
15181
- 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);
15182
15231
  }
15183
15232
  if (snapshot2.url && snapshot2.url !== wc.getURL()) {
15184
15233
  try {
@@ -15187,7 +15236,7 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15187
15236
  await waitForLoad(wc, 3e3);
15188
15237
  return;
15189
15238
  } catch (err) {
15190
- 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);
15191
15240
  }
15192
15241
  }
15193
15242
  if (snapshot2.url) {
@@ -15195,7 +15244,7 @@ async function restoreLocaleSnapshot(wc, snapshot2) {
15195
15244
  await wc.reload();
15196
15245
  await waitForLoad(wc, 3e3);
15197
15246
  } catch (err) {
15198
- logger$c.warn("Failed to restore locale via page reload:", err);
15247
+ logger$e.warn("Failed to restore locale via page reload:", err);
15199
15248
  }
15200
15249
  }
15201
15250
  }
@@ -15547,7 +15596,7 @@ ${postActivationOverlayHint}`;
15547
15596
  return `${clickText} -> ${hrefFallbackUrl} (recovered via href fallback)`;
15548
15597
  }
15549
15598
  } catch (err) {
15550
- 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);
15551
15600
  }
15552
15601
  }
15553
15602
  }
@@ -15592,7 +15641,7 @@ async function tryAutoDismissCartDialog(wc) {
15592
15641
  return result;
15593
15642
  }
15594
15643
  } catch (err) {
15595
- 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);
15596
15645
  }
15597
15646
  return null;
15598
15647
  }
@@ -17536,6 +17585,19 @@ const KNOWN_TOOLS = /* @__PURE__ */ new Set([
17536
17585
  ]);
17537
17586
  async function executeAction(name, args, ctx) {
17538
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
+ }
17539
17601
  if (!KNOWN_TOOLS.has(name)) {
17540
17602
  for (const known of KNOWN_TOOLS) {
17541
17603
  if (name.startsWith(known) && name.length > known.length) {
@@ -17862,7 +17924,7 @@ async function executeAction(name, args, ctx) {
17862
17924
  )
17863
17925
  ]);
17864
17926
  } catch (err) {
17865
- 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);
17866
17928
  content = null;
17867
17929
  }
17868
17930
  if (!content || content.content.length === 0) {
@@ -17879,12 +17941,12 @@ async function executeAction(name, args, ctx) {
17879
17941
  new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
17880
17942
  ]);
17881
17943
  } catch (err) {
17882
- 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);
17883
17945
  content = null;
17884
17946
  }
17885
17947
  }
17886
17948
  } catch (err) {
17887
- 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);
17888
17950
  }
17889
17951
  }
17890
17952
  if (content && content.content.length > 0) {
@@ -18297,7 +18359,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
18297
18359
  try {
18298
18360
  page = await extractContent(wc);
18299
18361
  } catch (err) {
18300
- logger$c.warn("Failed to extract content for suggest:", err);
18362
+ logger$e.warn("Failed to extract content for suggest:", err);
18301
18363
  return "Could not read page. Try navigate to a working URL.";
18302
18364
  }
18303
18365
  const suggestions = [];
@@ -18685,7 +18747,80 @@ WARNING: You have clicked ${clickStreakCount} elements on this page without veri
18685
18747
  }
18686
18748
  return formattedResult + await getPostActionState$1(ctx, name) + clickNavSummary + streakWarning + flowCtx;
18687
18749
  }
18688
- 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
+ }
18689
18824
  const lowerQuery = query.toLowerCase().trim();
18690
18825
  const isSummarize = lowerQuery.startsWith("summarize") || lowerQuery.startsWith("tldr") || lowerQuery === "summary";
18691
18826
  if (provider.streamAgentQuery && tabManager && activeWebContents && runtime2) {
@@ -19622,7 +19757,7 @@ Exception: ${result.exceptionDetails}`);
19622
19757
  }
19623
19758
  );
19624
19759
  }
19625
- const logger$b = createLogger("VaultShared");
19760
+ const logger$d = createLogger("VaultShared");
19626
19761
  const ALGORITHM = "aes-256-gcm";
19627
19762
  const IV_LENGTH = 12;
19628
19763
  const AUTH_TAG_LENGTH = 16;
@@ -19716,7 +19851,7 @@ function createVaultIO(vaultFilename, encrypt2, decrypt2) {
19716
19851
  cachedEntries = JSON.parse(json);
19717
19852
  return cachedEntries;
19718
19853
  } catch (err) {
19719
- logger$b.error("Failed to load vault:", err);
19854
+ logger$d.error("Failed to load vault:", err);
19720
19855
  throw new Error("Could not unlock the vault. Check OS secret storage availability.");
19721
19856
  }
19722
19857
  }
@@ -19799,7 +19934,7 @@ function createAuditLog(filename, maxEntries) {
19799
19934
  } catch {
19800
19935
  }
19801
19936
  } catch (err) {
19802
- logger$b.error("Failed to write audit log:", err);
19937
+ logger$d.error("Failed to write audit log:", err);
19803
19938
  }
19804
19939
  }
19805
19940
  function readAuditLog2(limit = 100) {
@@ -19809,7 +19944,7 @@ function createAuditLog(filename, maxEntries) {
19809
19944
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
19810
19945
  return lines.slice(-Math.min(limit, maxEntries)).map((line) => JSON.parse(line)).reverse();
19811
19946
  } catch (err) {
19812
- logger$b.error("Failed to read audit log:", err);
19947
+ logger$d.error("Failed to read audit log:", err);
19813
19948
  return [];
19814
19949
  }
19815
19950
  }
@@ -19913,7 +20048,7 @@ async function requestConsent(request) {
19913
20048
  }
19914
20049
  const AUDIT_FILENAME = "vessel-vault-audit.jsonl";
19915
20050
  const MAX_ENTRIES = 1e3;
19916
- const logger$a = createLogger("VaultAudit");
20051
+ const logger$c = createLogger("VaultAudit");
19917
20052
  function getAuditPath() {
19918
20053
  return path$1.join(electron.app.getPath("userData"), AUDIT_FILENAME);
19919
20054
  }
@@ -19927,7 +20062,7 @@ function appendAuditEntry(entry) {
19927
20062
  });
19928
20063
  fs$1.chmodSync(auditPath, 384);
19929
20064
  } catch (err) {
19930
- logger$a.error("Failed to write audit log:", err);
20065
+ logger$c.error("Failed to write audit log:", err);
19931
20066
  }
19932
20067
  }
19933
20068
  function readAuditLog$1(limit = 100) {
@@ -19937,7 +20072,7 @@ function readAuditLog$1(limit = 100) {
19937
20072
  const lines = fs$1.readFileSync(auditPath, "utf-8").split("\n").filter((l) => l.trim());
19938
20073
  return lines.slice(-Math.min(limit, MAX_ENTRIES)).map((line) => JSON.parse(line)).reverse();
19939
20074
  } catch (err) {
19940
- logger$a.error("Failed to read audit log:", err);
20075
+ logger$c.error("Failed to read audit log:", err);
19941
20076
  return [];
19942
20077
  }
19943
20078
  }
@@ -20105,7 +20240,7 @@ async function requestHumanVaultConsent(request) {
20105
20240
  }
20106
20241
  let httpServer = null;
20107
20242
  let mcpAuthToken = null;
20108
- const logger$9 = createLogger("MCP");
20243
+ const logger$b = createLogger("MCP");
20109
20244
  const MCP_AUTH_FILENAME = "mcp-auth.json";
20110
20245
  function getMcpAuthFilePath() {
20111
20246
  const configDir = process.env.VESSEL_CONFIG_DIR || path$1.join(
@@ -20142,7 +20277,7 @@ function writeMcpAuthFile(endpoint, token) {
20142
20277
  );
20143
20278
  fs$1.chmodSync(filePath2, 384);
20144
20279
  } catch (err) {
20145
- logger$9.warn("Failed to write auth file:", err);
20280
+ logger$b.warn("Failed to write auth file:", err);
20146
20281
  }
20147
20282
  }
20148
20283
  function clearMcpAuthFile() {
@@ -20168,7 +20303,7 @@ function clearMcpAuthFile() {
20168
20303
  );
20169
20304
  fs$1.chmodSync(filePath2, 384);
20170
20305
  } catch (err) {
20171
- logger$9.warn("Failed to clear auth file:", err);
20306
+ logger$b.warn("Failed to clear auth file:", err);
20172
20307
  }
20173
20308
  }
20174
20309
  function regenerateMcpAuthToken() {
@@ -20270,7 +20405,7 @@ async function getPostActionState(tabManager, name) {
20270
20405
  }
20271
20406
  }
20272
20407
  } catch (err) {
20273
- logger$9.warn("Failed to compute post-action state warning:", err);
20408
+ logger$b.warn("Failed to compute post-action state warning:", err);
20274
20409
  }
20275
20410
  return `${warning}
20276
20411
  [state: url=${wc.getURL()}, canGoBack=${tab.canGoBack()}, canGoForward=${tab.canGoForward()}, loading=${wc.isLoading()}]`;
@@ -20373,7 +20508,7 @@ async function waitForConditionMcp(wc, text, selector, timeoutMs) {
20373
20508
  }
20374
20509
  })()
20375
20510
  `).catch((err) => {
20376
- logger$9.warn("Failed to gather wait_for timeout diagnostic:", err);
20511
+ logger$b.warn("Failed to gather wait_for timeout diagnostic:", err);
20377
20512
  return null;
20378
20513
  });
20379
20514
  if (typeof diagnostic === "string" && diagnostic.trim()) {
@@ -20460,7 +20595,7 @@ function registerTools(server, tabManager, runtime2) {
20460
20595
  const page = await extractContent(wc);
20461
20596
  pageType = detectPageType(page);
20462
20597
  } catch (err) {
20463
- 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);
20464
20599
  }
20465
20600
  }
20466
20601
  const scored = TOOL_DEFINITIONS.map((def) => {
@@ -21858,7 +21993,7 @@ ${JSON.stringify(otherHighlights, null, 2)}`
21858
21993
  void 0,
21859
21994
  h.color
21860
21995
  ).catch(
21861
- (err) => logger$9.warn("Failed to restore highlight after removal:", err)
21996
+ (err) => logger$b.warn("Failed to restore highlight after removal:", err)
21862
21997
  );
21863
21998
  }
21864
21999
  }
@@ -22706,7 +22841,7 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
22706
22841
  try {
22707
22842
  page = await extractContent(wc);
22708
22843
  } catch (err) {
22709
- logger$9.warn("Failed to extract page while generating suggestions:", err);
22844
+ logger$b.warn("Failed to extract page while generating suggestions:", err);
22710
22845
  return asTextResponse(
22711
22846
  "Could not read page. Try navigate to a working URL."
22712
22847
  );
@@ -23315,7 +23450,7 @@ ${JSON.stringify(tableJson, null, 2)}`;
23315
23450
  try {
23316
23451
  targetDomain = new URL(tab.state.url).hostname;
23317
23452
  } catch (err) {
23318
- 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);
23319
23454
  return asErrorTextResponse("Could not parse active tab URL");
23320
23455
  }
23321
23456
  }
@@ -23381,7 +23516,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23381
23516
  try {
23382
23517
  hostname = new URL(tab.state.url).hostname;
23383
23518
  } catch (err) {
23384
- 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);
23385
23520
  return asErrorTextResponse("Could not parse active tab URL");
23386
23521
  }
23387
23522
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -23475,7 +23610,7 @@ Use vault_login to fill the login form. Credentials are filled directly — you
23475
23610
  try {
23476
23611
  hostname = new URL(tab.state.url).hostname;
23477
23612
  } catch (err) {
23478
- 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);
23479
23614
  return asErrorTextResponse("Could not parse active tab URL");
23480
23615
  }
23481
23616
  const matches = findEntriesForDomain(`https://${hostname}`);
@@ -23815,7 +23950,7 @@ function startMcpServer(tabManager, runtime2, port) {
23815
23950
  await mcpServer.connect(transport);
23816
23951
  await transport.handleRequest(req, res);
23817
23952
  } catch (error) {
23818
- logger$9.error("Error handling request:", error);
23953
+ logger$b.error("Error handling request:", error);
23819
23954
  if (!res.headersSent) {
23820
23955
  res.writeHead(500, { "Content-Type": "application/json" });
23821
23956
  res.end(
@@ -23834,7 +23969,7 @@ function startMcpServer(tabManager, runtime2, port) {
23834
23969
  };
23835
23970
  server.once("error", (error) => {
23836
23971
  const message = error.code === "EADDRINUSE" ? `Port ${port} is already in use. MCP server not started.` : error.message;
23837
- logger$9.error("Server error:", error);
23972
+ logger$b.error("Server error:", error);
23838
23973
  clearMcpAuthFile();
23839
23974
  setMcpHealth({
23840
23975
  configuredPort: port,
@@ -23866,7 +24001,7 @@ function startMcpServer(tabManager, runtime2, port) {
23866
24001
  message: `MCP server listening on ${endpoint}.`
23867
24002
  });
23868
24003
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
23869
- logger$9.info(`Server listening on ${endpoint} (auth enabled)`);
24004
+ logger$b.info(`Server listening on ${endpoint} (auth enabled)`);
23870
24005
  }
23871
24006
  if (mcpAuthToken) {
23872
24007
  writeMcpAuthFile(endpoint, mcpAuthToken);
@@ -23905,7 +24040,7 @@ function stopMcpServer() {
23905
24040
  message: "MCP server is stopped."
23906
24041
  });
23907
24042
  if (process.env.VESSEL_DEBUG_MCP === "1" || process.env.VESSEL_DEBUG_MCP === "true") {
23908
- logger$9.info("Server stopped");
24043
+ logger$b.info("Server stopped");
23909
24044
  }
23910
24045
  resolve();
23911
24046
  });
@@ -23926,7 +24061,7 @@ const KIT_ID_UNSAFE_CHAR_PATTERN = /[\/\\\0]/;
23926
24061
  function isSafeAutomationKitId(id) {
23927
24062
  return id.length > 0 && !KIT_ID_UNSAFE_CHAR_PATTERN.test(id);
23928
24063
  }
23929
- const logger$8 = createLogger("KitRegistry");
24064
+ const logger$a = createLogger("KitRegistry");
23930
24065
  function getUserKitsDir() {
23931
24066
  return path$1.join(electron.app.getPath("userData"), "kits");
23932
24067
  }
@@ -23964,10 +24099,10 @@ function getInstalledKits() {
23964
24099
  if (isValidKit(parsed)) {
23965
24100
  kits.push(parsed);
23966
24101
  } else {
23967
- logger$8.warn(`Skipping invalid kit file: ${file}`);
24102
+ logger$a.warn(`Skipping invalid kit file: ${file}`);
23968
24103
  }
23969
24104
  } catch (err) {
23970
- logger$8.warn(`Failed to read kit file: ${file}`, err);
24105
+ logger$a.warn(`Failed to read kit file: ${file}`, err);
23971
24106
  }
23972
24107
  }
23973
24108
  return kits;
@@ -24076,7 +24211,7 @@ function getActiveTabInfo(tabManager) {
24076
24211
  if (wc.isDestroyed()) return null;
24077
24212
  return { tab, wc };
24078
24213
  }
24079
- const logger$7 = createLogger("Scheduler");
24214
+ const logger$9 = createLogger("Scheduler");
24080
24215
  let jobs = [];
24081
24216
  let removeIdleListener = null;
24082
24217
  let broadcastFn = null;
@@ -24107,7 +24242,7 @@ function saveJobs() {
24107
24242
  });
24108
24243
  fs$1.chmodSync(jobsPath, 384);
24109
24244
  } catch (err) {
24110
- logger$7.warn("Failed to save jobs:", err);
24245
+ logger$9.warn("Failed to save jobs:", err);
24111
24246
  }
24112
24247
  }
24113
24248
  function normalizeJob(job, now = /* @__PURE__ */ new Date()) {
@@ -24229,7 +24364,7 @@ async function fireJob(job, windowState, runtime2) {
24229
24364
  };
24230
24365
  startActivity();
24231
24366
  if (!settings2.chatProvider) {
24232
- logger$7.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
24367
+ logger$9.warn(`Job "${job.kitName}" skipped — no chat provider configured`);
24233
24368
  appendActivity(
24234
24369
  "Chat provider not configured. Open Settings (Ctrl+,) to choose a provider."
24235
24370
  );
@@ -24237,7 +24372,7 @@ async function fireJob(job, windowState, runtime2) {
24237
24372
  return;
24238
24373
  }
24239
24374
  if (process.env.VESSEL_DEBUG_SCHEDULER === "1" || process.env.VESSEL_DEBUG_SCHEDULER === "true") {
24240
- logger$7.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
24375
+ logger$9.info(`Firing scheduled job: ${job.kitName} (${job.id})`);
24241
24376
  }
24242
24377
  try {
24243
24378
  const provider = createProvider(settings2.chatProvider);
@@ -24290,7 +24425,7 @@ function tick(windowState, runtime2) {
24290
24425
  saveJobs();
24291
24426
  broadcastFn?.(Channels.SCHEDULE_JOBS_UPDATE, jobs);
24292
24427
  void fireJob(job, windowState, runtime2).catch((err) => {
24293
- logger$7.warn("Unexpected error firing job:", err);
24428
+ logger$9.warn("Unexpected error firing job:", err);
24294
24429
  }).finally(fireNext);
24295
24430
  };
24296
24431
  fireNext();
@@ -24812,6 +24947,777 @@ function registerPageDiffHandlers(windowState, sendToRendererViews) {
24812
24947
  schedulePageSnapshotCapture(wc, sendToRendererViews);
24813
24948
  });
24814
24949
  }
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();
25012
+ });
25013
+ electron.ipcMain.handle(
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
+ }
24815
25721
  function registerVaultHandlers() {
24816
25722
  electron.ipcMain.handle(Channels.VAULT_LIST, (event) => {
24817
25723
  assertTrustedIpcSender(event);
@@ -26676,6 +27582,18 @@ function registerIpcHandlers(windowState, runtime2) {
26676
27582
  const requireTrusted = (event) => {
26677
27583
  assertTrustedIpcSender(event);
26678
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
+ };
26679
27597
  electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, (event) => {
26680
27598
  requireTrusted(event);
26681
27599
  createPrivateWindow();
@@ -26944,7 +27862,8 @@ function registerIpcHandlers(windowState, runtime2) {
26944
27862
  () => sendToRendererViews(Channels.AI_STREAM_END, "completed"),
26945
27863
  tabManager,
26946
27864
  runtime2,
26947
- history
27865
+ history,
27866
+ researchOrchestrator
26948
27867
  );
26949
27868
  } catch (err) {
26950
27869
  const msg = err instanceof Error ? err.message : "Unknown error";
@@ -27111,6 +28030,12 @@ function registerIpcHandlers(windowState, runtime2) {
27111
28030
  await stopMcpServer();
27112
28031
  await startMcpServer(tabManager, runtime2, updatedSettings.mcpPort);
27113
28032
  }
28033
+ if (key2 === "chatProvider" && researchOrchestrator) {
28034
+ try {
28035
+ researchOrchestrator.setProvider(createProvider(value));
28036
+ } catch {
28037
+ }
28038
+ }
27114
28039
  const rendererSettings = getRendererSettings();
27115
28040
  sendToRendererViews(Channels.SETTINGS_UPDATE, rendererSettings);
27116
28041
  return rendererSettings;
@@ -27318,6 +28243,7 @@ function registerIpcHandlers(windowState, runtime2) {
27318
28243
  registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
27319
28244
  registerAutofillHandlers(windowState);
27320
28245
  registerPageDiffHandlers(windowState, sendToRendererViews);
28246
+ registerResearchHandlers(() => getResearchOrchestrator());
27321
28247
  electron.ipcMain.handle(Channels.CLEAR_BROWSING_DATA, async (event, options) => {
27322
28248
  requireTrusted(event);
27323
28249
  const { cache, cookies, history, localStorage: clearLs, timeRange } = options;
@@ -27764,6 +28690,10 @@ ${lines.join("\n")}
27764
28690
  }
27765
28691
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
27766
28692
  const PERSIST_DEBOUNCE_MS = 500;
28693
+ const INTERRUPTED_ACTION_STATUSES = /* @__PURE__ */ new Set([
28694
+ "running",
28695
+ "waiting-approval"
28696
+ ]);
27767
28697
  const logger$3 = createLogger("Runtime");
27768
28698
  function clone(value) {
27769
28699
  return JSON.parse(JSON.stringify(value));
@@ -27786,6 +28716,15 @@ function getRuntimeStatePath() {
27786
28716
  return path$1.join(electron.app.getPath("userData"), "vessel-agent-runtime.json");
27787
28717
  }
27788
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
+ ) : [];
27789
28728
  return {
27790
28729
  session: persisted?.session ?? null,
27791
28730
  supervisor: {
@@ -27794,7 +28733,7 @@ function sanitizePersistence(persisted) {
27794
28733
  pendingApprovals: [],
27795
28734
  lastError: persisted?.supervisor?.lastError
27796
28735
  },
27797
- actions: Array.isArray(persisted?.actions) ? persisted.actions.slice(-120) : [],
28736
+ actions,
27798
28737
  checkpoints: Array.isArray(persisted?.checkpoints) ? persisted.checkpoints.slice(-20) : [],
27799
28738
  transcript: [],
27800
28739
  mcpStatus: "stopped",