@quanta-intellect/vessel-browser 0.1.99 → 0.1.103

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",
@@ -4789,7 +4801,14 @@ const TELEMETRY_PROPERTY_ALLOWLIST = {
4789
4801
  bookmark_action: /* @__PURE__ */ new Set(["action"]),
4790
4802
  vault_action: /* @__PURE__ */ new Set(["action"]),
4791
4803
  extraction_failed: /* @__PURE__ */ new Set(["reason"]),
4792
- premium_funnel: /* @__PURE__ */ new Set(["step", "status", "reason"])
4804
+ premium_funnel: /* @__PURE__ */ new Set([
4805
+ "step",
4806
+ "status",
4807
+ "reason",
4808
+ "source",
4809
+ "previous_status",
4810
+ "new_status"
4811
+ ])
4793
4812
  };
4794
4813
  function getDeviceIdPath() {
4795
4814
  return path.join(electron.app.getPath("userData"), ".vessel-device-id");
@@ -25086,6 +25105,9 @@ function registerResearchHandlers(getOrchestrator) {
25086
25105
  electron.ipcMain.handle(Channels.RESEARCH_CANCEL, () => {
25087
25106
  getOrchestrator().cancel();
25088
25107
  });
25108
+ electron.ipcMain.handle(Channels.RESEARCH_STOP_AND_SYNTHESIZE, () => {
25109
+ getOrchestrator().stopAndSynthesizeCurrentFindings();
25110
+ });
25089
25111
  electron.ipcMain.handle(Channels.RESEARCH_EXPORT_REPORT, async () => {
25090
25112
  try {
25091
25113
  if (isToolGated("research_export_report")) {
@@ -25213,6 +25235,96 @@ const MAX_THREADS = 5;
25213
25235
  function clone$1(value) {
25214
25236
  return structuredClone(value);
25215
25237
  }
25238
+ function normalizeSourceDomain(value) {
25239
+ const trimmed = value.trim().toLowerCase();
25240
+ if (!trimmed) return "";
25241
+ try {
25242
+ return new URL(
25243
+ trimmed.includes("://") ? trimmed : `https://${trimmed}`
25244
+ ).hostname.replace(/^www\./, "");
25245
+ } catch {
25246
+ return trimmed.replace(/^www\./, "");
25247
+ }
25248
+ }
25249
+ function mergeBlockedSourceDomains(thread) {
25250
+ const globalBlocked = loadSettings().sourceDoNotAllowList.map(normalizeSourceDomain).filter(Boolean);
25251
+ if (globalBlocked.length === 0) return thread;
25252
+ const blockedDomains = Array.from(
25253
+ /* @__PURE__ */ new Set([
25254
+ ...thread.blockedDomains.map(normalizeSourceDomain).filter(Boolean),
25255
+ ...globalBlocked
25256
+ ])
25257
+ );
25258
+ return {
25259
+ ...thread,
25260
+ blockedDomains
25261
+ };
25262
+ }
25263
+ function matchesSourceDomain(hostname, domain) {
25264
+ return hostname === domain || hostname.endsWith(`.${domain}`);
25265
+ }
25266
+ function getBlockedSourceNavigation(url, blockedDomains) {
25267
+ if (typeof url !== "string" || blockedDomains.length === 0) return null;
25268
+ try {
25269
+ const hostname = new URL(url).hostname.toLowerCase().replace(/^www\./, "");
25270
+ return blockedDomains.find(
25271
+ (domain) => matchesSourceDomain(hostname, normalizeSourceDomain(domain))
25272
+ ) ?? null;
25273
+ } catch {
25274
+ return null;
25275
+ }
25276
+ }
25277
+ function buildFallbackSourceIndex(findings) {
25278
+ const seen = /* @__PURE__ */ new Set();
25279
+ const sources = [];
25280
+ for (const claim of findings.flatMap((finding) => finding.claims)) {
25281
+ if (!claim.sourceUrl || seen.has(claim.sourceUrl)) continue;
25282
+ seen.add(claim.sourceUrl);
25283
+ sources.push({
25284
+ index: sources.length + 1,
25285
+ url: claim.sourceUrl,
25286
+ title: claim.sourceTitle || claim.sourceUrl,
25287
+ accessedAt: claim.extractedAt,
25288
+ supportingQuote: claim.extractedQuote
25289
+ });
25290
+ }
25291
+ return sources;
25292
+ }
25293
+ function citationForClaim(claim, sourceIndex) {
25294
+ const index = sourceIndex.find((source) => source.url === claim.sourceUrl)?.index ?? 0;
25295
+ return index > 0 ? `[${index}]` : "";
25296
+ }
25297
+ function buildFallbackFindingsByThread(findings, sourceIndex = buildFallbackSourceIndex(findings)) {
25298
+ return findings.map((finding) => {
25299
+ const claimLines = finding.claims.map((claim) => {
25300
+ const citation = citationForClaim(claim, sourceIndex);
25301
+ return citation ? `${claim.claim} ${citation}` : claim.claim;
25302
+ });
25303
+ return {
25304
+ threadLabel: finding.threadLabel,
25305
+ content: claimLines.length > 0 ? claimLines.join("\n\n") : `No citeable claims were extracted for this thread. ${finding.executionSummary}`
25306
+ };
25307
+ });
25308
+ }
25309
+ function buildFallbackReport(objectives, findings, reason) {
25310
+ const sourceIndex = buildFallbackSourceIndex(findings);
25311
+ const findingsByThread = buildFallbackFindingsByThread(findings, sourceIndex);
25312
+ const claimCount = findings.reduce(
25313
+ (sum, finding) => sum + finding.claims.length,
25314
+ 0
25315
+ );
25316
+ 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.`;
25317
+ return {
25318
+ title: objectives.researchQuestion,
25319
+ executiveSummary,
25320
+ findingsByThread,
25321
+ contradictions: [],
25322
+ gaps: [`Final synthesis JSON could not be parsed: ${reason}`],
25323
+ sourceIndex,
25324
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25325
+ objectives
25326
+ };
25327
+ }
25216
25328
  class ResearchOrchestrator {
25217
25329
  constructor(provider, tabManager, runtime2) {
25218
25330
  this.provider = provider;
@@ -25225,6 +25337,8 @@ class ResearchOrchestrator {
25225
25337
  runtime;
25226
25338
  state;
25227
25339
  updateListener = null;
25340
+ stopRequested = false;
25341
+ synthesizeAfterStop = false;
25228
25342
  // ── state access ──────────────────────────────────────────────
25229
25343
  initialState() {
25230
25344
  return {
@@ -25233,6 +25347,7 @@ class ResearchOrchestrator {
25233
25347
  includeTraces: false,
25234
25348
  objectives: null,
25235
25349
  threads: [],
25350
+ threadProgress: [],
25236
25351
  threadFindings: [],
25237
25352
  report: null,
25238
25353
  subAgentTraces: [],
@@ -25265,9 +25380,30 @@ class ResearchOrchestrator {
25265
25380
  this.emit();
25266
25381
  }
25267
25382
  cancel() {
25383
+ this.stopRequested = true;
25384
+ this.synthesizeAfterStop = false;
25385
+ this.provider?.cancel();
25268
25386
  this.state = this.initialState();
25269
25387
  this.emit();
25270
25388
  }
25389
+ stopAndSynthesizeCurrentFindings() {
25390
+ if (this.state.phase !== "executing") {
25391
+ logger$7.warn("Not executing, ignoring stopAndSynthesizeCurrentFindings");
25392
+ return;
25393
+ }
25394
+ this.stopRequested = true;
25395
+ this.synthesizeAfterStop = true;
25396
+ this.state.threadProgress = this.state.threadProgress.map(
25397
+ (progress) => progress.status === "completed" || progress.status === "failed" ? progress : {
25398
+ ...progress,
25399
+ status: "stopping",
25400
+ message: "Stopping and preparing to synthesize current findings",
25401
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
25402
+ }
25403
+ );
25404
+ this.emit();
25405
+ this.provider?.cancel();
25406
+ }
25271
25407
  /**
25272
25408
  * Swap the AI provider used by this orchestrator.
25273
25409
  * Safe to call while research is in progress — running sub-agents
@@ -25313,8 +25449,12 @@ class ResearchOrchestrator {
25313
25449
  logger$7.warn("Not in planning phase, ignoring setObjectives");
25314
25450
  return;
25315
25451
  }
25316
- this.state.objectives = objectives;
25317
- this.state.threads = objectives.threads.slice(0, MAX_THREADS);
25452
+ const threads = objectives.threads.slice(0, MAX_THREADS).map(mergeBlockedSourceDomains);
25453
+ this.state.objectives = {
25454
+ ...objectives,
25455
+ threads
25456
+ };
25457
+ this.state.threads = threads;
25318
25458
  this.setPhase("awaiting_approval");
25319
25459
  }
25320
25460
  /**
@@ -25383,8 +25523,28 @@ class ResearchOrchestrator {
25383
25523
  }
25384
25524
  if (mode) this.state.supervisionMode = mode;
25385
25525
  if (includeTraces !== void 0) this.state.includeTraces = includeTraces;
25526
+ this.stopRequested = false;
25527
+ this.synthesizeAfterStop = false;
25528
+ this.state.threadFindings = [];
25529
+ this.state.threadProgress = this.state.threads.map((thread) => ({
25530
+ threadLabel: thread.label,
25531
+ status: "queued",
25532
+ message: "Queued",
25533
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
25534
+ }));
25386
25535
  this.setPhase("executing");
25387
25536
  }
25537
+ updateThreadProgress(threadLabel, status, message) {
25538
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
25539
+ const existingIndex = this.state.threadProgress.findIndex(
25540
+ (progress) => progress.threadLabel === threadLabel
25541
+ );
25542
+ const next = { threadLabel, status, message, updatedAt };
25543
+ this.state.threadProgress = existingIndex >= 0 ? this.state.threadProgress.map(
25544
+ (progress, index) => index === existingIndex ? next : progress
25545
+ ) : [...this.state.threadProgress, next];
25546
+ this.emit();
25547
+ }
25388
25548
  // ── phase: executing → synthesizing ────────────────────────────
25389
25549
  async executeSubAgents() {
25390
25550
  if (this.state.phase !== "executing" || !this.state.objectives) return;
@@ -25404,8 +25564,20 @@ class ResearchOrchestrator {
25404
25564
  });
25405
25565
  })
25406
25566
  );
25567
+ const shouldSynthesize = this.synthesizeAfterStop;
25407
25568
  if (this.state.phase !== "executing") return;
25408
25569
  this.state.threadFindings = results.filter((f) => f !== null);
25570
+ this.stopRequested = false;
25571
+ this.synthesizeAfterStop = false;
25572
+ if (!shouldSynthesize) {
25573
+ for (const finding of this.state.threadFindings) {
25574
+ this.updateThreadProgress(
25575
+ finding.threadLabel,
25576
+ finding.claims.length > 0 ? "completed" : "failed",
25577
+ finding.claims.length > 0 ? `${finding.claims.length} claim${finding.claims.length === 1 ? "" : "s"} extracted` : "No citeable claims extracted"
25578
+ );
25579
+ }
25580
+ }
25409
25581
  this.setPhase("synthesizing");
25410
25582
  try {
25411
25583
  await this.synthesizeReport();
@@ -25426,6 +25598,7 @@ class ResearchOrchestrator {
25426
25598
  };
25427
25599
  const tabId = this.tabManager.createTab();
25428
25600
  let sourcesConsumed = 0;
25601
+ this.updateThreadProgress(thread.label, "running", "Researching sources");
25429
25602
  if (tabId) this.tabManager.switchTab(tabId);
25430
25603
  const discardedSources = [];
25431
25604
  let transcript = "";
@@ -25454,10 +25627,32 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25454
25627
  },
25455
25628
  async (name, args) => {
25456
25629
  const t0 = Date.now();
25457
- if (this.state.phase !== "executing") {
25630
+ if (this.state.phase !== "executing" || this.stopRequested) {
25458
25631
  const msg = "Research cancelled — stopping.";
25459
25632
  return msg;
25460
25633
  }
25634
+ if (name === "navigate") {
25635
+ const blockedDomain = getBlockedSourceNavigation(
25636
+ args.url,
25637
+ thread.blockedDomains
25638
+ );
25639
+ if (blockedDomain) {
25640
+ const msg = `Source skipped: ${String(args.url)} matches the Research Desk source do-not-allow list (${blockedDomain}). Choose a different source.`;
25641
+ discardedSources.push({
25642
+ url: String(args.url || ""),
25643
+ title: String(args.url || "excluded source"),
25644
+ reason: msg
25645
+ });
25646
+ trace.toolCalls.push({
25647
+ tool: name,
25648
+ args,
25649
+ result: msg,
25650
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
25651
+ durationMs: 0
25652
+ });
25653
+ return msg;
25654
+ }
25655
+ }
25461
25656
  if (name === "navigate" || name === "search") {
25462
25657
  sourcesConsumed++;
25463
25658
  if (sourcesConsumed > thread.sourceBudget) {
@@ -25513,6 +25708,9 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25513
25708
  message: String(err),
25514
25709
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
25515
25710
  });
25711
+ if (this.state.phase === "executing") {
25712
+ this.updateThreadProgress(thread.label, "stopping", "Stopping thread");
25713
+ }
25516
25714
  } finally {
25517
25715
  trace.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
25518
25716
  if (tabId) {
@@ -25537,6 +25735,13 @@ Start by searching for: ${thread.searchQueries.join(" or ")}`;
25537
25735
  const pagesVisited = trace.toolCalls.filter(
25538
25736
  (t) => ["navigate", "read_page", "search"].includes(t.tool)
25539
25737
  ).length;
25738
+ if (this.state.phase === "executing") {
25739
+ this.updateThreadProgress(
25740
+ thread.label,
25741
+ claims.length > 0 ? "completed" : this.stopRequested ? "stopping" : "failed",
25742
+ claims.length > 0 ? `${claims.length} claim${claims.length === 1 ? "" : "s"} extracted` : this.stopRequested ? "Stopped before citeable claims were extracted" : "No citeable claims extracted"
25743
+ );
25744
+ }
25540
25745
  return {
25541
25746
  threadLabel: thread.label,
25542
25747
  threadQuestion: thread.question,
@@ -25633,7 +25838,7 @@ ${transcript.slice(0, 32e3)}`;
25633
25838
  () => {
25634
25839
  }
25635
25840
  );
25636
- const report = this.parseReportFromJson(response, objectives);
25841
+ const report = this.parseReportFromJson(response, objectives, findings);
25637
25842
  this.setReport(report);
25638
25843
  this.setPhase("delivered");
25639
25844
  return report;
@@ -25642,7 +25847,7 @@ ${transcript.slice(0, 32e3)}`;
25642
25847
  * Parse the LLM's JSON synthesis response into a structured ResearchReport.
25643
25848
  * Handles both bare JSON and JSON wrapped in markdown fences.
25644
25849
  */
25645
- parseReportFromJson(text, objectives) {
25850
+ parseReportFromJson(text, objectives, findings) {
25646
25851
  let json = text.trim();
25647
25852
  const fenceMatch = json.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
25648
25853
  if (fenceMatch) json = fenceMatch[1].trim();
@@ -25650,16 +25855,27 @@ ${transcript.slice(0, 32e3)}`;
25650
25855
  if (objMatch) json = objMatch[0];
25651
25856
  try {
25652
25857
  const parsed = JSON.parse(json);
25858
+ const sourceIndex = Array.isArray(parsed.sourceIndex) ? parsed.sourceIndex.map((s) => {
25859
+ const obj = s;
25860
+ return {
25861
+ index: typeof obj.index === "number" ? obj.index : parseInt(String(obj.index), 10) || 0,
25862
+ url: String(obj.url || "").trim(),
25863
+ title: String(obj.title || "").trim(),
25864
+ accessedAt: String(obj.accessedAt || "").trim(),
25865
+ supportingQuote: String(obj.supportingQuote || "").trim()
25866
+ };
25867
+ }).filter((s) => s.url && s.title) : [];
25868
+ const findingsByThread = Array.isArray(parsed.findingsByThread) ? parsed.findingsByThread.map((s) => {
25869
+ const obj = s;
25870
+ return {
25871
+ threadLabel: String(obj.threadLabel || "").trim(),
25872
+ content: String(obj.content || "").trim()
25873
+ };
25874
+ }) : [];
25653
25875
  return {
25654
25876
  title: String(parsed.title || objectives.researchQuestion).trim(),
25655
25877
  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
- }) : [],
25878
+ findingsByThread: findingsByThread.length > 0 ? findingsByThread : buildFallbackFindingsByThread(findings),
25663
25879
  contradictions: Array.isArray(parsed.contradictions) ? parsed.contradictions.map((c) => {
25664
25880
  const obj = c;
25665
25881
  const sourceA = obj.sourceA ?? {};
@@ -25680,31 +25896,13 @@ ${transcript.slice(0, 32e3)}`;
25680
25896
  (c) => c.claim && c.sourceA.url && c.sourceB.url && c.resolution
25681
25897
  ) : [],
25682
25898
  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) : [],
25899
+ sourceIndex: sourceIndex.length > 0 ? sourceIndex : buildFallbackSourceIndex(findings),
25693
25900
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
25694
25901
  objectives
25695
25902
  };
25696
25903
  } 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
- };
25904
+ logger$7.warn("Failed to parse synthesis JSON, using sourced fallback report", err);
25905
+ return buildFallbackReport(objectives, findings, String(err));
25708
25906
  }
25709
25907
  }
25710
25908
  // ── report management ──────────────────────────────────────────
@@ -27075,6 +27273,14 @@ const PREMIUM_TRACKABLE_STEPS = [
27075
27273
  ];
27076
27274
  const premiumApiOrigin = process.env.VESSEL_PREMIUM_API ? new URL(process.env.VESSEL_PREMIUM_API).origin : "https://vesselpremium.quantaintellect.com";
27077
27275
  function registerPremiumHandlers(tabManager, sendToRendererViews) {
27276
+ const trackPremiumStatusChange = (previousStatus, nextStatus, source) => {
27277
+ if (previousStatus === nextStatus) return;
27278
+ trackPremiumFunnel("premium_status_changed", {
27279
+ previous_status: previousStatus,
27280
+ new_status: nextStatus,
27281
+ source
27282
+ });
27283
+ };
27078
27284
  const watchPremiumCheckoutTab = (tabId) => {
27079
27285
  const tab = tabManager.getTab(tabId);
27080
27286
  const wc = tab?.view.webContents;
@@ -27112,13 +27318,26 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
27112
27318
  return;
27113
27319
  }
27114
27320
  trackPremiumFunnel("auto_activation_attempted");
27321
+ trackPremiumFunnel("premium_verify_started", {
27322
+ source: "checkout_auto"
27323
+ });
27324
+ const previousStatus = getPremiumState().status;
27115
27325
  const state2 = await verifySubscription(sessionId);
27116
27326
  if (isPremiumActiveState(state2)) {
27117
27327
  sendToRendererViews(Channels.PREMIUM_UPDATE, state2);
27328
+ trackPremiumFunnel("premium_verify_succeeded", {
27329
+ status: state2.status,
27330
+ source: "checkout_auto"
27331
+ });
27332
+ trackPremiumStatusChange(previousStatus, state2.status, "checkout_auto");
27118
27333
  trackPremiumFunnel("auto_activation_succeeded", {
27119
27334
  status: state2.status
27120
27335
  });
27121
27336
  } else {
27337
+ trackPremiumFunnel("premium_verify_failed", {
27338
+ status: state2.status,
27339
+ source: "checkout_auto"
27340
+ });
27122
27341
  trackPremiumFunnel("auto_activation_failed", {
27123
27342
  status: state2.status
27124
27343
  });
@@ -27151,9 +27370,19 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
27151
27370
  return errorResult("Invalid email format");
27152
27371
  }
27153
27372
  trackPremiumFunnel("activation_attempted");
27373
+ trackPremiumFunnel("activation_code_requested", {
27374
+ source: "settings"
27375
+ });
27154
27376
  const result = await requestActivationCode(email);
27155
27377
  if (!result.ok) {
27378
+ trackPremiumFunnel("activation_code_failed", {
27379
+ source: "settings"
27380
+ });
27156
27381
  trackPremiumFunnel("activation_failed");
27382
+ } else {
27383
+ trackPremiumFunnel("activation_code_sent", {
27384
+ source: "settings"
27385
+ });
27157
27386
  }
27158
27387
  return result;
27159
27388
  });
@@ -27170,13 +27399,30 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
27170
27399
  });
27171
27400
  }
27172
27401
  trackPremiumFunnel("activation_attempted");
27402
+ trackPremiumFunnel("premium_verify_started", {
27403
+ source: "settings_code"
27404
+ });
27405
+ const previousStatus = getPremiumState().status;
27173
27406
  const result = await verifyActivationCode(email, code, challengeToken);
27174
27407
  if (result.ok) {
27408
+ trackPremiumFunnel("premium_verify_succeeded", {
27409
+ status: result.state.status,
27410
+ source: "settings_code"
27411
+ });
27175
27412
  trackPremiumFunnel("activation_succeeded", {
27176
27413
  status: result.state.status
27177
27414
  });
27415
+ trackPremiumStatusChange(
27416
+ previousStatus,
27417
+ result.state.status,
27418
+ "settings_code"
27419
+ );
27178
27420
  sendToRendererViews(Channels.PREMIUM_UPDATE, result.state);
27179
27421
  } else {
27422
+ trackPremiumFunnel("premium_verify_failed", {
27423
+ status: result.state.status,
27424
+ source: "settings_code"
27425
+ });
27180
27426
  trackPremiumFunnel("activation_failed", { status: result.state.status });
27181
27427
  }
27182
27428
  return result;
@@ -27189,6 +27435,8 @@ function registerPremiumHandlers(tabManager, sendToRendererViews) {
27189
27435
  if (result.ok && result.url) {
27190
27436
  const tabId = tabManager.createTab(result.url);
27191
27437
  watchPremiumCheckoutTab(tabId);
27438
+ } else {
27439
+ trackPremiumFunnel("checkout_open_failed");
27192
27440
  }
27193
27441
  return result;
27194
27442
  });
@@ -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: {