codex-chatgpt-control 0.1.0-alpha.1 → 0.3.0-alpha.1

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 (255) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +55 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +8 -0
  4. package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
  5. package/contracts/v1/fixtures/command-descriptors.json +224 -16
  6. package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
  7. package/contracts/v1/fixtures/doctor-scenario-preflight.json +119 -0
  8. package/contracts/v1/fixtures/existing-tab-diagnostics-blocker.json +57 -0
  9. package/contracts/v1/fixtures/files-preflight-success.json +32 -0
  10. package/contracts/v1/fixtures/help-root.json +1 -1
  11. package/contracts/v1/fixtures/output-json-parse-success.json +17 -2
  12. package/contracts/v1/fixtures/project-sources-plan-add.json +71 -0
  13. package/contracts/v1/fixtures/read-latest-clipped.json +31 -0
  14. package/contracts/v1/fixtures/reports-create-redacted.json +15 -0
  15. package/contracts/v1/fixtures/responses-basic-success.json +11 -0
  16. package/contracts/v1/fixtures/run-basic-success.json +15 -0
  17. package/contracts/v1/fixtures/run-browser-bridge-blocker.json +2 -2
  18. package/contracts/v1/fixtures/run-timeout-partial.json +2 -2
  19. package/contracts/v1/fixtures/runner-budget-blocker.json +1 -1
  20. package/contracts/v1/fixtures/stream-blocked.ndjson +1 -1
  21. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
  22. package/contracts/v1/manifest.json +30 -0
  23. package/contracts/v1/parity-suite.json +80 -7
  24. package/contracts/v1/schemas/backend-request.schema.json +3 -0
  25. package/contracts/v1/schemas/command-result.schema.json +48 -0
  26. package/contracts/v1/schemas/response.schema.json +28 -0
  27. package/contracts/v1/schemas/run-result.schema.json +84 -0
  28. package/contracts/v1/surface-drift-policy.json +210 -0
  29. package/dist/codex-chatgpt-control-backend.mjs +7478 -1789
  30. package/dist/codex-chatgpt-control.bundle.mjs +8023 -2266
  31. package/dist/src/backend/client.d.ts +6 -1
  32. package/dist/src/backend/client.js +5 -0
  33. package/dist/src/backend/protocol.d.ts +1 -1
  34. package/dist/src/backend/protocol.js +8 -0
  35. package/dist/src/backend/session.js +16 -0
  36. package/dist/src/browser/attach.js +128 -20
  37. package/dist/src/browser/clipboard.d.ts +11 -0
  38. package/dist/src/browser/clipboard.js +34 -7
  39. package/dist/src/browser/downloads.js +2 -1
  40. package/dist/src/browser/page-state.js +18 -5
  41. package/dist/src/client.d.ts +17 -1
  42. package/dist/src/client.js +89 -9
  43. package/dist/src/commands/artifacts.d.ts +5 -0
  44. package/dist/src/commands/artifacts.js +432 -0
  45. package/dist/src/commands/context.js +8 -3
  46. package/dist/src/commands/conversation.d.ts +15 -0
  47. package/dist/src/commands/conversation.js +44 -0
  48. package/dist/src/commands/deadline.d.ts +8 -0
  49. package/dist/src/commands/deadline.js +14 -0
  50. package/dist/src/commands/doctor.d.ts +10 -2
  51. package/dist/src/commands/doctor.js +249 -14
  52. package/dist/src/commands/files.d.ts +2 -1
  53. package/dist/src/commands/files.js +406 -18
  54. package/dist/src/commands/messages.d.ts +2 -1
  55. package/dist/src/commands/messages.js +430 -98
  56. package/dist/src/commands/modes.d.ts +4 -1
  57. package/dist/src/commands/modes.js +404 -37
  58. package/dist/src/commands/probes.d.ts +15 -0
  59. package/dist/src/commands/probes.js +64 -0
  60. package/dist/src/commands/project-sources.d.ts +10 -0
  61. package/dist/src/commands/project-sources.js +643 -0
  62. package/dist/src/commands/registry.js +108 -14
  63. package/dist/src/commands/reports.d.ts +9 -0
  64. package/dist/src/commands/reports.js +84 -7
  65. package/dist/src/commands/response-actions.js +33 -0
  66. package/dist/src/commands/sequence.js +14 -0
  67. package/dist/src/commands/threads.js +4 -25
  68. package/dist/src/commands/timeouts.d.ts +2 -0
  69. package/dist/src/commands/timeouts.js +18 -0
  70. package/dist/src/diagnostics/blockers.d.ts +54 -0
  71. package/dist/src/diagnostics/blockers.js +299 -0
  72. package/dist/src/dom/artifacts.d.ts +7 -0
  73. package/dist/src/dom/artifacts.js +198 -0
  74. package/dist/src/dom/generation-state.d.ts +15 -0
  75. package/dist/src/dom/generation-state.js +109 -0
  76. package/dist/src/dom/label-match.d.ts +3 -0
  77. package/dist/src/dom/label-match.js +25 -0
  78. package/dist/src/dom/locale/am.d.ts +34 -0
  79. package/dist/src/dom/locale/am.js +34 -0
  80. package/dist/src/dom/locale/ar.d.ts +35 -0
  81. package/dist/src/dom/locale/ar.js +35 -0
  82. package/dist/src/dom/locale/bg.d.ts +35 -0
  83. package/dist/src/dom/locale/bg.js +35 -0
  84. package/dist/src/dom/locale/bn.d.ts +35 -0
  85. package/dist/src/dom/locale/bn.js +35 -0
  86. package/dist/src/dom/locale/bs.d.ts +34 -0
  87. package/dist/src/dom/locale/bs.js +34 -0
  88. package/dist/src/dom/locale/ca.d.ts +34 -0
  89. package/dist/src/dom/locale/ca.js +34 -0
  90. package/dist/src/dom/locale/cs.d.ts +34 -0
  91. package/dist/src/dom/locale/cs.js +34 -0
  92. package/dist/src/dom/locale/da.d.ts +33 -0
  93. package/dist/src/dom/locale/da.js +33 -0
  94. package/dist/src/dom/locale/de.d.ts +36 -0
  95. package/dist/src/dom/locale/de.js +36 -0
  96. package/dist/src/dom/locale/el.d.ts +34 -0
  97. package/dist/src/dom/locale/el.js +34 -0
  98. package/dist/src/dom/locale/en.d.ts +64 -0
  99. package/dist/src/dom/locale/en.js +71 -0
  100. package/dist/src/dom/locale/es-419.d.ts +34 -0
  101. package/dist/src/dom/locale/es-419.js +34 -0
  102. package/dist/src/dom/locale/es-ES.d.ts +34 -0
  103. package/dist/src/dom/locale/es-ES.js +34 -0
  104. package/dist/src/dom/locale/et.d.ts +34 -0
  105. package/dist/src/dom/locale/et.js +34 -0
  106. package/dist/src/dom/locale/fa.d.ts +35 -0
  107. package/dist/src/dom/locale/fa.js +35 -0
  108. package/dist/src/dom/locale/fi.d.ts +34 -0
  109. package/dist/src/dom/locale/fi.js +34 -0
  110. package/dist/src/dom/locale/fr-CA.d.ts +35 -0
  111. package/dist/src/dom/locale/fr-CA.js +35 -0
  112. package/dist/src/dom/locale/fr-FR.d.ts +35 -0
  113. package/dist/src/dom/locale/fr-FR.js +35 -0
  114. package/dist/src/dom/locale/gu.d.ts +34 -0
  115. package/dist/src/dom/locale/gu.js +34 -0
  116. package/dist/src/dom/locale/hi.d.ts +34 -0
  117. package/dist/src/dom/locale/hi.js +34 -0
  118. package/dist/src/dom/locale/hr.d.ts +33 -0
  119. package/dist/src/dom/locale/hr.js +33 -0
  120. package/dist/src/dom/locale/hu.d.ts +34 -0
  121. package/dist/src/dom/locale/hu.js +34 -0
  122. package/dist/src/dom/locale/hy.d.ts +35 -0
  123. package/dist/src/dom/locale/hy.js +35 -0
  124. package/dist/src/dom/locale/id.d.ts +34 -0
  125. package/dist/src/dom/locale/id.js +34 -0
  126. package/dist/src/dom/locale/index.d.ts +83 -0
  127. package/dist/src/dom/locale/index.js +239 -0
  128. package/dist/src/dom/locale/is.d.ts +34 -0
  129. package/dist/src/dom/locale/is.js +34 -0
  130. package/dist/src/dom/locale/it.d.ts +33 -0
  131. package/dist/src/dom/locale/it.js +33 -0
  132. package/dist/src/dom/locale/ja.d.ts +33 -0
  133. package/dist/src/dom/locale/ja.js +33 -0
  134. package/dist/src/dom/locale/ka.d.ts +34 -0
  135. package/dist/src/dom/locale/ka.js +34 -0
  136. package/dist/src/dom/locale/kk.d.ts +34 -0
  137. package/dist/src/dom/locale/kk.js +34 -0
  138. package/dist/src/dom/locale/kn.d.ts +35 -0
  139. package/dist/src/dom/locale/kn.js +35 -0
  140. package/dist/src/dom/locale/ko.d.ts +34 -0
  141. package/dist/src/dom/locale/ko.js +34 -0
  142. package/dist/src/dom/locale/lt.d.ts +35 -0
  143. package/dist/src/dom/locale/lt.js +35 -0
  144. package/dist/src/dom/locale/lv.d.ts +34 -0
  145. package/dist/src/dom/locale/lv.js +34 -0
  146. package/dist/src/dom/locale/mk.d.ts +33 -0
  147. package/dist/src/dom/locale/mk.js +33 -0
  148. package/dist/src/dom/locale/ml.d.ts +35 -0
  149. package/dist/src/dom/locale/ml.js +35 -0
  150. package/dist/src/dom/locale/mn.d.ts +35 -0
  151. package/dist/src/dom/locale/mn.js +35 -0
  152. package/dist/src/dom/locale/mr.d.ts +35 -0
  153. package/dist/src/dom/locale/mr.js +35 -0
  154. package/dist/src/dom/locale/ms.d.ts +34 -0
  155. package/dist/src/dom/locale/ms.js +34 -0
  156. package/dist/src/dom/locale/my.d.ts +34 -0
  157. package/dist/src/dom/locale/my.js +34 -0
  158. package/dist/src/dom/locale/nb.d.ts +34 -0
  159. package/dist/src/dom/locale/nb.js +34 -0
  160. package/dist/src/dom/locale/nl.d.ts +34 -0
  161. package/dist/src/dom/locale/nl.js +34 -0
  162. package/dist/src/dom/locale/pa.d.ts +35 -0
  163. package/dist/src/dom/locale/pa.js +35 -0
  164. package/dist/src/dom/locale/pl.d.ts +34 -0
  165. package/dist/src/dom/locale/pl.js +34 -0
  166. package/dist/src/dom/locale/pt-BR.d.ts +34 -0
  167. package/dist/src/dom/locale/pt-BR.js +34 -0
  168. package/dist/src/dom/locale/pt-PT.d.ts +34 -0
  169. package/dist/src/dom/locale/pt-PT.js +34 -0
  170. package/dist/src/dom/locale/ro.d.ts +33 -0
  171. package/dist/src/dom/locale/ro.js +33 -0
  172. package/dist/src/dom/locale/ru.d.ts +31 -0
  173. package/dist/src/dom/locale/ru.js +31 -0
  174. package/dist/src/dom/locale/sk.d.ts +34 -0
  175. package/dist/src/dom/locale/sk.js +34 -0
  176. package/dist/src/dom/locale/sl.d.ts +34 -0
  177. package/dist/src/dom/locale/sl.js +34 -0
  178. package/dist/src/dom/locale/so.d.ts +34 -0
  179. package/dist/src/dom/locale/so.js +34 -0
  180. package/dist/src/dom/locale/sq.d.ts +34 -0
  181. package/dist/src/dom/locale/sq.js +34 -0
  182. package/dist/src/dom/locale/sr.d.ts +31 -0
  183. package/dist/src/dom/locale/sr.js +31 -0
  184. package/dist/src/dom/locale/sv.d.ts +34 -0
  185. package/dist/src/dom/locale/sv.js +34 -0
  186. package/dist/src/dom/locale/sw.d.ts +34 -0
  187. package/dist/src/dom/locale/sw.js +34 -0
  188. package/dist/src/dom/locale/ta.d.ts +35 -0
  189. package/dist/src/dom/locale/ta.js +35 -0
  190. package/dist/src/dom/locale/te.d.ts +35 -0
  191. package/dist/src/dom/locale/te.js +35 -0
  192. package/dist/src/dom/locale/th.d.ts +34 -0
  193. package/dist/src/dom/locale/th.js +34 -0
  194. package/dist/src/dom/locale/tl.d.ts +28 -0
  195. package/dist/src/dom/locale/tl.js +28 -0
  196. package/dist/src/dom/locale/tr.d.ts +34 -0
  197. package/dist/src/dom/locale/tr.js +34 -0
  198. package/dist/src/dom/locale/types.d.ts +52 -0
  199. package/dist/src/dom/locale/types.js +1 -0
  200. package/dist/src/dom/locale/uk.d.ts +34 -0
  201. package/dist/src/dom/locale/uk.js +34 -0
  202. package/dist/src/dom/locale/ur.d.ts +34 -0
  203. package/dist/src/dom/locale/ur.js +34 -0
  204. package/dist/src/dom/locale/vi.d.ts +34 -0
  205. package/dist/src/dom/locale/vi.js +34 -0
  206. package/dist/src/dom/locale/zh-HK.d.ts +35 -0
  207. package/dist/src/dom/locale/zh-HK.js +35 -0
  208. package/dist/src/dom/locale/zh-Hans.d.ts +35 -0
  209. package/dist/src/dom/locale/zh-Hans.js +35 -0
  210. package/dist/src/dom/locale/zh-TW.d.ts +36 -0
  211. package/dist/src/dom/locale/zh-TW.js +36 -0
  212. package/dist/src/dom/locale-labels.d.ts +26 -0
  213. package/dist/src/dom/locale-labels.js +26 -0
  214. package/dist/src/dom/menus.d.ts +6 -0
  215. package/dist/src/dom/menus.js +47 -10
  216. package/dist/src/dom/message-format.d.ts +2 -1
  217. package/dist/src/dom/message-format.js +67 -10
  218. package/dist/src/dom/messages.d.ts +2 -1
  219. package/dist/src/dom/messages.js +2 -4
  220. package/dist/src/dom/selectors.d.ts +1 -0
  221. package/dist/src/dom/selectors.js +42 -15
  222. package/dist/src/dom/wait-snapshot.d.ts +43 -0
  223. package/dist/src/dom/wait-snapshot.js +127 -0
  224. package/dist/src/index.d.ts +5 -0
  225. package/dist/src/index.js +5 -0
  226. package/dist/src/platform/local-paths.d.ts +4 -0
  227. package/dist/src/platform/local-paths.js +20 -0
  228. package/dist/src/runner/interruptions.js +15 -4
  229. package/dist/src/runner/responses.js +18 -1
  230. package/dist/src/runner/result.js +15 -0
  231. package/dist/src/runner/resume.js +1 -0
  232. package/dist/src/runner/types.d.ts +3 -0
  233. package/dist/src/safety/risk.d.ts +8 -0
  234. package/dist/src/safety/risk.js +8 -0
  235. package/dist/src/safety/untrusted-output.d.ts +73 -0
  236. package/dist/src/safety/untrusted-output.js +199 -0
  237. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +1 -0
  238. package/dist/src/scripts/apply-intelligence-locale-captures.js +270 -0
  239. package/dist/src/scripts/capture-intelligence-locales.d.ts +25 -0
  240. package/dist/src/scripts/capture-intelligence-locales.js +669 -0
  241. package/dist/src/scripts/check-surface-drift.d.ts +1 -0
  242. package/dist/src/scripts/check-surface-drift.js +11 -0
  243. package/dist/src/scripts/live-smoke/scenarios.js +84 -0
  244. package/dist/src/scripts/locale-capture/language-coverage.d.ts +10 -0
  245. package/dist/src/scripts/locale-capture/language-coverage.js +39 -0
  246. package/dist/src/testing/surface-drift.d.ts +66 -0
  247. package/dist/src/testing/surface-drift.js +363 -0
  248. package/dist/src/types.d.ts +229 -2
  249. package/package.json +3 -2
  250. package/references/backend-protocol.md +96 -2
  251. package/references/language-coverage.md +142 -0
  252. package/references/localization.md +257 -0
  253. package/references/python-parity.md +67 -3
  254. package/references/safety.md +2 -0
  255. package/references/troubleshooting.md +83 -2
@@ -1,7 +1,9 @@
1
- import { attachFiles, downloadLatestFile } from "./commands/files.js";
1
+ import { downloadLatestArtifact, listLatestArtifacts, waitForArtifact } from "./commands/artifacts.js";
2
+ import { attachFiles, downloadLatestFile, preflightFiles } from "./commands/files.js";
3
+ import { addProjectSources, buildProjectSourceAddPlan, listProjectSources } from "./commands/project-sources.js";
2
4
  import { doctor } from "./commands/doctor.js";
3
5
  import { askMessage, composeMessage, readLatest, submitMessage, waitAndRead, waitForMessage } from "./commands/messages.js";
4
- import { selectTool, setMode } from "./commands/modes.js";
6
+ import { getMode, selectTool, setMode } from "./commands/modes.js";
5
7
  import { createRunReport } from "./commands/reports.js";
6
8
  import { copyResponse } from "./commands/response-actions.js";
7
9
  import { commandDescriptors, describeCommand, helpText } from "./commands/registry.js";
@@ -14,6 +16,7 @@ import { toRunResult } from "./runner/result.js";
14
16
  import { responseFromRunResult, responsesCreateArgsToRunInput, unsupportedResponse, validateResponsesCreateArgs } from "./runner/responses.js";
15
17
  import { streamFromRunResult } from "./runner/stream.js";
16
18
  import { redactReportValue } from "./safety/report-redaction.js";
19
+ import { explainCommandBlocker } from "./diagnostics/blockers.js";
17
20
  export function createChatGPT(options = {}) {
18
21
  const env = runtimeEnv(options);
19
22
  const limits = normalizeLimits(options.limits);
@@ -44,6 +47,7 @@ export function createChatGPT(options = {}) {
44
47
  runPlan: plan => runPlanInvocation(plan, env, limits, options.defaults, options.reporting),
45
48
  doctor: args => doctor(env, args),
46
49
  createReport: (result, args) => createRunReport(env, result, args ?? options.reporting ?? {}),
50
+ explainBlocker: (resultOrBlocker, args) => explainCommandBlocker(resultOrBlocker, args),
47
51
  reports: {
48
52
  create: (result, args) => createRunReport(env, result, args ?? options.reporting ?? {}),
49
53
  redact: async (value, args) => resultOk(redactReportValue(value, args), {}),
@@ -70,11 +74,25 @@ export function createChatGPT(options = {}) {
70
74
  waitAndRead: args => waitAndRead(env, args)
71
75
  },
72
76
  files: {
77
+ preflight: args => preflightFiles(env, args),
73
78
  attach: args => attachFiles(env, args),
74
79
  downloadLatest: args => downloadLatestFile(env, args)
75
80
  },
81
+ projects: {
82
+ sources: {
83
+ list: args => listProjectSources(env, args),
84
+ planAdd: args => buildProjectSourceAddPlan(env, args),
85
+ add: args => addProjectSources(env, args)
86
+ }
87
+ },
88
+ artifacts: {
89
+ listLatest: args => listLatestArtifacts(env, args),
90
+ wait: args => waitForArtifact(env, args),
91
+ downloadLatest: args => downloadLatestArtifact(env, args)
92
+ },
76
93
  modes: {
77
- set: args => setMode(env, args)
94
+ set: args => setMode(env, args),
95
+ get: args => getMode(env, args)
78
96
  },
79
97
  tools: {
80
98
  select: args => selectTool(env, args)
@@ -88,6 +106,9 @@ async function runGuarded(plan, env, limits, report) {
88
106
  const budget = checkRunBudget(plan, limits);
89
107
  if (budget !== undefined)
90
108
  return budget;
109
+ const filePreflight = await preflightPlanFiles(plan, env);
110
+ if (filePreflight !== undefined)
111
+ return filePreflight;
91
112
  const result = await runSequence(plan, env);
92
113
  if (report === undefined || report.enabled === false)
93
114
  return result;
@@ -133,6 +154,18 @@ async function runGuarded(plan, env, limits, report) {
133
154
  ]
134
155
  };
135
156
  }
157
+ async function preflightPlanFiles(plan, env) {
158
+ const paths = plan.steps
159
+ .flatMap(step => step.command === "files.attach" ? pathsFromAttachStep(step) : []);
160
+ if (paths.length === 0)
161
+ return undefined;
162
+ const result = await preflightFiles(env, { paths });
163
+ return result.ok ? undefined : result;
164
+ }
165
+ function pathsFromAttachStep(step) {
166
+ const paths = step.args.paths;
167
+ return paths.every(item => typeof item === "string") ? paths : [];
168
+ }
136
169
  function normalizeLimits(limits) {
137
170
  return {
138
171
  maxPromptsPerRun: limits?.maxPromptsPerRun ?? 5,
@@ -232,6 +265,7 @@ function planAgentWorkflowFromNormalized(agent, input, defaults = {}) {
232
265
  const wait = input.wait ?? agent.defaults.wait ?? defaults.wait ?? true;
233
266
  const read = input.read ?? agent.defaults.read ?? defaults.read ?? { format: "markdown" };
234
267
  const thread = input.thread ?? agent.defaults.thread ?? { type: "new" };
268
+ const artifactDownload = input.download !== undefined && input.download !== false && usesCreateImageTool(input.tools);
235
269
  const steps = [
236
270
  bootstrapStepForWorkflow(thread, input.existingTab ?? agent.defaults.existingTab ?? defaults.existingTab, input.preferExistingTab ?? agent.defaults.preferExistingTab ?? defaults.preferExistingTab),
237
271
  ...threadSteps(thread)
@@ -246,6 +280,9 @@ function planAgentWorkflowFromNormalized(agent, input, defaults = {}) {
246
280
  if (input.files.length > 0) {
247
281
  steps.push({ id: "attach", command: "files.attach", args: { paths: input.files } });
248
282
  }
283
+ if (artifactDownload) {
284
+ steps.push({ id: "artifactBaseline", command: "artifacts.listLatest", args: { kind: "image" } });
285
+ }
249
286
  if (agent.instructionsMode === "visible_setup_message" && hasInstructions(agent)) {
250
287
  steps.push({
251
288
  id: "agent_setup",
@@ -262,15 +299,22 @@ function planAgentWorkflowFromNormalized(agent, input, defaults = {}) {
262
299
  command: "messages.ask",
263
300
  args: {
264
301
  text: renderRunnerPrompt(agent, input.prompt),
265
- wait,
266
- read
302
+ wait: artifactDownload ? false : wait,
303
+ read: artifactDownload ? false : read
267
304
  }
268
305
  });
306
+ if (artifactDownload) {
307
+ steps.push({
308
+ id: "artifact",
309
+ command: "artifacts.wait",
310
+ args: artifactWaitArgs(wait, input.download === false ? undefined : input.download)
311
+ });
312
+ }
269
313
  if (input.copy !== undefined && input.copy !== false) {
270
314
  steps.push({ id: "copy", command: "response.copy", args: input.copy });
271
315
  }
272
316
  if (input.download !== undefined && input.download !== false) {
273
- steps.push({ id: "download", command: "files.downloadLatest", args: input.download });
317
+ steps.push({ id: "download", command: artifactDownload ? "artifacts.downloadLatest" : "files.downloadLatest", args: input.download });
274
318
  }
275
319
  return {
276
320
  name: `agent-run:${agent.name}`,
@@ -425,17 +469,28 @@ function planAskWorkflow(args, defaults = {}) {
425
469
  if (files.length > 0) {
426
470
  steps.push({ id: "attach", command: "files.attach", args: { paths: files } });
427
471
  }
472
+ const artifactDownload = args.download !== undefined && usesCreateImageTool(args.tools ?? []);
473
+ if (artifactDownload) {
474
+ steps.push({ id: "artifactBaseline", command: "artifacts.listLatest", args: { kind: "image" } });
475
+ }
428
476
  steps.push({
429
477
  id: "ask",
430
478
  command: "messages.ask",
431
479
  args: {
432
480
  text: args.prompt,
433
- wait: args.wait ?? defaults.wait ?? true,
434
- read: args.read ?? defaults.read ?? { format: "markdown" }
481
+ wait: artifactDownload ? false : args.wait ?? defaults.wait ?? true,
482
+ read: artifactDownload ? false : args.read ?? defaults.read ?? { format: "markdown" }
435
483
  }
436
484
  });
437
485
  if (args.download !== undefined) {
438
- steps.push({ id: "download", command: "files.downloadLatest", args: args.download });
486
+ if (artifactDownload) {
487
+ steps.push({
488
+ id: "artifact",
489
+ command: "artifacts.wait",
490
+ args: artifactWaitArgs(args.wait ?? defaults.wait ?? true, args.download)
491
+ });
492
+ }
493
+ steps.push({ id: "download", command: artifactDownload ? "artifacts.downloadLatest" : "files.downloadLatest", args: args.download });
439
494
  }
440
495
  return {
441
496
  name: args.download === undefined ? "ask" : "ask-and-download",
@@ -443,6 +498,31 @@ function planAskWorkflow(args, defaults = {}) {
443
498
  steps
444
499
  };
445
500
  }
501
+ function usesCreateImageTool(tools) {
502
+ return tools.some(tool => normalizeToolName(tool.tool) === "create_image");
503
+ }
504
+ function normalizeToolName(tool) {
505
+ return tool.trim().toLowerCase().replace(/[\s-]+/g, "_");
506
+ }
507
+ function artifactWaitArgs(wait, download) {
508
+ const args = {
509
+ kind: "image",
510
+ afterArtifactCount: "${artifactBaseline.data.count}",
511
+ requireDownload: true
512
+ };
513
+ if (typeof wait === "object") {
514
+ if (wait.timeoutMs !== undefined)
515
+ args.timeoutMs = wait.timeoutMs;
516
+ if (wait.stableMs !== undefined)
517
+ args.stableMs = wait.stableMs;
518
+ if (wait.pollMs !== undefined)
519
+ args.pollMs = wait.pollMs;
520
+ }
521
+ if (args.timeoutMs === undefined && download?.timeoutMs !== undefined) {
522
+ args.timeoutMs = download.timeoutMs;
523
+ }
524
+ return args;
525
+ }
446
526
  function planRunMessages(args, defaults = {}) {
447
527
  const thread = args.thread ?? { type: "new" };
448
528
  const steps = [
@@ -0,0 +1,5 @@
1
+ import type { ArtifactDownloadArgs, ArtifactListData, ArtifactWaitArgs, ArtifactWaitData, CommandResult, DownloadedFile, ListArtifactsArgs, LocatorLike, RuntimeEnv } from "../types.js";
2
+ export declare function listLatestArtifacts(env: RuntimeEnv, args?: ListArtifactsArgs): Promise<CommandResult<ArtifactListData>>;
3
+ export declare function waitForArtifact(env: RuntimeEnv, args?: ArtifactWaitArgs): Promise<CommandResult<ArtifactWaitData>>;
4
+ export declare function downloadLatestArtifact(env: RuntimeEnv, args: ArtifactDownloadArgs): Promise<CommandResult<DownloadedFile>>;
5
+ export declare function locatorCountWithTimeout(locator: LocatorLike | undefined, timeoutMs: number, code: string): Promise<number>;
@@ -0,0 +1,432 @@
1
+ import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { waitForDownloadFromClick } from "../browser/downloads.js";
4
+ import { readPageState } from "../browser/page-state.js";
5
+ import { listPageArtifacts, readLatestImageDataUrl } from "../dom/artifacts.js";
6
+ import { cssSelectors, requiredLocator } from "../dom/selectors.js";
7
+ import { localeLabels } from "../dom/locale-labels.js";
8
+ import { resultOk } from "../errors.js";
9
+ import { contextFromPage } from "./context.js";
10
+ import { bootstrap } from "./session.js";
11
+ import { localGuardTimeout, withTimeout } from "./timeouts.js";
12
+ export async function listLatestArtifacts(env, args = {}) {
13
+ const boot = await ensurePage(env);
14
+ if (!boot.ok) {
15
+ return boot;
16
+ }
17
+ const page = env.page;
18
+ try {
19
+ const artifacts = await listPageArtifactsWithBridgeFallback(env, page, args);
20
+ return resultOk(artifactListData(artifacts), await contextFromPage(page));
21
+ }
22
+ catch (error) {
23
+ return artifactSelectorBlocker(error, await contextFromPage(page));
24
+ }
25
+ }
26
+ export async function waitForArtifact(env, args = {}) {
27
+ const boot = await ensurePage(env);
28
+ if (!boot.ok) {
29
+ return boot;
30
+ }
31
+ const page = env.page;
32
+ const timeoutMs = args.timeoutMs ?? 120000;
33
+ const stableMs = args.stableMs ?? 1000;
34
+ const pollMs = args.pollMs ?? 750;
35
+ const started = Date.now();
36
+ const afterArtifactCount = args.afterArtifactCount ?? 0;
37
+ let lastSignature = "";
38
+ let lastChangedAt = Date.now();
39
+ let latestArtifacts = [];
40
+ while (Date.now() - started < timeoutMs) {
41
+ const state = await withTimeout(readPageState(page), localGuardTimeout(timeoutMs, 5000), "Timed out while reading ChatGPT page state.").catch(() => undefined);
42
+ if (state?.blocker !== undefined && state.blocker.kind !== "modal") {
43
+ return {
44
+ ok: false,
45
+ status: "blocked",
46
+ warnings: [],
47
+ blocker: state.blocker,
48
+ context: await contextFromPage(page)
49
+ };
50
+ }
51
+ try {
52
+ latestArtifacts = await listPageArtifactsWithBridgeFallback(env, page, args);
53
+ }
54
+ catch (error) {
55
+ return artifactSelectorBlocker(error, await contextFromPage(page));
56
+ }
57
+ const latest = latestArtifacts.at(-1);
58
+ const signature = JSON.stringify({
59
+ count: latestArtifacts.length,
60
+ src: latest?.src,
61
+ width: latest?.width,
62
+ height: latest?.height,
63
+ downloadAvailable: latest?.downloadAvailable
64
+ });
65
+ if (signature !== lastSignature) {
66
+ lastSignature = signature;
67
+ lastChangedAt = Date.now();
68
+ }
69
+ const targetReached = latestArtifacts.length > afterArtifactCount
70
+ && latest !== undefined
71
+ && (args.requireDownload !== true || latest.downloadAvailable);
72
+ if (targetReached && Date.now() - lastChangedAt >= stableMs && !await hasStopControl(page, timeoutMs)) {
73
+ return resultOk({
74
+ complete: true,
75
+ count: latestArtifacts.length,
76
+ latest,
77
+ elapsedMs: Date.now() - started
78
+ }, await contextFromPage(page));
79
+ }
80
+ await sleep(page, pollMs);
81
+ }
82
+ const data = {
83
+ complete: false,
84
+ count: latestArtifacts.length,
85
+ elapsedMs: Date.now() - started
86
+ };
87
+ const latest = latestArtifacts.at(-1);
88
+ if (latest !== undefined)
89
+ data.latest = latest;
90
+ return {
91
+ ok: false,
92
+ status: "timeout",
93
+ data,
94
+ warnings: [],
95
+ blocker: {
96
+ kind: "artifact_unavailable",
97
+ code: args.requireDownload === true ? "artifact_download_not_ready" : "artifact_not_ready",
98
+ message: args.requireDownload === true
99
+ ? "No generated artifact with a visible download affordance appeared before the timeout."
100
+ : "No generated artifact appeared before the timeout.",
101
+ resumable: true
102
+ },
103
+ context: await contextFromPage(page)
104
+ };
105
+ }
106
+ export async function downloadLatestArtifact(env, args) {
107
+ const boot = await ensurePage(env);
108
+ if (!boot.ok) {
109
+ return boot;
110
+ }
111
+ const page = env.page;
112
+ const timeoutMs = args.timeoutMs ?? 120000;
113
+ if (args.prefer !== "visible_image_source") {
114
+ const byDownload = await tryDownloadControl(page, args, timeoutMs);
115
+ if (byDownload.ok || args.prefer === "download_control") {
116
+ return byDownload;
117
+ }
118
+ }
119
+ try {
120
+ const byImageSource = await saveLatestVisibleImageSource(page, args.destDir, timeoutMs);
121
+ if (byImageSource !== undefined) {
122
+ return resultOk(byImageSource, await contextFromPage(page));
123
+ }
124
+ }
125
+ catch (error) {
126
+ return artifactDownloadBlocker(error, await contextFromPage(page));
127
+ }
128
+ try {
129
+ const byPageAssets = await saveLatestPageAssetImage(env, page, args.destDir, timeoutMs);
130
+ if (byPageAssets !== undefined) {
131
+ return resultOk(byPageAssets, await contextFromPage(page));
132
+ }
133
+ }
134
+ catch (error) {
135
+ return artifactDownloadBlocker(error, await contextFromPage(page));
136
+ }
137
+ return artifactDownloadBlocker(new Error("No visible generated image source was available to save."), await contextFromPage(page));
138
+ }
139
+ export async function locatorCountWithTimeout(locator, timeoutMs, code) {
140
+ if (locator === undefined || typeof locator.count !== "function") {
141
+ return 0;
142
+ }
143
+ return withTimeout(locator.count(), timeoutMs, `${code}: locator count did not complete before the local guard timeout.`);
144
+ }
145
+ async function tryDownloadControl(page, args, timeoutMs) {
146
+ try {
147
+ const controls = requiredLocator(page, cssSelectors.generatedArtifactDownloadControls);
148
+ const count = await locatorCountWithTimeout(controls, localGuardTimeout(timeoutMs, 5000), "artifact_download_control_timeout");
149
+ if (count === 0) {
150
+ return artifactDownloadBlocker(new Error("No visible generated-image download control was found."), await contextFromPage(page));
151
+ }
152
+ const target = controls.last?.() ?? controls;
153
+ const downloaded = await waitForDownloadFromClick(page, async () => {
154
+ await target.click?.({ timeoutMs: localGuardTimeout(timeoutMs, 10000) });
155
+ }, args.destDir, timeoutMs);
156
+ return resultOk(downloaded, await contextFromPage(page));
157
+ }
158
+ catch (error) {
159
+ return artifactDownloadBlocker(error, await contextFromPage(page));
160
+ }
161
+ }
162
+ async function saveLatestVisibleImageSource(page, destDir, timeoutMs) {
163
+ const source = await readLatestImageDataUrl(page, timeoutMs);
164
+ if (source === undefined)
165
+ return undefined;
166
+ const parsed = parseDataUrl(source.dataUrl);
167
+ if (parsed === undefined)
168
+ return undefined;
169
+ const absoluteDest = resolve(destDir);
170
+ await mkdir(absoluteDest, { recursive: true });
171
+ const suggestedFilename = `generated-image-${Date.now()}.${extensionForMime(parsed.mimeType)}`;
172
+ const path = join(absoluteDest, suggestedFilename);
173
+ await writeFile(path, parsed.bytes);
174
+ const saved = await stat(path);
175
+ if (saved.size <= 0) {
176
+ throw new Error(`Generated image artifact file is empty: ${path}`);
177
+ }
178
+ return { path, suggestedFilename, bytes: saved.size };
179
+ }
180
+ async function listPageArtifactsWithBridgeFallback(env, page, args) {
181
+ try {
182
+ const artifacts = await listPageArtifacts(page, args);
183
+ if (artifacts.length > 0) {
184
+ return artifacts;
185
+ }
186
+ const fromAssets = await listPageAssetArtifacts(env, page, args, args.timeoutMs).catch(() => []);
187
+ return fromAssets.length > 0 ? fromAssets : artifacts;
188
+ }
189
+ catch (error) {
190
+ const fromAssets = await listPageAssetArtifacts(env, page, args, args.timeoutMs).catch(() => []);
191
+ if (fromAssets.length > 0) {
192
+ return fromAssets;
193
+ }
194
+ throw error;
195
+ }
196
+ }
197
+ async function listPageAssetArtifacts(env, page, args, timeoutMs) {
198
+ const inventory = await readPageAssetsInventory(page, timeoutMs).catch(() => undefined)
199
+ ?? await withTemporaryBridgeOwnedPage(env, page, timeoutMs, async (freshPage) => {
200
+ return await readPageAssetsInventory(freshPage, timeoutMs).catch(() => undefined);
201
+ });
202
+ if (inventory === undefined)
203
+ return [];
204
+ const artifacts = inventory.assets
205
+ .filter(asset => asset.kind === "image")
206
+ .filter(asset => !isInlineSvgAsset(asset) && isLikelyRasterImageAsset(asset))
207
+ .map((asset, index) => {
208
+ const artifact = {
209
+ kind: "image",
210
+ index,
211
+ visible: true,
212
+ downloadAvailable: true,
213
+ selectorProvenance: "pageAssets image inventory"
214
+ };
215
+ const src = safeArtifactSrc(asset.url);
216
+ if (src !== undefined)
217
+ artifact.src = src;
218
+ return artifact;
219
+ });
220
+ const max = args.max ?? artifacts.length;
221
+ return artifacts
222
+ .filter(artifact => artifact.kind === (args.kind ?? "image"))
223
+ .slice(-max)
224
+ .map((artifact, index) => ({ ...artifact, index }));
225
+ }
226
+ async function saveLatestPageAssetImage(env, page, destDir, timeoutMs) {
227
+ return await saveLatestPageAssetImageFromPage(page, destDir, timeoutMs).catch(() => undefined)
228
+ ?? await withTemporaryBridgeOwnedPage(env, page, timeoutMs, async (freshPage) => {
229
+ return await saveLatestPageAssetImageFromPage(freshPage, destDir, timeoutMs).catch(() => undefined);
230
+ });
231
+ }
232
+ async function saveLatestPageAssetImageFromPage(page, destDir, timeoutMs) {
233
+ const capability = await getPageAssetsCapability(page);
234
+ if (capability === undefined)
235
+ return undefined;
236
+ const inventory = await withTimeout(capability.list(), localGuardTimeout(timeoutMs, 15000), "Timed out while listing page assets for generated image download.");
237
+ const candidateIds = inventory.assets
238
+ .filter(asset => asset.kind === "image")
239
+ .filter(asset => !isInlineSvgAsset(asset) && isLikelyRasterImageAsset(asset))
240
+ .map(asset => asset.id);
241
+ if (candidateIds.length === 0)
242
+ return undefined;
243
+ const bundled = await withTimeout(capability.bundle({ assetIds: candidateIds, inventoryId: inventory.id, kinds: ["image"] }), localGuardTimeout(timeoutMs, 30000), "Timed out while bundling generated image page asset.");
244
+ const asset = bundled.assets
245
+ .filter(item => !isInlineSvgAsset(item) && isLikelyRasterImageAsset(item))
246
+ .at(-1);
247
+ if (asset === undefined)
248
+ return undefined;
249
+ const absoluteDest = resolve(destDir);
250
+ await mkdir(absoluteDest, { recursive: true });
251
+ const suggestedFilename = `generated-image-${Date.now()}.${extensionForMime(asset.contentType ?? "image/png")}`;
252
+ const path = join(absoluteDest, suggestedFilename);
253
+ await copyFile(asset.path, path);
254
+ const saved = await stat(path);
255
+ if (saved.size <= 0) {
256
+ throw new Error(`Generated image artifact file is empty: ${path}`);
257
+ }
258
+ return { path, suggestedFilename, bytes: saved.size };
259
+ }
260
+ async function readPageAssetsInventory(page, timeoutMs) {
261
+ const capability = await getPageAssetsCapability(page);
262
+ if (capability === undefined)
263
+ return undefined;
264
+ return await withTimeout(capability.list(), localGuardTimeout(timeoutMs, 15000), "Timed out while listing page assets for generated artifacts.");
265
+ }
266
+ async function getPageAssetsCapability(page) {
267
+ const capabilities = page.capabilities;
268
+ const get = capabilities?.get;
269
+ if (typeof get !== "function")
270
+ return undefined;
271
+ const capability = await get.call(capabilities, "pageAssets");
272
+ if (!isPageAssetsCapability(capability))
273
+ return undefined;
274
+ return capability;
275
+ }
276
+ async function withTemporaryBridgeOwnedPage(env, currentPage, timeoutMs, callback) {
277
+ const url = await currentPageUrl(currentPage);
278
+ if (url === undefined || !/^https:\/\/chatgpt\.com\/c\//i.test(url))
279
+ return undefined;
280
+ const freshPage = await openTemporaryPage(env, url, timeoutMs);
281
+ if (freshPage === undefined)
282
+ return undefined;
283
+ try {
284
+ await settlePage(freshPage, localGuardTimeout(timeoutMs, 5000));
285
+ return await callback(freshPage);
286
+ }
287
+ finally {
288
+ await closeTemporaryPage(freshPage).catch(() => undefined);
289
+ }
290
+ }
291
+ async function openTemporaryPage(env, url, timeoutMs) {
292
+ const browser = env.browser;
293
+ if (browser === undefined)
294
+ return undefined;
295
+ let page;
296
+ if (typeof browser.tabs?.create === "function") {
297
+ page = await Promise.resolve(browser.tabs.create.call(browser.tabs, url));
298
+ }
299
+ else if (typeof browser.tabs?.new === "function") {
300
+ page = await Promise.resolve(browser.tabs.new.call(browser.tabs));
301
+ if (typeof page?.goto === "function") {
302
+ await withTimeout(page.goto(url), localGuardTimeout(timeoutMs, 20000), "Timed out while opening generated image conversation in a temporary bridge tab.").catch(() => undefined);
303
+ }
304
+ }
305
+ else if (typeof browser.newPage === "function") {
306
+ page = await Promise.resolve(browser.newPage.call(browser));
307
+ if (typeof page?.goto === "function") {
308
+ await withTimeout(page.goto(url), localGuardTimeout(timeoutMs, 20000), "Timed out while opening generated image conversation in a temporary bridge page.").catch(() => undefined);
309
+ }
310
+ }
311
+ return page;
312
+ }
313
+ async function settlePage(page, timeoutMs) {
314
+ const waitForTimeout = page.waitForTimeout ?? page.playwright?.waitForTimeout;
315
+ if (typeof waitForTimeout !== "function")
316
+ return;
317
+ await withTimeout(waitForTimeout.call(page.waitForTimeout === waitForTimeout ? page : page.playwright, Math.min(timeoutMs, 5000)), timeoutMs, "Timed out while waiting for temporary bridge tab to settle.").catch(() => undefined);
318
+ }
319
+ async function closeTemporaryPage(page) {
320
+ if (typeof page.close === "function") {
321
+ await page.close();
322
+ }
323
+ }
324
+ async function currentPageUrl(page) {
325
+ const value = await Promise.resolve(page.url?.()).catch(() => undefined);
326
+ return typeof value === "string" && value.length > 0 ? value : undefined;
327
+ }
328
+ function isPageAssetsCapability(value) {
329
+ return typeof value === "object"
330
+ && value !== null
331
+ && typeof value.list === "function"
332
+ && typeof value.bundle === "function";
333
+ }
334
+ function isLikelyRasterImageAsset(asset) {
335
+ const contentType = asset.contentType ?? "";
336
+ if (/^image\/(png|jpe?g|webp|gif|avif)$/i.test(contentType))
337
+ return true;
338
+ const name = asset.name ?? basename(asset.path ?? "");
339
+ const url = asset.url ?? "";
340
+ return /\.(png|jpe?g|webp|gif|avif)(?:$|[?#])/i.test(name)
341
+ || /\.(png|jpe?g|webp|gif|avif)(?:$|[?#])/i.test(url)
342
+ || (contentType === "" && !isInlineSvgAsset(asset));
343
+ }
344
+ function isInlineSvgAsset(asset) {
345
+ return /^inline-svg:/i.test(asset.url ?? "")
346
+ || /svg/i.test(asset.contentType ?? "")
347
+ || /\.svg(?:$|[?#])/i.test(asset.name ?? "")
348
+ || /\.svg(?:$|[?#])/i.test(asset.path ?? "");
349
+ }
350
+ function safeArtifactSrc(src) {
351
+ if (src === undefined)
352
+ return undefined;
353
+ if (/^https:\/\/chatgpt\.com\/backend-api\/estuary\/content\b/i.test(src)) {
354
+ return undefined;
355
+ }
356
+ return src;
357
+ }
358
+ function parseDataUrl(dataUrl) {
359
+ const match = /^data:([^;,]+);base64,(.*)$/i.exec(dataUrl);
360
+ if (match === null || match[1] === undefined || match[2] === undefined)
361
+ return undefined;
362
+ return { mimeType: match[1], bytes: Buffer.from(match[2], "base64") };
363
+ }
364
+ function extensionForMime(mimeType) {
365
+ if (/jpeg|jpg/i.test(mimeType))
366
+ return "jpg";
367
+ if (/webp/i.test(mimeType))
368
+ return "webp";
369
+ if (/gif/i.test(mimeType))
370
+ return "gif";
371
+ return "png";
372
+ }
373
+ function artifactListData(artifacts) {
374
+ const data = {
375
+ count: artifacts.length,
376
+ artifacts
377
+ };
378
+ const latest = artifacts.at(-1);
379
+ if (latest !== undefined)
380
+ data.latest = latest;
381
+ return data;
382
+ }
383
+ function artifactSelectorBlocker(error, context) {
384
+ return {
385
+ ok: false,
386
+ status: "blocked",
387
+ warnings: [],
388
+ blocker: {
389
+ kind: "artifact_selector_drift",
390
+ code: "artifact_dom_timeout",
391
+ message: `Generated artifact detection could not inspect the ChatGPT page: ${error instanceof Error ? error.message : String(error)}`,
392
+ resumable: true
393
+ },
394
+ context
395
+ };
396
+ }
397
+ function artifactDownloadBlocker(error, context) {
398
+ return {
399
+ ok: false,
400
+ status: "unsupported",
401
+ warnings: [],
402
+ blocker: {
403
+ kind: "artifact_download_unavailable",
404
+ code: "artifact_download_unavailable",
405
+ message: `No downloadable generated artifact could be saved from the visible ChatGPT page: ${error instanceof Error ? error.message : String(error)}`,
406
+ resumable: true
407
+ },
408
+ context
409
+ };
410
+ }
411
+ async function ensurePage(env) {
412
+ if (env.page !== undefined) {
413
+ return resultOk({}, await contextFromPage(env.page));
414
+ }
415
+ return bootstrap(env, { preferExistingTab: true });
416
+ }
417
+ async function hasStopControl(page, timeoutMs) {
418
+ if (typeof page.evaluate !== "function")
419
+ return false;
420
+ return withTimeout(page.evaluate((phrases) => {
421
+ const text = document.body?.innerText ?? "";
422
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
423
+ return phrases.some(phrase => new RegExp(`\\b${escape(phrase)}\\b`, "i").test(text));
424
+ }, [...localeLabels.stopControl]), localGuardTimeout(timeoutMs, 2000), "Timed out while checking ChatGPT stop controls.").catch(() => false);
425
+ }
426
+ async function sleep(page, ms) {
427
+ if (typeof page.waitForTimeout === "function") {
428
+ await page.waitForTimeout(ms);
429
+ return;
430
+ }
431
+ await new Promise(resolve => setTimeout(resolve, ms));
432
+ }
@@ -1,13 +1,18 @@
1
1
  import { countPageMessages } from "../dom/messages.js";
2
2
  import { parseConversationId } from "../browser/page-state.js";
3
+ import { withTimeout } from "./timeouts.js";
3
4
  export async function contextFromPage(page, partial = {}) {
4
5
  if (page === undefined) {
5
6
  return { timestamp: new Date().toISOString(), ...partial };
6
7
  }
7
8
  const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => partial.url) : partial.url;
8
- const title = typeof page.title === "function" ? await page.title().catch(() => undefined) : partial.title;
9
- const turnCount = await countPageMessages(page).catch(() => partial.turnCount);
10
- const assistantTurnCount = await countPageMessages(page, "assistant").catch(() => partial.assistantTurnCount);
9
+ const title = typeof page.title === "function"
10
+ ? await withTimeout(page.title(), 1000, "Timed out while reading page title.").catch(() => partial.title)
11
+ : partial.title;
12
+ const [turnCount, assistantTurnCount] = await Promise.all([
13
+ withTimeout(countPageMessages(page), 1000, "Timed out while counting page messages.").catch(() => partial.turnCount),
14
+ withTimeout(countPageMessages(page, "assistant"), 1000, "Timed out while counting assistant messages.").catch(() => partial.assistantTurnCount)
15
+ ]);
11
16
  const conversationId = url !== undefined ? parseConversationId(url) : partial.conversationId;
12
17
  const context = {
13
18
  timestamp: new Date().toISOString(),
@@ -0,0 +1,15 @@
1
+ import type { PageLike } from "../types.js";
2
+ export type ConversationTarget = {
3
+ href?: string;
4
+ url: string;
5
+ };
6
+ export type EnsureConversationTargetOptions = {
7
+ timeoutMs: number;
8
+ };
9
+ export type EnsureConversationTargetResult = {
10
+ navigated: boolean;
11
+ targetUrl: string;
12
+ expectedConversationId?: string;
13
+ };
14
+ export declare function ensureConversationTarget(page: PageLike, target: ConversationTarget, options: EnsureConversationTargetOptions): Promise<EnsureConversationTargetResult>;
15
+ export declare function waitForConversationHydrated(page: PageLike, timeoutMs: number, expectedConversationId?: string): Promise<void>;