@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/README.md +6 -4
- package/out/main/index.js +1040 -101
- package/out/preload/index.js +34 -0
- package/out/renderer/assets/{index-BP-7cF0c.js → index-BLDCpaUR.js} +1196 -522
- package/out/renderer/assets/{index-Bf1d0lUq.css → index-CzIBoLK8.css} +288 -0
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
897
|
+
logger$o.warn("Failed to retrieve page source:", err);
|
|
897
898
|
return;
|
|
898
899
|
}
|
|
899
900
|
const escaped = html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -1021,7 +1022,7 @@ class Tab {
|
|
|
1021
1022
|
document.addEventListener('mouseup', window.__vesselHighlightHandler);
|
|
1022
1023
|
}
|
|
1023
1024
|
})()
|
|
1024
|
-
`).catch((err) => logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
3301
|
+
(err) => logger$m.warn("Failed to update highlight color:", err)
|
|
3301
3302
|
);
|
|
3302
3303
|
});
|
|
3303
3304
|
}
|
|
3304
3305
|
} catch (err) {
|
|
3305
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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$
|
|
8039
|
+
logger$j.info(`[agent-debug] ${JSON.stringify(payload)}`);
|
|
8007
8040
|
} catch (err) {
|
|
8008
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
24102
|
+
logger$a.warn(`Skipping invalid kit file: ${file}`);
|
|
23968
24103
|
}
|
|
23969
24104
|
} catch (err) {
|
|
23970
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
28736
|
+
actions,
|
|
27798
28737
|
checkpoints: Array.isArray(persisted?.checkpoints) ? persisted.checkpoints.slice(-20) : [],
|
|
27799
28738
|
transcript: [],
|
|
27800
28739
|
mcpStatus: "stopped",
|