@quanta-intellect/vessel-browser 0.1.99 → 0.1.101

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
@@ -65,6 +65,7 @@ const defaults = {
65
65
  chatProvider: null,
66
66
  maxToolIterations: 200,
67
67
  domainPolicy: { allowedDomains: [], blockedDomains: [] },
68
+ sourceDoNotAllowList: [],
68
69
  downloadPath: "",
69
70
  telemetryEnabled: true,
70
71
  defaultSearchEngine: "duckduckgo",
@@ -231,6 +232,11 @@ function sanitizePort(value) {
231
232
  function sanitizeReasoningEffortLevel$1(value) {
232
233
  return value === "low" || value === "medium" || value === "high" || value === "max" || value === "off" ? value : "off";
233
234
  }
235
+ function sanitizeStringList(value) {
236
+ return Array.isArray(value) ? Array.from(
237
+ new Set(value.map((item) => String(item).trim()).filter(Boolean))
238
+ ) : [];
239
+ }
234
240
  function sanitizeChatProvider(provider) {
235
241
  return provider ? {
236
242
  ...provider,
@@ -252,6 +258,9 @@ function loadSettings() {
252
258
  mergeChatProviderSecret(parsed.chatProvider ?? null)
253
259
  ),
254
260
  mcpPort: sanitizePort(parsed.mcpPort ?? defaults.mcpPort),
261
+ sourceDoNotAllowList: sanitizeStringList(
262
+ parsed.sourceDoNotAllowList ?? defaults.sourceDoNotAllowList
263
+ ),
255
264
  agentTranscriptMode: parsed.agentTranscriptMode === "off" || parsed.agentTranscriptMode === "summary" || parsed.agentTranscriptMode === "full" ? parsed.agentTranscriptMode : parsed.showAgentTranscript === false ? "off" : defaults.agentTranscriptMode
256
265
  };
257
266
  } catch (error) {
@@ -296,6 +305,8 @@ function setSetting(key2, value) {
296
305
  loadSettings();
297
306
  if (key2 === "mcpPort") {
298
307
  settings.mcpPort = sanitizePort(value);
308
+ } else if (key2 === "sourceDoNotAllowList") {
309
+ settings.sourceDoNotAllowList = sanitizeStringList(value);
299
310
  } else if (key2 === "chatProvider") {
300
311
  const nextProvider = value;
301
312
  if (!nextProvider) {
@@ -3564,6 +3575,7 @@ const Channels = {
3564
3575
  RESEARCH_SET_MODE: "research:set-mode",
3565
3576
  RESEARCH_SET_TRACES: "research:set-traces",
3566
3577
  RESEARCH_CANCEL: "research:cancel",
3578
+ RESEARCH_STOP_AND_SYNTHESIZE: "research:stop-and-synthesize",
3567
3579
  RESEARCH_EXPORT_REPORT: "research:export-report",
3568
3580
  // Codex OAuth
3569
3581
  CODEX_START_AUTH: "codex:start-auth",
@@ -25086,6 +25098,9 @@ function registerResearchHandlers(getOrchestrator) {
25086
25098
  electron.ipcMain.handle(Channels.RESEARCH_CANCEL, () => {
25087
25099
  getOrchestrator().cancel();
25088
25100
  });
25101
+ electron.ipcMain.handle(Channels.RESEARCH_STOP_AND_SYNTHESIZE, () => {
25102
+ getOrchestrator().stopAndSynthesizeCurrentFindings();
25103
+ });
25089
25104
  electron.ipcMain.handle(Channels.RESEARCH_EXPORT_REPORT, async () => {
25090
25105
  try {
25091
25106
  if (isToolGated("research_export_report")) {
@@ -25213,6 +25228,96 @@ const MAX_THREADS = 5;
25213
25228
  function clone$1(value) {
25214
25229
  return structuredClone(value);
25215
25230
  }
25231
+ function normalizeSourceDomain(value) {
25232
+ const trimmed = value.trim().toLowerCase();
25233
+ if (!trimmed) return "";
25234
+ try {
25235
+ return new URL(
25236
+ trimmed.includes("://") ? trimmed : `https://${trimmed}`
25237
+ ).hostname.replace(/^www\./, "");
25238
+ } catch {
25239
+ return trimmed.replace(/^www\./, "");
25240
+ }
25241
+ }
25242
+ function mergeBlockedSourceDomains(thread) {
25243
+ const globalBlocked = loadSettings().sourceDoNotAllowList.map(normalizeSourceDomain).filter(Boolean);
25244
+ if (globalBlocked.length === 0) return thread;
25245
+ const blockedDomains = Array.from(
25246
+ /* @__PURE__ */ new Set([
25247
+ ...thread.blockedDomains.map(normalizeSourceDomain).filter(Boolean),
25248
+ ...globalBlocked
25249
+ ])
25250
+ );
25251
+ return {
25252
+ ...thread,
25253
+ blockedDomains
25254
+ };
25255
+ }
25256
+ function matchesSourceDomain(hostname, domain) {
25257
+ return hostname === domain || hostname.endsWith(`.${domain}`);
25258
+ }
25259
+ function getBlockedSourceNavigation(url, blockedDomains) {
25260
+ if (typeof url !== "string" || blockedDomains.length === 0) return null;
25261
+ try {
25262
+ const hostname = new URL(url).hostname.toLowerCase().replace(/^www\./, "");
25263
+ return blockedDomains.find(
25264
+ (domain) => matchesSourceDomain(hostname, normalizeSourceDomain(domain))
25265
+ ) ?? null;
25266
+ } catch {
25267
+ return null;
25268
+ }
25269
+ }
25270
+ function buildFallbackSourceIndex(findings) {
25271
+ const seen = /* @__PURE__ */ new Set();
25272
+ const sources = [];
25273
+ for (const claim of findings.flatMap((finding) => finding.claims)) {
25274
+ if (!claim.sourceUrl || seen.has(claim.sourceUrl)) continue;
25275
+ seen.add(claim.sourceUrl);
25276
+ sources.push({
25277
+ index: sources.length + 1,
25278
+ url: claim.sourceUrl,
25279
+ title: claim.sourceTitle || claim.sourceUrl,
25280
+ accessedAt: claim.extractedAt,
25281
+ supportingQuote: claim.extractedQuote
25282
+ });
25283
+ }
25284
+ return sources;
25285
+ }
25286
+ function citationForClaim(claim, sourceIndex) {
25287
+ const index = sourceIndex.find((source) => source.url === claim.sourceUrl)?.index ?? 0;
25288
+ return index > 0 ? `[${index}]` : "";
25289
+ }
25290
+ function buildFallbackFindingsByThread(findings, sourceIndex = buildFallbackSourceIndex(findings)) {
25291
+ return findings.map((finding) => {
25292
+ const claimLines = finding.claims.map((claim) => {
25293
+ const citation = citationForClaim(claim, sourceIndex);
25294
+ return citation ? `${claim.claim} ${citation}` : claim.claim;
25295
+ });
25296
+ return {
25297
+ threadLabel: finding.threadLabel,
25298
+ content: claimLines.length > 0 ? claimLines.join("\n\n") : `No citeable claims were extracted for this thread. ${finding.executionSummary}`
25299
+ };
25300
+ });
25301
+ }
25302
+ function buildFallbackReport(objectives, findings, reason) {
25303
+ const sourceIndex = buildFallbackSourceIndex(findings);
25304
+ const findingsByThread = buildFallbackFindingsByThread(findings, sourceIndex);
25305
+ const claimCount = findings.reduce(
25306
+ (sum, finding) => sum + finding.claims.length,
25307
+ 0
25308
+ );
25309
+ const executiveSummary = claimCount > 0 ? `The model's final synthesis response could not be parsed, so Vessel generated this sourced fallback from ${claimCount} extracted claim${claimCount === 1 ? "" : "s"} across ${sourceIndex.length} source${sourceIndex.length === 1 ? "" : "s"}.` : `The model's final synthesis response could not be parsed, and no citeable claims were extracted from the research threads.`;
25310
+ return {
25311
+ title: objectives.researchQuestion,
25312
+ executiveSummary,
25313
+ findingsByThread,
25314
+ contradictions: [],
25315
+ gaps: [`Final synthesis JSON could not be parsed: ${reason}`],
25316
+ sourceIndex,
25317
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25318
+ objectives
25319
+ };
25320
+ }
25216
25321
  class ResearchOrchestrator {
25217
25322
  constructor(provider, tabManager, runtime2) {
25218
25323
  this.provider = provider;
@@ -25225,6 +25330,8 @@ class ResearchOrchestrator {
25225
25330
  runtime;
25226
25331
  state;
25227
25332
  updateListener = null;
25333
+ stopRequested = false;
25334
+ synthesizeAfterStop = false;
25228
25335
  // ── state access ──────────────────────────────────────────────
25229
25336
  initialState() {
25230
25337
  return {
@@ -25233,6 +25340,7 @@ class ResearchOrchestrator {
25233
25340
  includeTraces: false,
25234
25341
  objectives: null,
25235
25342
  threads: [],
25343
+ threadProgress: [],
25236
25344
  threadFindings: [],
25237
25345
  report: null,
25238
25346
  subAgentTraces: [],
@@ -25265,9 +25373,30 @@ class ResearchOrchestrator {
25265
25373
  this.emit();
25266
25374
  }
25267
25375
  cancel() {
25376
+ this.stopRequested = true;
25377
+ this.synthesizeAfterStop = false;
25378
+ this.provider?.cancel();
25268
25379
  this.state = this.initialState();
25269
25380
  this.emit();
25270
25381
  }
25382
+ stopAndSynthesizeCurrentFindings() {
25383
+ if (this.state.phase !== "executing") {
25384
+ logger$7.warn("Not executing, ignoring stopAndSynthesizeCurrentFindings");
25385
+ return;
25386
+ }
25387
+ this.stopRequested = true;
25388
+ this.synthesizeAfterStop = true;
25389
+ this.state.threadProgress = this.state.threadProgress.map(
25390
+ (progress) => progress.status === "completed" || progress.status === "failed" ? progress : {
25391
+ ...progress,
25392
+ status: "stopping",
25393
+ message: "Stopping and preparing to synthesize current findings",
25394
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
25395
+ }
25396
+ );
25397
+ this.emit();
25398
+ this.provider?.cancel();
25399
+ }
25271
25400
  /**
25272
25401
  * Swap the AI provider used by this orchestrator.
25273
25402
  * Safe to call while research is in progress — running sub-agents
@@ -25313,8 +25442,12 @@ class ResearchOrchestrator {
25313
25442
  logger$7.warn("Not in planning phase, ignoring setObjectives");
25314
25443
  return;
25315
25444
  }
25316
- this.state.objectives = objectives;
25317
- this.state.threads = objectives.threads.slice(0, MAX_THREADS);
25445
+ const threads = objectives.threads.slice(0, MAX_THREADS).map(mergeBlockedSourceDomains);
25446
+ this.state.objectives = {
25447
+ ...objectives,
25448
+ threads
25449
+ };
25450
+ this.state.threads = threads;
25318
25451
  this.setPhase("awaiting_approval");
25319
25452
  }
25320
25453
  /**
@@ -25383,8 +25516,28 @@ class ResearchOrchestrator {
25383
25516
  }
25384
25517
  if (mode) this.state.supervisionMode = mode;
25385
25518
  if (includeTraces !== void 0) this.state.includeTraces = includeTraces;
25519
+ this.stopRequested = false;
25520
+ this.synthesizeAfterStop = false;
25521
+ this.state.threadFindings = [];
25522
+ this.state.threadProgress = this.state.threads.map((thread) => ({
25523
+ threadLabel: thread.label,
25524
+ status: "queued",
25525
+ message: "Queued",
25526
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
25527
+ }));
25386
25528
  this.setPhase("executing");
25387
25529
  }
25530
+ updateThreadProgress(threadLabel, status, message) {
25531
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
25532
+ const existingIndex = this.state.threadProgress.findIndex(
25533
+ (progress) => progress.threadLabel === threadLabel
25534
+ );
25535
+ const next = { threadLabel, status, message, updatedAt };
25536
+ this.state.threadProgress = existingIndex >= 0 ? this.state.threadProgress.map(
25537
+ (progress, index) => index === existingIndex ? next : progress
25538
+ ) : [...this.state.threadProgress, next];
25539
+ this.emit();
25540
+ }
25388
25541
  // ── phase: executing → synthesizing ────────────────────────────
25389
25542
  async executeSubAgents() {
25390
25543
  if (this.state.phase !== "executing" || !this.state.objectives) return;
@@ -25404,8 +25557,20 @@ class ResearchOrchestrator {
25404
25557
  });
25405
25558
  })
25406
25559
  );
25560
+ const shouldSynthesize = this.synthesizeAfterStop;
25407
25561
  if (this.state.phase !== "executing") return;
25408
25562
  this.state.threadFindings = results.filter((f) => f !== null);
25563
+ this.stopRequested = false;
25564
+ this.synthesizeAfterStop = false;
25565
+ if (!shouldSynthesize) {
25566
+ for (const finding of this.state.threadFindings) {
25567
+ this.updateThreadProgress(
25568
+ finding.threadLabel,
25569
+ finding.claims.length > 0 ? "completed" : "failed",
25570
+ finding.claims.length > 0 ? `${finding.claims.length} claim${finding.claims.length === 1 ? "" : "s"} extracted` : "No citeable claims extracted"
25571
+ );
25572
+ }
25573
+ }
25409
25574
  this.setPhase("synthesizing");
25410
25575
  try {
25411
25576
  await this.synthesizeReport();
@@ -25426,6 +25591,7 @@ class ResearchOrchestrator {
25426
25591
  };
25427
25592
  const tabId = this.tabManager.createTab();
25428
25593
  let sourcesConsumed = 0;
25594
+ this.updateThreadProgress(thread.label, "running", "Researching sources");
25429
25595
  if (tabId) this.tabManager.switchTab(tabId);
25430
25596
  const discardedSources = [];
25431
25597
  let transcript = "";
@@ -25454,10 +25620,32 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25454
25620
  },
25455
25621
  async (name, args) => {
25456
25622
  const t0 = Date.now();
25457
- if (this.state.phase !== "executing") {
25623
+ if (this.state.phase !== "executing" || this.stopRequested) {
25458
25624
  const msg = "Research cancelled — stopping.";
25459
25625
  return msg;
25460
25626
  }
25627
+ if (name === "navigate") {
25628
+ const blockedDomain = getBlockedSourceNavigation(
25629
+ args.url,
25630
+ thread.blockedDomains
25631
+ );
25632
+ if (blockedDomain) {
25633
+ const msg = `Source skipped: ${String(args.url)} matches the Research Desk source do-not-allow list (${blockedDomain}). Choose a different source.`;
25634
+ discardedSources.push({
25635
+ url: String(args.url || ""),
25636
+ title: String(args.url || "excluded source"),
25637
+ reason: msg
25638
+ });
25639
+ trace.toolCalls.push({
25640
+ tool: name,
25641
+ args,
25642
+ result: msg,
25643
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25644
+ durationMs: 0
25645
+ });
25646
+ return msg;
25647
+ }
25648
+ }
25461
25649
  if (name === "navigate" || name === "search") {
25462
25650
  sourcesConsumed++;
25463
25651
  if (sourcesConsumed > thread.sourceBudget) {
@@ -25513,6 +25701,9 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25513
25701
  message: String(err),
25514
25702
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
25515
25703
  });
25704
+ if (this.state.phase === "executing") {
25705
+ this.updateThreadProgress(thread.label, "stopping", "Stopping thread");
25706
+ }
25516
25707
  } finally {
25517
25708
  trace.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
25518
25709
  if (tabId) {
@@ -25537,6 +25728,13 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25537
25728
  const pagesVisited = trace.toolCalls.filter(
25538
25729
  (t) => ["navigate", "read_page", "search"].includes(t.tool)
25539
25730
  ).length;
25731
+ if (this.state.phase === "executing") {
25732
+ this.updateThreadProgress(
25733
+ thread.label,
25734
+ claims.length > 0 ? "completed" : this.stopRequested ? "stopping" : "failed",
25735
+ claims.length > 0 ? `${claims.length} claim${claims.length === 1 ? "" : "s"} extracted` : this.stopRequested ? "Stopped before citeable claims were extracted" : "No citeable claims extracted"
25736
+ );
25737
+ }
25540
25738
  return {
25541
25739
  threadLabel: thread.label,
25542
25740
  threadQuestion: thread.question,
@@ -25633,7 +25831,7 @@ ${transcript.slice(0, 32e3)}`;
25633
25831
  () => {
25634
25832
  }
25635
25833
  );
25636
- const report = this.parseReportFromJson(response, objectives);
25834
+ const report = this.parseReportFromJson(response, objectives, findings);
25637
25835
  this.setReport(report);
25638
25836
  this.setPhase("delivered");
25639
25837
  return report;
@@ -25642,7 +25840,7 @@ ${transcript.slice(0, 32e3)}`;
25642
25840
  * Parse the LLM's JSON synthesis response into a structured ResearchReport.
25643
25841
  * Handles both bare JSON and JSON wrapped in markdown fences.
25644
25842
  */
25645
- parseReportFromJson(text, objectives) {
25843
+ parseReportFromJson(text, objectives, findings) {
25646
25844
  let json = text.trim();
25647
25845
  const fenceMatch = json.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
25648
25846
  if (fenceMatch) json = fenceMatch[1].trim();
@@ -25650,16 +25848,27 @@ ${transcript.slice(0, 32e3)}`;
25650
25848
  if (objMatch) json = objMatch[0];
25651
25849
  try {
25652
25850
  const parsed = JSON.parse(json);
25851
+ const sourceIndex = Array.isArray(parsed.sourceIndex) ? parsed.sourceIndex.map((s) => {
25852
+ const obj = s;
25853
+ return {
25854
+ index: typeof obj.index === "number" ? obj.index : parseInt(String(obj.index), 10) || 0,
25855
+ url: String(obj.url || "").trim(),
25856
+ title: String(obj.title || "").trim(),
25857
+ accessedAt: String(obj.accessedAt || "").trim(),
25858
+ supportingQuote: String(obj.supportingQuote || "").trim()
25859
+ };
25860
+ }).filter((s) => s.url && s.title) : [];
25861
+ const findingsByThread = Array.isArray(parsed.findingsByThread) ? parsed.findingsByThread.map((s) => {
25862
+ const obj = s;
25863
+ return {
25864
+ threadLabel: String(obj.threadLabel || "").trim(),
25865
+ content: String(obj.content || "").trim()
25866
+ };
25867
+ }) : [];
25653
25868
  return {
25654
25869
  title: String(parsed.title || objectives.researchQuestion).trim(),
25655
25870
  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
- }) : [],
25871
+ findingsByThread: findingsByThread.length > 0 ? findingsByThread : buildFallbackFindingsByThread(findings),
25663
25872
  contradictions: Array.isArray(parsed.contradictions) ? parsed.contradictions.map((c) => {
25664
25873
  const obj = c;
25665
25874
  const sourceA = obj.sourceA ?? {};
@@ -25680,31 +25889,13 @@ ${transcript.slice(0, 32e3)}`;
25680
25889
  (c) => c.claim && c.sourceA.url && c.sourceB.url && c.resolution
25681
25890
  ) : [],
25682
25891
  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) : [],
25892
+ sourceIndex: sourceIndex.length > 0 ? sourceIndex : buildFallbackSourceIndex(findings),
25693
25893
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25694
25894
  objectives
25695
25895
  };
25696
25896
  } 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
- };
25897
+ logger$7.warn("Failed to parse synthesis JSON, using sourced fallback report", err);
25898
+ return buildFallbackReport(objectives, findings, String(err));
25708
25899
  }
25709
25900
  }
25710
25901
  // ── report management ──────────────────────────────────────────
@@ -204,6 +204,7 @@ const Channels = {
204
204
  RESEARCH_SET_MODE: "research:set-mode",
205
205
  RESEARCH_SET_TRACES: "research:set-traces",
206
206
  RESEARCH_CANCEL: "research:cancel",
207
+ RESEARCH_STOP_AND_SYNTHESIZE: "research:stop-and-synthesize",
207
208
  RESEARCH_EXPORT_REPORT: "research:export-report",
208
209
  // Codex OAuth
209
210
  CODEX_START_AUTH: "codex:start-auth",
@@ -332,6 +333,7 @@ const api = {
332
333
  setMode: (mode) => electron.ipcRenderer.invoke(Channels.RESEARCH_SET_MODE, mode),
333
334
  setTraces: (include) => electron.ipcRenderer.invoke(Channels.RESEARCH_SET_TRACES, include),
334
335
  cancel: () => electron.ipcRenderer.invoke(Channels.RESEARCH_CANCEL),
336
+ stopAndSynthesize: () => electron.ipcRenderer.invoke(Channels.RESEARCH_STOP_AND_SYNTHESIZE),
335
337
  exportReport: () => electron.ipcRenderer.invoke(Channels.RESEARCH_EXPORT_REPORT)
336
338
  },
337
339
  content: {