chatgpt-pro-research 0.2.1 → 0.2.5

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.
Files changed (3) hide show
  1. package/lib/client.js +224 -27
  2. package/lib/index.js +105 -9
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -34,8 +34,135 @@ var import_react = require("react");
34
34
  var RESEARCH_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-40 -76 200 200"><title>Anyvia R</title><defs><mask id="R-mono-black-standard-gap" maskUnits="userSpaceOnUse" x="-100" y="-100" width="400" height="400"><rect x="-100" y="-100" width="400" height="400" fill="white"/><path d="M77.18032292691355 52.74975779269961 A3 3 0 0 1 79.72446721538282 48.16 L108.02474889607315 48.16 A3 3 0 0 1 110.56889318454242 49.57024220730038 L139.21290427606598 95.41024220730039 A3 3 0 0 1 136.6687599875967 100 L108.36847830690638 100 A3 3 0 0 1 105.82433401843711 98.58975779269961 Z" transform="translate(-30 -12)" fill="black" stroke="black" stroke-width="6" stroke-linejoin="round"/></mask></defs><g mask="url(#R-mono-black-standard-gap)"><path d="M3.46 -40 H67 A39 39 0 0 1 67 38 H36.81354211962983 V84.54 Q36.81354211962983 88 33.35354211962983 88 H3.46 Q0 88 0 84.54 V-36.54 Q0 -40 3.46 -40 Z M36.81354211962983 -14 V12 H67 A13 13 0 0 0 67 -14 Z" fill="currentColor" fill-rule="evenodd"/></g><path d="M77.18032292691355 52.74975779269961 A3 3 0 0 1 79.72446721538282 48.16 L108.02474889607315 48.16 A3 3 0 0 1 110.56889318454242 49.57024220730038 L139.21290427606598 95.41024220730039 A3 3 0 0 1 136.6687599875967 100 L108.36847830690638 100 A3 3 0 0 1 105.82433401843711 98.58975779269961 Z" transform="translate(-30 -12)" fill="currentColor"/></svg>';
35
35
  var DESIGN_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-22.599456503942804 -56 200 200"><title>Anyvia X</title><defs><mask id="X-mono-black-standard-gap" maskUnits="userSpaceOnUse" x="-100" y="-100" width="400" height="400"><rect x="-100" y="-100" width="400" height="400" fill="white"/><path d="M77.18032292691355 52.74975779269961 A3 3 0 0 1 79.72446721538282 48.16 L108.02474889607315 48.16 A3 3 0 0 1 110.56889318454242 49.57024220730038 L139.21290427606598 95.41024220730039 A3 3 0 0 1 136.6687599875967 100 L108.36847830690638 100 A3 3 0 0 1 105.82433401843711 98.58975779269961 Z" transform="translate(-12 0)" fill="black" stroke="black" stroke-width="6" stroke-linejoin="round"/></mask></defs><g mask="url(#X-mono-black-standard-gap)"><path d="M89.67202319894069 -10.373520654246889 A3.46 3.46 0 0 1 92.60626961164192 -12 L127.85608887910462 -12 A3.46 3.46 0 0 1 130.79033529180586 -6.706479345753113 L65.12906379317373 98.37352065424689 A3.46 3.46 0 0 1 62.19481738047249 100 L26.94499811300981 100 A3.46 3.46 0 0 1 24.010751700308575 94.70647934575311 Z" fill="currentColor"/><path d="M28.027942559241307 -6.706479345753113 A3.46 3.46 0 0 1 30.96218897194254 -12 L58.17762652153973 -12 A3.46 3.46 0 0 1 61.111872934240964 -10.373520654246889 L91.78046072595076 38.70647934575311 A3.46 3.46 0 0 1 88.84621431324952 44 L61.63077676365233 44 A3.46 3.46 0 0 1 58.69653035095109 42.37352065424689 Z" fill="currentColor"/></g><path d="M77.18032292691355 52.74975779269961 A3 3 0 0 1 79.72446721538282 48.16 L108.02474889607315 48.16 A3 3 0 0 1 110.56889318454242 49.57024220730038 L139.21290427606598 95.41024220730039 A3 3 0 0 1 136.6687599875967 100 L108.36847830690638 100 A3 3 0 0 1 105.82433401843711 98.58975779269961 Z" transform="translate(-12 0)" fill="currentColor"/></svg>';
36
36
 
37
+ // client/sidebar-status.ts
38
+ var POLL_MS = 5e3;
39
+ function countTopics(topics) {
40
+ const counts = { active: 0, completed: 0, failed: 0 };
41
+ for (const topic of topics) {
42
+ switch (topic.status) {
43
+ case "queued":
44
+ case "uploading":
45
+ case "submitted":
46
+ case "waiting":
47
+ case "answered":
48
+ case "adjudicating":
49
+ counts.active += 1;
50
+ break;
51
+ case "completed":
52
+ counts.completed += 1;
53
+ break;
54
+ case "failed":
55
+ case "stalled":
56
+ counts.failed += 1;
57
+ break;
58
+ default:
59
+ break;
60
+ }
61
+ }
62
+ return counts;
63
+ }
64
+ function badgeText(counts) {
65
+ const parts = [];
66
+ if (counts.active > 0) parts.push(`${counts.active} \u8FDB\u884C`);
67
+ if (counts.completed > 0) parts.push(`${counts.completed} \u5B8C\u6210`);
68
+ if (counts.failed > 0) parts.push(`${counts.failed} \u5931\u8D25`);
69
+ return parts.join(" \xB7 ");
70
+ }
71
+ function mountSidebarStatus(options) {
72
+ if (typeof document === "undefined") return () => {
73
+ };
74
+ let badge;
75
+ let text = "";
76
+ let disposed = false;
77
+ let polling = false;
78
+ let attachTimer;
79
+ let observer;
80
+ const rowFor = () => {
81
+ const buttons = document.querySelectorAll("button[aria-label]");
82
+ for (const button of buttons) {
83
+ if (button.getAttribute("aria-label") === options.label) return button;
84
+ }
85
+ return void 0;
86
+ };
87
+ const applyText = (next) => {
88
+ text = next;
89
+ if (badge !== void 0 && badge.textContent !== text) badge.textContent = text;
90
+ ensureMounted();
91
+ };
92
+ const ensureMounted = () => {
93
+ if (disposed) return;
94
+ if (text === "") {
95
+ badge?.remove();
96
+ badge = void 0;
97
+ return;
98
+ }
99
+ const row = rowFor();
100
+ if (row === void 0) return;
101
+ const title = row.querySelector('[class*="panelTitle"]');
102
+ if (title === null) {
103
+ badge?.remove();
104
+ badge = void 0;
105
+ return;
106
+ }
107
+ if (badge !== void 0 && badge.parentElement === row) {
108
+ if (badge.textContent !== text) badge.textContent = text;
109
+ return;
110
+ }
111
+ badge = document.createElement("span");
112
+ badge.dataset.dshProBadge = options.panelId;
113
+ badge.textContent = text;
114
+ badge.setAttribute("aria-hidden", "true");
115
+ badge.style.cssText = [
116
+ "margin-left:auto",
117
+ "display:inline-flex",
118
+ "align-items:center",
119
+ "flex:0 0 auto",
120
+ "font-size:10px",
121
+ "line-height:1",
122
+ "opacity:.62",
123
+ "white-space:nowrap",
124
+ "font-variant-numeric:tabular-nums",
125
+ "pointer-events:none"
126
+ ].join(";");
127
+ row.appendChild(badge);
128
+ };
129
+ const refresh = async () => {
130
+ if (disposed || polling) return;
131
+ polling = true;
132
+ try {
133
+ const response = await fetch(`${options.routes}/topics`, { headers: { accept: "application/json" } });
134
+ if (!response.ok) return;
135
+ const data = await response.json();
136
+ applyText(badgeText(countTopics(data.topics ?? [])));
137
+ } catch {
138
+ } finally {
139
+ polling = false;
140
+ }
141
+ };
142
+ observer = new MutationObserver(() => {
143
+ if (attachTimer !== void 0) return;
144
+ attachTimer = window.setTimeout(() => {
145
+ attachTimer = void 0;
146
+ ensureMounted();
147
+ }, 100);
148
+ });
149
+ observer.observe(document.body, { childList: true, subtree: true });
150
+ const timer = window.setInterval(() => {
151
+ void refresh();
152
+ }, POLL_MS);
153
+ void refresh();
154
+ return () => {
155
+ disposed = true;
156
+ window.clearInterval(timer);
157
+ if (attachTimer !== void 0) window.clearTimeout(attachTimer);
158
+ observer?.disconnect();
159
+ badge?.remove();
160
+ badge = void 0;
161
+ };
162
+ }
163
+
37
164
  // client/panel.tsx
38
- var POLL_MS = 3e3;
165
+ var POLL_MS2 = 3e3;
39
166
  var config = {
40
167
  routes: "/chatgpt-pro-research",
41
168
  panelId: "chatgpt-pro-research",
@@ -126,7 +253,7 @@ function Panel() {
126
253
  void loadList();
127
254
  const timer = setInterval(() => {
128
255
  void loadList();
129
- }, POLL_MS);
256
+ }, POLL_MS2);
130
257
  return () => clearInterval(timer);
131
258
  }, [loadList]);
132
259
  (0, import_react.useEffect)(() => {
@@ -134,7 +261,7 @@ function Panel() {
134
261
  void loadDetail(selected);
135
262
  const timer = setInterval(() => {
136
263
  void loadDetail(selected);
137
- }, POLL_MS);
264
+ }, POLL_MS2);
138
265
  return () => clearInterval(timer);
139
266
  }, [selected, loadDetail, topics]);
140
267
  const act = (0, import_react.useCallback)(async (action, conclusion) => {
@@ -216,26 +343,7 @@ function Panel() {
216
343
  ) : void 0
217
344
  ),
218
345
  (0, import_react.createElement)("div", { style: { fontWeight: 600, marginBottom: 4 } }, "\u8F6E\u6B21"),
219
- (detail.rounds_detail ?? []).length === 0 ? (0, import_react.createElement)("div", { style: { opacity: 0.6 } }, "\u8FD8\u6CA1\u6709\u63D0\u4EA4\u8BB0\u5F55\u3002") : (0, import_react.createElement)(
220
- "table",
221
- { style: { width: "100%", borderCollapse: "collapse", fontSize: 12 } },
222
- (0, import_react.createElement)("thead", {}, (0, import_react.createElement)(
223
- "tr",
224
- {},
225
- ["\u8F6E", "\u88C1\u51B3", "\u6587\u4EF6", "\u63D0\u4EA4\u65F6\u95F4"].map((label) => (0, import_react.createElement)("th", {
226
- key: label,
227
- style: { textAlign: "left", padding: "4px 6px", borderBottom: "1px solid rgba(127,127,127,.25)", opacity: 0.7, fontWeight: 500 }
228
- }, label))
229
- )),
230
- (0, import_react.createElement)("tbody", {}, (detail.rounds_detail ?? []).map((round) => (0, import_react.createElement)(
231
- "tr",
232
- { key: round.n },
233
- (0, import_react.createElement)("td", { style: cellStyle() }, String(round.n)),
234
- (0, import_react.createElement)("td", { style: cellStyle() }, round.verdict ?? "\u2014"),
235
- (0, import_react.createElement)("td", { style: cellStyle() }, String(round.files)),
236
- (0, import_react.createElement)("td", { style: cellStyle() }, round.submittedAt?.slice(0, 16).replace("T", " ") ?? "\u2014")
237
- )))
238
- ),
346
+ (detail.rounds_detail ?? []).length === 0 ? (0, import_react.createElement)("div", { style: { opacity: 0.6 } }, "\u8FD8\u6CA1\u6709\u63D0\u4EA4\u8BB0\u5F55\u3002") : (0, import_react.createElement)("div", {}, (detail.rounds_detail ?? []).map((round) => renderRound(detail, round))),
239
347
  config.designMode && detail.designs != null ? (0, import_react.createElement)("div", { style: { marginTop: 8, opacity: 0.75 } }, `\u6BCF\u8F6E\u65B9\u5411\uFF1A${detail.designs}`) : void 0,
240
348
  renderArtifacts(detail)
241
349
  )
@@ -249,7 +357,16 @@ function renderArtifacts(detail) {
249
357
  const artifacts = detail.artifacts ?? [];
250
358
  if (artifacts.length === 0) return void 0;
251
359
  if (!config.designMode) {
252
- return (0, import_react.createElement)("div", { style: { marginTop: 10, opacity: 0.75 } }, `\u4EA7\u7269\uFF1A${artifacts.join("\u3001")}`);
360
+ return (0, import_react.createElement)(
361
+ "div",
362
+ { style: { marginTop: 10 } },
363
+ (0, import_react.createElement)("div", { style: { fontWeight: 600, marginBottom: 4 } }, "\u5168\u90E8\u4EA7\u7269"),
364
+ (0, import_react.createElement)(
365
+ "div",
366
+ { style: { display: "flex", gap: 6, flexWrap: "wrap" } },
367
+ artifacts.map((path) => artifactChip(detail.topic_id, path))
368
+ )
369
+ );
253
370
  }
254
371
  const images = artifacts.filter((path) => /\.(png|jpe?g|webp|gif|svg)$/i.test(path));
255
372
  const demos = artifacts.filter((path) => /\.html?$/i.test(path));
@@ -290,6 +407,87 @@ function renderArtifacts(detail) {
290
407
  (0, import_react.createElement)("div", { style: { marginTop: 8, opacity: 0.6, fontSize: 11, wordBreak: "break-all" } }, artifacts.join("\u3001"))
291
408
  );
292
409
  }
410
+ function renderRound(detail, round) {
411
+ const prefix = `/round-${String(round.n).padStart(2, "0")}/`;
412
+ const artifacts = round.artifacts ?? (detail.artifacts ?? []).filter((path) => path.includes(prefix));
413
+ const attempts = round.attempts ?? 1;
414
+ return (0, import_react.createElement)(
415
+ "div",
416
+ {
417
+ key: round.n,
418
+ style: { padding: "8px 0", borderTop: "1px solid rgba(127,127,127,.18)" }
419
+ },
420
+ (0, import_react.createElement)(
421
+ "div",
422
+ { style: { display: "flex", gap: 10, flexWrap: "wrap", alignItems: "baseline", fontSize: 12 } },
423
+ (0, import_react.createElement)("strong", {}, `\u7B2C ${round.n} \u8F6E`),
424
+ (0, import_react.createElement)("span", { style: { opacity: 0.72 } }, round.verdict ?? (round.answeredAt !== void 0 ? "\u5DF2\u7B54\u590D" : "\u8FDB\u884C\u4E2D")),
425
+ (0, import_react.createElement)("span", { style: { opacity: 0.6 } }, `${round.files} \u6587\u4EF6`),
426
+ (0, import_react.createElement)("span", { style: { opacity: 0.6 } }, `\u63D0\u4EA4 ${formatTime(round.submittedAt)}`),
427
+ (0, import_react.createElement)("span", { style: { opacity: 0.6 } }, `\u5B8C\u6210 ${formatTime(round.answeredAt)}`),
428
+ attempts > 1 ? (0, import_react.createElement)("span", { style: { color: "#ed6c02" } }, `\u91CD\u8BD5 ${attempts - 1}`) : void 0
429
+ ),
430
+ (0, import_react.createElement)(
431
+ "div",
432
+ { style: { display: "flex", gap: 6, flexWrap: "wrap", marginTop: 6 } },
433
+ (0, import_react.createElement)("button", {
434
+ onClick: () => window.open(`${config.routes}/answer?id=${encodeURIComponent(detail.topic_id)}&round=${round.n}`, "_blank", "noopener,noreferrer"),
435
+ style: chipStyle()
436
+ }, "\u67E5\u770B\u56DE\u7B54"),
437
+ artifacts.map((path) => artifactChip(detail.topic_id, path))
438
+ )
439
+ );
440
+ }
441
+ function artifactChip(topicId, path) {
442
+ const name = path.split("/").pop() ?? path;
443
+ if (/\.(png|jpe?g|webp|gif|svg)$/i.test(path)) {
444
+ return (0, import_react.createElement)("a", {
445
+ key: path,
446
+ href: assetUrl(topicId, path),
447
+ target: "_blank",
448
+ rel: "noreferrer",
449
+ style: chipStyle()
450
+ }, name);
451
+ }
452
+ if (/\.html?$/i.test(path)) {
453
+ return (0, import_react.createElement)("a", {
454
+ key: path,
455
+ href: assetUrl(topicId, path),
456
+ target: "_blank",
457
+ rel: "noreferrer",
458
+ style: chipStyle()
459
+ }, `${name} \u2197`);
460
+ }
461
+ return (0, import_react.createElement)("button", {
462
+ key: path,
463
+ onClick: () => void revealArtifact(topicId, path),
464
+ style: chipStyle()
465
+ }, `${name} \xB7 \u76EE\u5F55`);
466
+ }
467
+ async function revealArtifact(topicId, path) {
468
+ await fetch(`${config.routes}/open`, {
469
+ method: "POST",
470
+ headers: { "content-type": "application/json" },
471
+ body: JSON.stringify({ topic_id: topicId, path })
472
+ }).catch(() => void 0);
473
+ }
474
+ function formatTime(value) {
475
+ return value === void 0 ? "\u2014" : value.slice(0, 16).replace("T", " ");
476
+ }
477
+ function chipStyle() {
478
+ return {
479
+ padding: "2px 8px",
480
+ borderRadius: 999,
481
+ border: "1px solid rgba(127,127,127,.35)",
482
+ background: "transparent",
483
+ color: "inherit",
484
+ cursor: "pointer",
485
+ fontSize: 11,
486
+ textDecoration: "none",
487
+ display: "inline-flex",
488
+ alignItems: "center"
489
+ };
490
+ }
293
491
  function buttonStyle() {
294
492
  return {
295
493
  padding: "4px 10px",
@@ -301,9 +499,6 @@ function buttonStyle() {
301
499
  fontSize: 12
302
500
  };
303
501
  }
304
- function cellStyle() {
305
- return { padding: "4px 6px", borderBottom: "1px solid rgba(127,127,127,.12)" };
306
- }
307
502
  function Icon({ kind }) {
308
503
  const svg = kind === "design" ? DESIGN_ICON : RESEARCH_ICON;
309
504
  return (0, import_react.createElement)("span", {
@@ -315,6 +510,8 @@ function Icon({ kind }) {
315
510
  function apply(ctx) {
316
511
  ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: config.panelId }, () => (0, import_react.createElement)(Panel)));
317
512
  ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({ name: "sidebar.panellist", id: config.panelId, order: 21, label: () => config.label }, () => (0, import_react.createElement)(Icon, { kind: config.icon })));
513
+ const disposeBadge = mountSidebarStatus({ label: config.label, routes: config.routes, panelId: config.panelId });
514
+ if (typeof ctx.effect === "function") ctx.effect(() => disposeBadge, `${config.panelId}: sidebar status badge`);
318
515
  }
319
516
 
320
517
  // client/index.tsx
package/lib/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // chatgpt-pro-research — built from packages/research/src/index.ts
2
2
 
3
3
  // src/plugin.ts
4
+ import { spawn } from "node:child_process";
4
5
  import { homedir } from "node:os";
5
6
  import { dirname as dirname2, extname, isAbsolute, join as join3, resolve as resolve3, sep } from "node:path";
6
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
7
+ import { access, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import z from "@deepseek-ai/schemastery";
9
10
  import { defineTool } from "@deepseek-ai/dsh-tools";
@@ -649,7 +650,7 @@ var ResearchEngine = class {
649
650
  }
650
651
  let round = pending ?? topic.rounds.find((item) => item.n === n);
651
652
  if (round === void 0) {
652
- round = { n, files: 0 };
653
+ round = { n, files: 0, attempts: 0 };
653
654
  topic.rounds.push(round);
654
655
  const stored = n === 1 ? await this.options.store.readQuestion(topic, 0) : await this.options.store.readQuestion(topic, n);
655
656
  const brief = stored ?? this.followUpBrief(topic);
@@ -667,7 +668,10 @@ var ResearchEngine = class {
667
668
  const fetched = await this.waitAnswer(round, signal);
668
669
  if (fetched === void 0) return;
669
670
  await this.options.store.writeAnswer(topic, n, fetched.answer);
670
- await this.options.store.writeArtifacts(topic, n, fetched);
671
+ const written = await this.options.store.writeArtifacts(topic, n, fetched);
672
+ if (written.length > 0) {
673
+ round.artifacts = [.../* @__PURE__ */ new Set([...round.artifacts ?? [], ...written])];
674
+ }
671
675
  round.answeredAt = (/* @__PURE__ */ new Date()).toISOString();
672
676
  await this.transition(topic, "answered");
673
677
  this.emit({ type: "answered", topicId: topic.id, round: n });
@@ -690,6 +694,7 @@ var ResearchEngine = class {
690
694
  fallbackFiles = allFiles;
691
695
  }
692
696
  round.files = files.length;
697
+ round.attempts = (round.attempts ?? 0) + 1;
693
698
  const result = await this.options.transport.submit({
694
699
  topicId: topic.id,
695
700
  round: round.n,
@@ -733,7 +738,9 @@ var ResearchEngine = class {
733
738
  try {
734
739
  return await this.options.transport.fetch({ submissionId });
735
740
  } catch (error) {
736
- if (polled.answer !== void 0) return { answer: polled.answer };
741
+ if (polled.answer !== void 0) {
742
+ return { answer: polled.answer.startsWith("image:") ? "" : polled.answer };
743
+ }
737
744
  throw error;
738
745
  }
739
746
  }
@@ -932,6 +939,16 @@ function advanceCompletion(previous, text, streaming) {
932
939
  return { lastText: text, stable: 1, answered: false };
933
940
  }
934
941
 
942
+ // ../transport/src/assets.ts
943
+ function parseDataUrlImage(source) {
944
+ const match = /^data:([^;,]+);base64,(.*)$/s.exec(source);
945
+ if (match === null) return void 0;
946
+ const mime = match[1];
947
+ const base64 = match[2];
948
+ if (mime === void 0 || base64 === void 0) return void 0;
949
+ return { mime, base64: base64.replace(/\s/g, "") };
950
+ }
951
+
935
952
  // ../transport/src/chatgpt.ts
936
953
  function sha256(value) {
937
954
  return createHash3("sha256").update(value, "utf8").digest("hex");
@@ -959,6 +976,8 @@ var ChatGptWebTransport = class {
959
976
  context;
960
977
  launching;
961
978
  submissions = /* @__PURE__ */ new Map();
979
+ /** One visible tab per topic so later rounds update the same conversation. */
980
+ topicPages = /* @__PURE__ */ new Map();
962
981
  options;
963
982
  constructor(options) {
964
983
  this.options = options;
@@ -1030,7 +1049,9 @@ var ChatGptWebTransport = class {
1030
1049
  }
1031
1050
  async submit(request) {
1032
1051
  const context = await this.ensureContext();
1033
- const page = await context.newPage();
1052
+ const existing = this.topicPages.get(request.topicId);
1053
+ const page = existing !== void 0 && !existing.isClosed() ? existing : await context.newPage();
1054
+ this.topicPages.set(request.topicId, page);
1034
1055
  try {
1035
1056
  const target = request.session?.conversationUrl ?? this.appUrl;
1036
1057
  let reused = request.session?.conversationUrl !== void 0;
@@ -1140,6 +1161,17 @@ ${attachments.join("\n")}`);
1140
1161
  const images = [];
1141
1162
  for (const [index, source] of sources.slice(0, 8).entries()) {
1142
1163
  try {
1164
+ if (source.startsWith("data:")) {
1165
+ const parsed = parseDataUrlImage(source);
1166
+ if (parsed === void 0) continue;
1167
+ if (Buffer.from(parsed.base64, "base64").byteLength < 2e4) continue;
1168
+ images.push({
1169
+ name: `image-${String(index + 1).padStart(2, "0")}.${extensionFor(parsed.mime)}`,
1170
+ mime: parsed.mime,
1171
+ base64: parsed.base64
1172
+ });
1173
+ continue;
1174
+ }
1143
1175
  if (source.startsWith("blob:")) {
1144
1176
  const dataUrl = await page.evaluate(async (src) => {
1145
1177
  const response = await fetch(src);
@@ -1181,10 +1213,15 @@ ${attachments.join("\n")}`);
1181
1213
  })).filter((block) => block.text.trim() !== ""));
1182
1214
  }
1183
1215
  async cancel() {
1184
- for (const submission of this.submissions.values()) {
1185
- await submission.page.close().catch(() => void 0);
1216
+ const pages = /* @__PURE__ */ new Set([
1217
+ ...[...this.submissions.values()].map((submission) => submission.page),
1218
+ ...this.topicPages.values()
1219
+ ]);
1220
+ for (const page of pages) {
1221
+ await page.close().catch(() => void 0);
1186
1222
  }
1187
1223
  this.submissions.clear();
1224
+ this.topicPages.clear();
1188
1225
  }
1189
1226
  async close() {
1190
1227
  await this.cancel();
@@ -1237,8 +1274,8 @@ ${attachments.join("\n")}`);
1237
1274
  async richAnswerText(page) {
1238
1275
  const text = await this.lastAssistantText(page);
1239
1276
  if (text !== "") return text;
1240
- const images = await page.evaluate(() => [...document.querySelectorAll("main img")].filter((image) => image.naturalWidth >= 300).map((image) => image.src));
1241
- return images.length > 0 ? `image:${images.join("|")}` : "";
1277
+ const images = await page.evaluate(() => [...document.querySelectorAll("main img")].filter((image) => image.naturalWidth >= 300).map((image) => `${image.naturalWidth}x${image.naturalHeight}`));
1278
+ return images.length > 0 ? `image:${images.length}:${images.join(",")}` : "";
1242
1279
  }
1243
1280
  async lastAssistantText(page) {
1244
1281
  const messages = page.locator(SELECTORS.assistantMessage);
@@ -1601,6 +1638,28 @@ ${input.answer.slice(0, 3e4)}`
1601
1638
  const bytes = await readFile3(target);
1602
1639
  return { bytes, mime: mimeFor(target) };
1603
1640
  }
1641
+ /** Raw answer text for one round; shown in a new tab from the panel. */
1642
+ async answerText(id, round) {
1643
+ const entry = await this.entryFor(id);
1644
+ if (entry === void 0) return void 0;
1645
+ const store = this.storeAt(entry.root, entry.mode);
1646
+ const topic = await store.load(id);
1647
+ return await store.readAnswer(topic, round);
1648
+ }
1649
+ /** Reveal a topic-relative file in the OS file manager. */
1650
+ async revealFile(id, relativePath) {
1651
+ const entry = await this.entryFor(id);
1652
+ if (entry === void 0) throw new Error(`unknown topic: ${id}`);
1653
+ const store = this.storeAt(entry.root, entry.mode);
1654
+ const dir = await store.dirOf(id);
1655
+ const target = resolve3(dir, relativePath);
1656
+ if (target !== dir && !target.startsWith(`${dir}${sep}`)) {
1657
+ throw new Error("path escapes the topic");
1658
+ }
1659
+ await access(target);
1660
+ const child = process.platform === "win32" ? spawn("explorer.exe", ["/select,", target], { detached: true, stdio: "ignore" }) : process.platform === "darwin" ? spawn("open", ["-R", target], { detached: true, stdio: "ignore" }) : spawn("xdg-open", [dirname2(target)], { detached: true, stdio: "ignore" });
1661
+ child.unref();
1662
+ }
1604
1663
  async act(id, action, conclusion, note) {
1605
1664
  const entry = await this.entryFor(id);
1606
1665
  if (entry === void 0) throw new Error(`unknown topic: ${id}`);
@@ -1881,6 +1940,43 @@ function registerRoutes(scoped, service, spec2) {
1881
1940
  }
1882
1941
  }
1883
1942
  }), `${spec2.name}: asset route`);
1943
+ scoped.effect(() => webServer.register({
1944
+ kind: "exact",
1945
+ path: `${spec2.routes}/answer`,
1946
+ handler: async (req, res) => {
1947
+ const denied = reject(req);
1948
+ if (denied !== void 0) return sendJson(res, denied, { error: "unauthorized" });
1949
+ const url = new URL(req.url ?? "/", "http://localhost");
1950
+ const id = url.searchParams.get("id") ?? "";
1951
+ const round = Number(url.searchParams.get("round") ?? "0");
1952
+ try {
1953
+ const text = await service.answerText(id, round);
1954
+ if (text === void 0) return sendJson(res, 404, { error: "answer not found" });
1955
+ res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
1956
+ res.end(text);
1957
+ } catch (error) {
1958
+ sendJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
1959
+ }
1960
+ }
1961
+ }), `${spec2.name}: answer route`);
1962
+ scoped.effect(() => webServer.register({
1963
+ kind: "exact",
1964
+ path: `${spec2.routes}/open`,
1965
+ handler: async (req, res) => {
1966
+ const denied = reject(req);
1967
+ if (denied !== void 0) return sendJson(res, denied, { error: "unauthorized" });
1968
+ if (req.method !== "POST") return sendJson(res, 405, { error: "method not allowed" });
1969
+ try {
1970
+ const body = await readJsonBody(req);
1971
+ const id = typeof body.topic_id === "string" ? body.topic_id : "";
1972
+ const relativePath = typeof body.path === "string" ? body.path : "";
1973
+ await service.revealFile(id, relativePath);
1974
+ sendJson(res, 200, { ok: true });
1975
+ } catch (error) {
1976
+ sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
1977
+ }
1978
+ }
1979
+ }), `${spec2.name}: open route`);
1884
1980
  }
1885
1981
  function createPlugin(spec2) {
1886
1982
  return (ctx, config) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatgpt-pro-research",
3
- "version": "0.2.1",
3
+ "version": "0.2.5",
4
4
  "description": "ChatGPT Pro Research: unattended multi-round research through a real ChatGPT Pro web session, with local records and a DSH sidebar.",
5
5
  "private": false,
6
6
  "type": "module",