codex-chatgpt-control 0.2.0-alpha.1 → 0.5.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 (233) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +42 -2
  3. package/contracts/v1/fixtures/backend-capabilities.json +11 -0
  4. package/contracts/v1/fixtures/command-descriptors.json +289 -7
  5. package/contracts/v1/fixtures/describe-runner-run.json +5 -2
  6. package/contracts/v1/fixtures/doctor-scenario-preflight.json +45 -2
  7. package/contracts/v1/fixtures/help-root.json +1 -1
  8. package/contracts/v1/fixtures/output-json-parse-success.json +9 -0
  9. package/contracts/v1/fixtures/reports-create-redacted.json +1 -1
  10. package/contracts/v1/fixtures/run-timeout-partial.json +15 -4
  11. package/contracts/v1/fixtures/stream-in-progress.ndjson +3 -0
  12. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
  13. package/contracts/v1/fixtures/surface-chat-legacy.json +35 -0
  14. package/contracts/v1/fixtures/surface-chat-simplified.json +37 -0
  15. package/contracts/v1/fixtures/surface-sidebar-false-positive.json +29 -0
  16. package/contracts/v1/fixtures/surface-work-advanced.json +45 -0
  17. package/contracts/v1/fixtures/surface-work-basic.json +38 -0
  18. package/contracts/v1/fixtures/workflow-ask-success.json +8 -0
  19. package/contracts/v1/manifest.json +32 -1
  20. package/contracts/v1/parity-suite.json +121 -1
  21. package/contracts/v1/schemas/backend-request.schema.json +15 -0
  22. package/contracts/v1/schemas/manifest.schema.json +6 -3
  23. package/contracts/v1/schemas/surface-profile.schema.json +166 -0
  24. package/dist/codex-chatgpt-control-backend.mjs +3198 -340
  25. package/dist/codex-chatgpt-control.bundle.mjs +4454 -1555
  26. package/dist/src/backend/client.d.ts +26 -1
  27. package/dist/src/backend/client.js +23 -1
  28. package/dist/src/backend/protocol.d.ts +1 -1
  29. package/dist/src/backend/protocol.js +11 -0
  30. package/dist/src/backend/session.js +22 -0
  31. package/dist/src/browser/attach.d.ts +1 -0
  32. package/dist/src/browser/attach.js +3 -3
  33. package/dist/src/browser/clipboard.d.ts +11 -0
  34. package/dist/src/browser/clipboard.js +34 -7
  35. package/dist/src/browser/page-state.js +17 -4
  36. package/dist/src/client.d.ts +32 -1
  37. package/dist/src/client.js +72 -16
  38. package/dist/src/commands/artifacts.js +1 -7
  39. package/dist/src/commands/configuration.d.ts +15 -0
  40. package/dist/src/commands/configuration.js +674 -0
  41. package/dist/src/commands/context.js +3 -1
  42. package/dist/src/commands/conversation.d.ts +15 -0
  43. package/dist/src/commands/conversation.js +44 -0
  44. package/dist/src/commands/deadline.d.ts +8 -0
  45. package/dist/src/commands/deadline.js +14 -0
  46. package/dist/src/commands/doctor.js +47 -3
  47. package/dist/src/commands/experience.d.ts +12 -0
  48. package/dist/src/commands/experience.js +288 -0
  49. package/dist/src/commands/files.js +61 -13
  50. package/dist/src/commands/messages.d.ts +2 -1
  51. package/dist/src/commands/messages.js +349 -87
  52. package/dist/src/commands/modes.d.ts +4 -1
  53. package/dist/src/commands/modes.js +243 -44
  54. package/dist/src/commands/probes.d.ts +15 -0
  55. package/dist/src/commands/probes.js +64 -0
  56. package/dist/src/commands/registry.js +102 -8
  57. package/dist/src/commands/reports.js +1 -1
  58. package/dist/src/commands/response-actions.js +1 -7
  59. package/dist/src/commands/sequence.js +24 -1
  60. package/dist/src/commands/session.d.ts +2 -0
  61. package/dist/src/commands/session.js +50 -1
  62. package/dist/src/commands/threads.js +3 -31
  63. package/dist/src/commands/work.d.ts +6 -0
  64. package/dist/src/commands/work.js +338 -0
  65. package/dist/src/dom/generation-state.d.ts +7 -0
  66. package/dist/src/dom/generation-state.js +7 -1
  67. package/dist/src/dom/label-match.d.ts +3 -0
  68. package/dist/src/dom/label-match.js +25 -0
  69. package/dist/src/dom/locale/am.d.ts +8 -1
  70. package/dist/src/dom/locale/am.js +8 -1
  71. package/dist/src/dom/locale/ar.d.ts +9 -1
  72. package/dist/src/dom/locale/ar.js +9 -1
  73. package/dist/src/dom/locale/bg.d.ts +9 -1
  74. package/dist/src/dom/locale/bg.js +9 -1
  75. package/dist/src/dom/locale/bn.d.ts +9 -1
  76. package/dist/src/dom/locale/bn.js +9 -1
  77. package/dist/src/dom/locale/bs.d.ts +8 -1
  78. package/dist/src/dom/locale/bs.js +8 -1
  79. package/dist/src/dom/locale/ca.d.ts +8 -1
  80. package/dist/src/dom/locale/ca.js +8 -1
  81. package/dist/src/dom/locale/cs.d.ts +8 -1
  82. package/dist/src/dom/locale/cs.js +8 -1
  83. package/dist/src/dom/locale/da.d.ts +7 -1
  84. package/dist/src/dom/locale/da.js +7 -1
  85. package/dist/src/dom/locale/de.d.ts +10 -3
  86. package/dist/src/dom/locale/de.js +10 -3
  87. package/dist/src/dom/locale/el.d.ts +8 -1
  88. package/dist/src/dom/locale/el.js +8 -1
  89. package/dist/src/dom/locale/en.d.ts +40 -1
  90. package/dist/src/dom/locale/en.js +42 -1
  91. package/dist/src/dom/locale/es-419.d.ts +8 -1
  92. package/dist/src/dom/locale/es-419.js +8 -1
  93. package/dist/src/dom/locale/es-ES.d.ts +8 -1
  94. package/dist/src/dom/locale/es-ES.js +8 -1
  95. package/dist/src/dom/locale/et.d.ts +8 -1
  96. package/dist/src/dom/locale/et.js +8 -1
  97. package/dist/src/dom/locale/fa.d.ts +9 -1
  98. package/dist/src/dom/locale/fa.js +9 -1
  99. package/dist/src/dom/locale/fi.d.ts +8 -1
  100. package/dist/src/dom/locale/fi.js +8 -1
  101. package/dist/src/dom/locale/fr-CA.d.ts +8 -1
  102. package/dist/src/dom/locale/fr-CA.js +8 -1
  103. package/dist/src/dom/locale/fr-FR.d.ts +8 -3
  104. package/dist/src/dom/locale/fr-FR.js +8 -3
  105. package/dist/src/dom/locale/gu.d.ts +8 -1
  106. package/dist/src/dom/locale/gu.js +8 -1
  107. package/dist/src/dom/locale/hi.d.ts +8 -1
  108. package/dist/src/dom/locale/hi.js +8 -1
  109. package/dist/src/dom/locale/hr.d.ts +7 -1
  110. package/dist/src/dom/locale/hr.js +7 -1
  111. package/dist/src/dom/locale/hu.d.ts +8 -1
  112. package/dist/src/dom/locale/hu.js +8 -1
  113. package/dist/src/dom/locale/hy.d.ts +9 -1
  114. package/dist/src/dom/locale/hy.js +9 -1
  115. package/dist/src/dom/locale/id.d.ts +8 -1
  116. package/dist/src/dom/locale/id.js +8 -1
  117. package/dist/src/dom/locale/index.d.ts +20 -1
  118. package/dist/src/dom/locale/index.js +102 -0
  119. package/dist/src/dom/locale/is.d.ts +8 -1
  120. package/dist/src/dom/locale/is.js +8 -1
  121. package/dist/src/dom/locale/it.d.ts +8 -1
  122. package/dist/src/dom/locale/it.js +8 -1
  123. package/dist/src/dom/locale/ja.d.ts +8 -1
  124. package/dist/src/dom/locale/ja.js +8 -1
  125. package/dist/src/dom/locale/ka.d.ts +8 -1
  126. package/dist/src/dom/locale/ka.js +8 -1
  127. package/dist/src/dom/locale/kk.d.ts +8 -1
  128. package/dist/src/dom/locale/kk.js +8 -1
  129. package/dist/src/dom/locale/kn.d.ts +9 -1
  130. package/dist/src/dom/locale/kn.js +9 -1
  131. package/dist/src/dom/locale/ko.d.ts +8 -1
  132. package/dist/src/dom/locale/ko.js +8 -1
  133. package/dist/src/dom/locale/lt.d.ts +9 -1
  134. package/dist/src/dom/locale/lt.js +9 -1
  135. package/dist/src/dom/locale/lv.d.ts +8 -1
  136. package/dist/src/dom/locale/lv.js +8 -1
  137. package/dist/src/dom/locale/mk.d.ts +7 -1
  138. package/dist/src/dom/locale/mk.js +7 -1
  139. package/dist/src/dom/locale/ml.d.ts +9 -1
  140. package/dist/src/dom/locale/ml.js +9 -1
  141. package/dist/src/dom/locale/mn.d.ts +9 -1
  142. package/dist/src/dom/locale/mn.js +9 -1
  143. package/dist/src/dom/locale/mr.d.ts +9 -1
  144. package/dist/src/dom/locale/mr.js +9 -1
  145. package/dist/src/dom/locale/ms.d.ts +8 -1
  146. package/dist/src/dom/locale/ms.js +8 -1
  147. package/dist/src/dom/locale/my.d.ts +8 -1
  148. package/dist/src/dom/locale/my.js +8 -1
  149. package/dist/src/dom/locale/nb.d.ts +8 -1
  150. package/dist/src/dom/locale/nb.js +8 -1
  151. package/dist/src/dom/locale/nl.d.ts +8 -1
  152. package/dist/src/dom/locale/nl.js +8 -1
  153. package/dist/src/dom/locale/pa.d.ts +9 -1
  154. package/dist/src/dom/locale/pa.js +9 -1
  155. package/dist/src/dom/locale/pl.d.ts +8 -1
  156. package/dist/src/dom/locale/pl.js +8 -1
  157. package/dist/src/dom/locale/pt-BR.d.ts +8 -1
  158. package/dist/src/dom/locale/pt-BR.js +8 -1
  159. package/dist/src/dom/locale/pt-PT.d.ts +8 -1
  160. package/dist/src/dom/locale/pt-PT.js +8 -1
  161. package/dist/src/dom/locale/ro.d.ts +7 -1
  162. package/dist/src/dom/locale/ro.js +7 -1
  163. package/dist/src/dom/locale/ru.d.ts +5 -1
  164. package/dist/src/dom/locale/ru.js +5 -1
  165. package/dist/src/dom/locale/sk.d.ts +8 -1
  166. package/dist/src/dom/locale/sk.js +8 -1
  167. package/dist/src/dom/locale/sl.d.ts +8 -1
  168. package/dist/src/dom/locale/sl.js +8 -1
  169. package/dist/src/dom/locale/so.d.ts +8 -1
  170. package/dist/src/dom/locale/so.js +8 -1
  171. package/dist/src/dom/locale/sq.d.ts +8 -1
  172. package/dist/src/dom/locale/sq.js +8 -1
  173. package/dist/src/dom/locale/sr.d.ts +5 -1
  174. package/dist/src/dom/locale/sr.js +5 -1
  175. package/dist/src/dom/locale/sv.d.ts +8 -1
  176. package/dist/src/dom/locale/sv.js +8 -1
  177. package/dist/src/dom/locale/sw.d.ts +8 -1
  178. package/dist/src/dom/locale/sw.js +8 -1
  179. package/dist/src/dom/locale/ta.d.ts +9 -1
  180. package/dist/src/dom/locale/ta.js +9 -1
  181. package/dist/src/dom/locale/te.d.ts +9 -1
  182. package/dist/src/dom/locale/te.js +9 -1
  183. package/dist/src/dom/locale/th.d.ts +8 -1
  184. package/dist/src/dom/locale/th.js +8 -1
  185. package/dist/src/dom/locale/tl.d.ts +4 -5
  186. package/dist/src/dom/locale/tl.js +4 -5
  187. package/dist/src/dom/locale/tr.d.ts +8 -1
  188. package/dist/src/dom/locale/tr.js +8 -1
  189. package/dist/src/dom/locale/types.d.ts +25 -1
  190. package/dist/src/dom/locale/uk.d.ts +8 -1
  191. package/dist/src/dom/locale/uk.js +8 -1
  192. package/dist/src/dom/locale/ur.d.ts +8 -1
  193. package/dist/src/dom/locale/ur.js +8 -1
  194. package/dist/src/dom/locale/vi.d.ts +8 -1
  195. package/dist/src/dom/locale/vi.js +8 -1
  196. package/dist/src/dom/locale/zh-HK.d.ts +9 -1
  197. package/dist/src/dom/locale/zh-HK.js +10 -2
  198. package/dist/src/dom/locale/zh-Hans.d.ts +9 -1
  199. package/dist/src/dom/locale/zh-Hans.js +9 -1
  200. package/dist/src/dom/locale/zh-TW.d.ts +9 -1
  201. package/dist/src/dom/locale/zh-TW.js +9 -1
  202. package/dist/src/dom/menus.js +13 -2
  203. package/dist/src/dom/selectors.js +6 -1
  204. package/dist/src/dom/wait-snapshot.d.ts +43 -0
  205. package/dist/src/dom/wait-snapshot.js +127 -0
  206. package/dist/src/index.d.ts +3 -0
  207. package/dist/src/index.js +3 -0
  208. package/dist/src/runner/responses.d.ts +3 -1
  209. package/dist/src/runner/responses.js +16 -1
  210. package/dist/src/runner/result.js +129 -4
  211. package/dist/src/runner/stream.d.ts +1 -1
  212. package/dist/src/runner/stream.js +6 -0
  213. package/dist/src/runner/types.d.ts +35 -1
  214. package/dist/src/safety/risk.d.ts +11 -0
  215. package/dist/src/safety/risk.js +11 -0
  216. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +6 -0
  217. package/dist/src/scripts/apply-intelligence-locale-captures.js +147 -15
  218. package/dist/src/scripts/capture-intelligence-locales.d.ts +3 -0
  219. package/dist/src/scripts/capture-intelligence-locales.js +372 -3
  220. package/dist/src/scripts/capture-surface-profile.d.ts +23 -0
  221. package/dist/src/scripts/capture-surface-profile.js +304 -0
  222. package/dist/src/scripts/live-smoke/scenarios.js +34 -1
  223. package/dist/src/scripts/live-smoke.js +1 -0
  224. package/dist/src/types.d.ts +273 -2
  225. package/package.json +4 -3
  226. package/references/2026-07-16-chat-work-surfaces.md +87 -0
  227. package/references/agents-runner.md +13 -2
  228. package/references/backend-protocol.md +19 -7
  229. package/references/language-coverage.md +40 -3
  230. package/references/localization.md +48 -10
  231. package/references/python-parity.md +16 -4
  232. package/references/responses-adapter.md +12 -1
  233. package/references/troubleshooting.md +49 -6
@@ -1,12 +1,16 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readPageState } from "../browser/page-state.js";
2
3
  import { resultError, resultOk } from "../errors.js";
3
- import { latestAssistantTurnHasResponseActions, readAssistantGenerationState } from "../dom/generation-state.js";
4
+ import { EMPTY_GENERATION_STATE, latestAssistantTurnHasResponseActions, readAssistantGenerationState } from "../dom/generation-state.js";
4
5
  import { countPageMessages, isTransientAssistantText, readLatestMessage, readLatestMessageText, readLatestMessageTextSnapshot, readMessages } from "../dom/messages.js";
5
- import { composerTextbox, sendButton } from "../dom/selectors.js";
6
+ import { composerTextbox, copyResponseButtons, sendButton } from "../dom/selectors.js";
7
+ import { readWaitDomSnapshot, waitTextMetadata } from "../dom/wait-snapshot.js";
6
8
  import { normalizeLineBreaks, normalizeWhitespace } from "../dom/visible-text.js";
7
9
  import { contextFromPage } from "./context.js";
10
+ import { createDeadline } from "./deadline.js";
8
11
  import { withCommandOutputText } from "./output.js";
9
- import { bootstrap } from "./session.js";
12
+ import { createSingleFlightProbe } from "./probes.js";
13
+ import { ensurePage } from "./session.js";
10
14
  export function isResponseComplete(snapshot) {
11
15
  return snapshot.latestText.trim().length > 0
12
16
  && !isTransientAssistantText(snapshot.latestText)
@@ -94,8 +98,14 @@ export async function submitMessage(env, args = {}) {
94
98
  }
95
99
  if (userTurn === undefined) {
96
100
  const latestUser = await readLatestMessage(page, "user", "normalized_text");
101
+ const generation = await readAssistantGenerationState(page).catch(() => EMPTY_GENERATION_STATE);
102
+ const turnCount = await countPageMessages(page).catch(() => undefined);
97
103
  if (submittedUserTurnMatches(latestUser?.text, args.text)) {
98
- return resultOk(submitData(latestUser?.text, await countPageMessages(page).catch(() => undefined)), await contextFromPage(page));
104
+ return resultOk(submitData(latestUser?.text, turnCount, generation.active ? "submitted_generating" : "submitted", generation), await contextFromPage(page));
105
+ }
106
+ const turnAdvanced = previousTurnCount !== undefined && turnCount !== undefined && turnCount > previousTurnCount;
107
+ if (turnAdvanced || generation.active) {
108
+ return resultOk(submitData(undefined, turnCount, generation.active ? "submitted_generating" : "submitted_unconfirmed", generation), await contextFromPage(page), ["Submitted prompt could not be matched to a rendered user turn yet, but ChatGPT page state indicates progress."]);
99
109
  }
100
110
  return {
101
111
  ok: false,
@@ -109,7 +119,7 @@ export async function submitMessage(env, args = {}) {
109
119
  context: await contextFromPage(page)
110
120
  };
111
121
  }
112
- return resultOk(submitData(userTurn, await countPageMessages(page).catch(() => undefined)), await contextFromPage(page));
122
+ return resultOk(submitData(userTurn, await countPageMessages(page).catch(() => undefined), "submitted", await readAssistantGenerationState(page).catch(() => EMPTY_GENERATION_STATE)), await contextFromPage(page));
113
123
  }
114
124
  catch (error) {
115
125
  return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
@@ -222,7 +232,16 @@ async function readVisibleTextForSubmit(page) {
222
232
  if (typeof page.evaluate !== "function") {
223
233
  return undefined;
224
234
  }
225
- return page.evaluate(() => document.body?.innerText ?? "");
235
+ // Attachment/upload status renders inside the composer form; prefer that region over
236
+ // the whole page so the not-ready poll does not serialize the full document text.
237
+ return page.evaluate(() => {
238
+ const composerForm = document.querySelector("main form") ?? document.querySelector("form");
239
+ const scopedText = composerForm?.innerText;
240
+ if (scopedText !== undefined && scopedText.trim().length > 0) {
241
+ return scopedText;
242
+ }
243
+ return document.body?.innerText ?? "";
244
+ });
226
245
  }
227
246
  async function sendTimeoutWarnings(page) {
228
247
  const state = await readSendButtonState(page).catch(() => undefined);
@@ -259,76 +278,135 @@ export async function waitForMessage(env, args = {}) {
259
278
  const stableMs = args.stableMs ?? (args.mode === "deep_research" ? 10_000 : 2_000);
260
279
  const pollMs = args.pollMs ?? 750;
261
280
  const started = Date.now();
262
- let lastTargetText = "";
281
+ const deadline = createDeadline(timeoutMs, started);
282
+ const probeTimeoutMs = Math.max(50, Math.min(1000, Math.max(pollMs, Math.floor(timeoutMs / 4))));
283
+ const waitWarnings = new Set();
284
+ // One combined DOM probe per poll: counts, latest-text metadata, generation state, and
285
+ // response actions come back in a single evaluate, sampled from the same DOM instant.
286
+ // The full assistant text never crosses the bridge during polling; it is fetched once
287
+ // at loop exit. Page-state blocker scans run on a coarser cadence below.
288
+ const waitSnapshotProbe = createSingleFlightProbe("wait snapshot", readWaitDomSnapshot);
289
+ const pageStateProbe = createSingleFlightProbe("page state", readPageState);
290
+ const PAGE_STATE_POLL_STRIDE = 4;
291
+ let pollIndex = 0;
292
+ let lastTargetKey = "";
263
293
  let lastChangedAt = Date.now();
294
+ let lastObservedTextLength = 0;
264
295
  let latestAssistantCount = await countPageMessages(page, "assistant").catch(() => 0);
296
+ let lastGeneration = EMPTY_GENERATION_STATE;
265
297
  while (Date.now() - started < timeoutMs) {
266
- const state = await readPageState(page).catch(() => undefined);
267
- if (state?.blocker !== undefined && state.blocker.kind !== "modal") {
268
- return {
269
- ok: false,
270
- status: "blocked",
271
- warnings: [],
272
- blocker: state.blocker,
273
- context: await contextFromPage(page)
274
- };
275
- }
276
- const progress = await readAssistantProgressSnapshot(page)
277
- .catch(() => fallbackAssistantProgressSnapshot(page, latestAssistantCount));
278
- latestAssistantCount = progress.assistantTurnCount;
279
- const targetReached = waitTargetReached(args, progress);
280
- const latestText = targetReached ? normalizeWhitespace(progress.latestText ?? "") : "";
281
- if (latestText !== lastTargetText) {
282
- lastTargetText = latestText;
298
+ const probeResult = await waitSnapshotProbe(page, deadline, { timeoutMs: probeTimeoutMs });
299
+ addWarnings(waitWarnings, probeResult.warnings);
300
+ const snapshot = await waitSnapshotFromProbeResult(page, probeResult, latestAssistantCount);
301
+ latestAssistantCount = snapshot.assistantTurnCount;
302
+ const targetReached = waitTargetReached(args, snapshot);
303
+ const targetKey = targetReached && snapshot.text.length > 0
304
+ ? `${snapshot.text.length}:${snapshot.text.hash}`
305
+ : "";
306
+ if (targetKey !== lastTargetKey) {
307
+ lastTargetKey = targetKey;
283
308
  lastChangedAt = Date.now();
284
309
  }
285
- const snapshot = {
286
- latestText,
287
- stableMs,
288
- textStableForMs: Date.now() - lastChangedAt,
289
- generation: await readAssistantGenerationState(page),
290
- hasResponseActions: await latestAssistantTurnHasResponseActions(page)
291
- };
292
- if (targetReached && snapshot.generation.stopped && latestText.length > 0) {
310
+ if (targetReached && snapshot.text.length > 0) {
311
+ lastObservedTextLength = snapshot.text.length;
312
+ }
313
+ if (pollIndex % PAGE_STATE_POLL_STRIDE === 0) {
314
+ const state = await pageStateFromProbe(pageStateProbe(page, deadline, { timeoutMs: probeTimeoutMs }), waitWarnings);
315
+ if (state?.blocker !== undefined && state.blocker.kind !== "modal") {
316
+ return {
317
+ ok: false,
318
+ status: "blocked",
319
+ warnings: [...waitWarnings],
320
+ blocker: state.blocker,
321
+ context: await contextFromPage(page)
322
+ };
323
+ }
324
+ }
325
+ pollIndex += 1;
326
+ const hasResponseActions = await resolveResponseActions(page, snapshot);
327
+ lastGeneration = snapshot.generation;
328
+ if (targetReached && snapshot.generation.stopped && snapshot.text.length > 0) {
329
+ const stoppedText = normalizeWhitespace(await fetchLatestAssistantText(page) ?? "");
330
+ // A failed re-read must not fabricate an empty capture: real text was observed in
331
+ // the snapshot, so omit response content entirely and tell the caller how to get it.
332
+ const data = stoppedText.length > 0
333
+ ? waitDataFromText(args, false, stoppedText, latestAssistantCount, Date.now() - started)
334
+ : waitDataWithoutText(latestAssistantCount, Date.now() - started);
335
+ data.completionState = "stopped";
336
+ data.generationActive = snapshot.generation.active;
337
+ data.generationSignals = snapshot.generation.signals;
338
+ if (stoppedText.length === 0) {
339
+ waitWarnings.add(`The interrupted assistant text (~${snapshot.text.length} chars observed) could not be re-read at wait exit; call messages.readLatest on the same thread to capture it.`);
340
+ }
293
341
  return withCommandOutputText({
294
342
  ok: false,
295
343
  status: "partial",
296
- data: {
297
- complete: false,
298
- responseText: latestText,
299
- assistantTurnCount: latestAssistantCount,
300
- elapsedMs: Date.now() - started
301
- },
344
+ data,
302
345
  warnings: [
346
+ ...waitWarnings,
347
+ ...(stoppedText.length > 0 ? responseContentWarnings(args, false) : []),
303
348
  "ChatGPT generation appears to have been stopped or interrupted before completion.",
304
349
  ...snapshot.generation.signals.map(signal => `Generation state signal: ${signal}`)
305
350
  ],
306
351
  context: await contextFromPage(page)
307
352
  });
308
353
  }
309
- if (targetReached && isResponseComplete(snapshot)) {
310
- return withCommandOutputText(resultOk({ complete: true, responseText: latestText, assistantTurnCount: latestAssistantCount, elapsedMs: Date.now() - started }, await contextFromPage(page)));
354
+ const metadataComplete = targetReached
355
+ && snapshot.text.length > 0
356
+ && !snapshot.text.transient
357
+ && Date.now() - lastChangedAt >= stableMs
358
+ && !snapshot.generation.active
359
+ && !snapshot.generation.stopped
360
+ && hasResponseActions;
361
+ if (metadataComplete) {
362
+ // Fetch the text once and confirm it still matches the stable snapshot before
363
+ // declaring completion; a hash mismatch means the answer moved on mid-fetch.
364
+ const latestText = normalizeWhitespace(await fetchLatestAssistantText(page) ?? "");
365
+ const fetchedMetadata = waitTextMetadata(latestText);
366
+ const completionSnapshot = {
367
+ latestText,
368
+ stableMs,
369
+ textStableForMs: Date.now() - lastChangedAt,
370
+ generation: snapshot.generation,
371
+ hasResponseActions
372
+ };
373
+ if (fetchedMetadata.hash === snapshot.text.hash && isResponseComplete(completionSnapshot)) {
374
+ const data = waitDataFromText(args, true, latestText, latestAssistantCount, Date.now() - started);
375
+ data.completionState = "complete";
376
+ data.generationActive = false;
377
+ data.generationSignals = snapshot.generation.signals;
378
+ return withCommandOutputText(resultOk(data, await contextFromPage(page), [...waitWarnings, ...responseContentWarnings(args, true)]));
379
+ }
380
+ if (latestText.length > 0) {
381
+ lastTargetKey = `${fetchedMetadata.length}:${fetchedMetadata.hash}`;
382
+ lastChangedAt = Date.now();
383
+ lastObservedTextLength = fetchedMetadata.length;
384
+ }
311
385
  }
312
386
  await sleep(page, pollMs);
313
387
  }
314
- if (lastTargetText.length > 0) {
315
- return withCommandOutputText({
316
- ok: false,
317
- status: "partial",
318
- data: {
319
- complete: false,
320
- responseText: lastTargetText,
321
- assistantTurnCount: latestAssistantCount,
322
- elapsedMs: Date.now() - started
323
- },
324
- warnings: ["Timed out after receiving partial assistant text."],
325
- context: await contextFromPage(page)
326
- });
388
+ if (lastObservedTextLength > 0) {
389
+ const partialText = await fetchLatestAssistantText(page);
390
+ if (partialText !== undefined && normalizeWhitespace(partialText).length > 0) {
391
+ const normalizedPartialText = normalizeWhitespace(partialText);
392
+ const data = waitDataFromText(args, false, normalizedPartialText, latestAssistantCount, Date.now() - started);
393
+ data.completionState = completionStateFromGeneration(lastGeneration, false, normalizedPartialText.length > 0);
394
+ data.generationActive = lastGeneration.active;
395
+ data.generationSignals = lastGeneration.signals;
396
+ return withCommandOutputText({
397
+ ok: false,
398
+ status: "partial",
399
+ data,
400
+ warnings: [...waitWarnings, ...responseContentWarnings(args, false), "Timed out after receiving partial assistant text."],
401
+ context: await contextFromPage(page)
402
+ });
403
+ }
404
+ waitWarnings.add(`Partial assistant text (${lastObservedTextLength} chars) was observed during polling but could not be re-read at wait exit.`);
327
405
  }
328
406
  return {
329
407
  ok: false,
330
408
  status: "timeout",
331
- warnings: [],
409
+ warnings: [...waitWarnings],
332
410
  error: {
333
411
  name: "WaitTimeout",
334
412
  message: "No assistant response appeared before the timeout.",
@@ -337,6 +415,107 @@ export async function waitForMessage(env, args = {}) {
337
415
  context: await contextFromPage(page)
338
416
  };
339
417
  }
418
+ async function fetchLatestAssistantText(page) {
419
+ const first = await readLatestMessageText(page, "assistant").catch(() => undefined);
420
+ if (first !== undefined) {
421
+ return first;
422
+ }
423
+ // One retry: exit-time reads race DOM reflow/navigation, and a transient failure here
424
+ // would otherwise discard an answer the polling snapshots proved exists.
425
+ await sleep(page, 150);
426
+ return readLatestMessageText(page, "assistant").catch(() => undefined);
427
+ }
428
+ function waitDataWithoutText(assistantTurnCount, elapsedMs) {
429
+ return { complete: false, assistantTurnCount, elapsedMs };
430
+ }
431
+ async function resolveResponseActions(page, snapshot) {
432
+ if (snapshot.hasResponseActions !== undefined) {
433
+ return snapshot.hasResponseActions;
434
+ }
435
+ // No conversation-turn markers: fall back to the structural copy-button locator, as
436
+ // the standalone response-actions probe does.
437
+ try {
438
+ const copyButtons = copyResponseButtons(page);
439
+ const count = await copyButtons.count?.();
440
+ if (count !== undefined) {
441
+ return count > 0;
442
+ }
443
+ return await copyButtons.isVisible?.() === true;
444
+ }
445
+ catch {
446
+ return false;
447
+ }
448
+ }
449
+ async function waitSnapshotFromProbeResult(page, result, previousAssistantTurnCount) {
450
+ if (result.ok && result.value !== undefined) {
451
+ return result.value;
452
+ }
453
+ if (!result.ok && (result.timedOut === true || result.skipped === true)) {
454
+ return {
455
+ turnCount: 0,
456
+ assistantTurnCount: previousAssistantTurnCount,
457
+ text: waitTextMetadata(""),
458
+ generation: EMPTY_GENERATION_STATE,
459
+ hasResponseActions: false
460
+ };
461
+ }
462
+ return fallbackWaitSnapshot(page, previousAssistantTurnCount);
463
+ }
464
+ /**
465
+ * Degraded snapshot for pages where the combined evaluate is unavailable or failed.
466
+ * Reuses the standalone facet probes, which carry their own content/locator fallbacks;
467
+ * text metadata is computed SDK-side from the extracted text.
468
+ */
469
+ async function fallbackWaitSnapshot(page, previousAssistantTurnCount) {
470
+ const progress = await fallbackAssistantProgressSnapshot(page, previousAssistantTurnCount);
471
+ const generation = await readAssistantGenerationState(page).catch(() => EMPTY_GENERATION_STATE);
472
+ const hasResponseActions = await latestAssistantTurnHasResponseActions(page).catch(() => false);
473
+ const snapshot = {
474
+ turnCount: progress.turnCount ?? 0,
475
+ assistantTurnCount: progress.assistantTurnCount,
476
+ text: waitTextMetadata(progress.latestText),
477
+ generation,
478
+ hasResponseActions
479
+ };
480
+ if (progress.latestAssistantTurnIndex !== undefined) {
481
+ snapshot.latestAssistantTurnIndex = progress.latestAssistantTurnIndex;
482
+ }
483
+ return snapshot;
484
+ }
485
+ function waitDataFromText(args, complete, responseText, assistantTurnCount, elapsedMs) {
486
+ const data = {
487
+ complete,
488
+ assistantTurnCount,
489
+ elapsedMs
490
+ };
491
+ if (args.responseContent === "metadata") {
492
+ data.responseContent = "metadata";
493
+ data.responseChars = responseText.length;
494
+ data.responseSha256 = createHash("sha256").update(responseText).digest("hex");
495
+ return data;
496
+ }
497
+ data.responseText = responseText;
498
+ return data;
499
+ }
500
+ function responseContentWarnings(args, complete) {
501
+ if (args.responseContent !== "metadata")
502
+ return [];
503
+ return [
504
+ complete
505
+ ? "Assistant response text was omitted because responseContent is metadata; call readLatest to capture the completed answer."
506
+ : "Partial assistant text was omitted because responseContent is metadata; call wait again on the same thread or readLatest after completion."
507
+ ];
508
+ }
509
+ async function pageStateFromProbe(probe, warnings) {
510
+ const result = await probe;
511
+ addWarnings(warnings, result.warnings);
512
+ return result.ok ? result.value : undefined;
513
+ }
514
+ function addWarnings(target, warnings) {
515
+ for (const warning of warnings) {
516
+ target.add(warning);
517
+ }
518
+ }
340
519
  export async function readLatest(env, args = {}) {
341
520
  const boot = await ensurePage(env);
342
521
  if (!boot.ok) {
@@ -391,9 +570,51 @@ export async function readLatest(env, args = {}) {
391
570
  data.thoughtDurationText = latest.thoughtDurationText;
392
571
  if (latest.sourcesAvailable !== undefined)
393
572
  data.sourcesAvailable = latest.sourcesAvailable;
573
+ if (role === "assistant") {
574
+ const generation = await readAssistantGenerationState(page).catch(() => EMPTY_GENERATION_STATE);
575
+ data.completionState = completionStateFromGeneration(generation, undefined, latest.text.trim().length > 0);
576
+ data.generationActive = generation.active;
577
+ data.generationSignals = generation.signals;
578
+ }
394
579
  return withCommandOutputText(resultOk(data, await contextFromPage(page), data.warnings ?? []));
395
580
  }
581
+ export async function messageStatus(env, args = {}) {
582
+ const boot = await ensurePage(env);
583
+ if (!boot.ok) {
584
+ return boot;
585
+ }
586
+ const page = env.page;
587
+ // Reuse the combined wait-snapshot evaluate for counts and generation state in a single
588
+ // round trip; it never carries full assistant text, so a bounded preview is fetched
589
+ // separately below only when the snapshot proves an assistant turn has text.
590
+ const snapshot = await readWaitDomSnapshot(page).catch(() => undefined) ?? await fallbackWaitSnapshot(page, 0);
591
+ const maxPreviewChars = Math.max(0, args.maxPreviewChars ?? 240);
592
+ const latestText = snapshot.text.length > 0
593
+ ? normalizeWhitespace(await fetchLatestAssistantText(page) ?? "")
594
+ : "";
595
+ const data = {
596
+ turnCount: snapshot.turnCount,
597
+ assistantTurnCount: snapshot.assistantTurnCount,
598
+ completionState: completionStateFromGeneration(snapshot.generation, undefined, latestText.length > 0),
599
+ generationActive: snapshot.generation.active,
600
+ generationSignals: snapshot.generation.signals
601
+ };
602
+ if (snapshot.latestAssistantTurnIndex !== undefined)
603
+ data.latestAssistantTurnIndex = snapshot.latestAssistantTurnIndex;
604
+ if (latestText.length > 0) {
605
+ data.latestAssistantTextLength = latestText.length;
606
+ data.latestAssistantPreview = latestText.length > maxPreviewChars
607
+ ? `${latestText.slice(0, Math.max(0, maxPreviewChars - 1))}...`
608
+ : latestText;
609
+ }
610
+ return resultOk(data, await contextFromPage(page));
611
+ }
396
612
  export async function askMessage(env, args) {
613
+ const normalizedPrompt = normalizeAskPrompt(args);
614
+ if (!normalizedPrompt.ok) {
615
+ return normalizedPrompt.result;
616
+ }
617
+ const prompt = normalizedPrompt.text;
397
618
  const boot = await ensurePage(env);
398
619
  if (!boot.ok) {
399
620
  return boot;
@@ -401,7 +622,7 @@ export async function askMessage(env, args) {
401
622
  const page = env.page;
402
623
  const beforeTurnCount = await countPageMessages(page).catch(() => undefined);
403
624
  const beforeAssistantTurnCount = await countPageMessages(page, "assistant").catch(() => undefined);
404
- const composeArgs = { text: args.text, mode: "replace" };
625
+ const composeArgs = { text: prompt, mode: "replace" };
405
626
  if (args.timeoutMs !== undefined) {
406
627
  composeArgs.timeoutMs = args.timeoutMs;
407
628
  }
@@ -409,7 +630,7 @@ export async function askMessage(env, args) {
409
630
  if (!compose.ok) {
410
631
  return forwardFailure(compose);
411
632
  }
412
- const submitArgs = { text: args.text };
633
+ const submitArgs = { text: prompt };
413
634
  if (beforeTurnCount !== undefined) {
414
635
  submitArgs.previousTurnCount = beforeTurnCount;
415
636
  }
@@ -466,7 +687,7 @@ export async function askMessage(env, args) {
466
687
  return forwardFailure(waitFailure);
467
688
  }
468
689
  const state = await readPageState(page).catch(() => undefined);
469
- const data = { prompt: args.text };
690
+ const data = { prompt };
470
691
  const complete = waitResult?.data?.complete ?? (waitResult === undefined ? undefined : false);
471
692
  if (complete !== undefined) {
472
693
  data.complete = complete;
@@ -480,6 +701,7 @@ export async function askMessage(env, args) {
480
701
  if (state?.title !== undefined) {
481
702
  data.title = state.title;
482
703
  }
704
+ applyMessageState(data, submit.data, waitResult?.data);
483
705
  if (waitFailure !== undefined) {
484
706
  data.complete = false;
485
707
  return withCommandOutputText({
@@ -517,7 +739,7 @@ export async function waitAndRead(env, args = {}) {
517
739
  }
518
740
  return forwardFailure(read);
519
741
  }
520
- const data = askReadData("", read.data?.text, wait.data?.complete);
742
+ const data = askReadData("", read.data?.text, wait.data?.complete, wait.data, read.data);
521
743
  const warnings = [...read.warnings, ...wait.warnings];
522
744
  if (!wait.ok && wait.status === "partial") {
523
745
  data.complete = false;
@@ -534,12 +756,6 @@ export async function waitAndRead(env, args = {}) {
534
756
  }
535
757
  return withCommandOutputText(resultOk(data, read.context, warnings));
536
758
  }
537
- async function ensurePage(env) {
538
- if (env.page !== undefined) {
539
- return resultOk({}, await contextFromPage(env.page));
540
- }
541
- return bootstrap(env, { preferExistingTab: true });
542
- }
543
759
  async function waitForSubmittedUserTurn(page, text, previousTurnCount, timeoutMs) {
544
760
  const started = Date.now();
545
761
  const wanted = text === undefined ? undefined : normalizeWhitespace(text);
@@ -601,27 +817,6 @@ function renderSubmittedTurnMarkdownSyntax(text, preserveFenceLanguage = true) {
601
817
  .replace(/\*([^*]+)\*/g, "$1")
602
818
  .replace(/_([^_]+)_/g, "$1");
603
819
  }
604
- async function readAssistantProgressSnapshot(page) {
605
- if (typeof page.evaluate === "function") {
606
- return page.evaluate(() => {
607
- const nodes = Array.from(document.querySelectorAll("[data-message-author-role]"));
608
- const assistantNodes = nodes.filter(node => node.getAttribute("data-message-author-role") === "assistant");
609
- const latestAssistant = assistantNodes.at(-1);
610
- const latestAssistantTurnIndex = latestAssistant === undefined ? undefined : nodes.indexOf(latestAssistant) + 1;
611
- const snapshot = {
612
- turnCount: nodes.length,
613
- assistantTurnCount: assistantNodes.length
614
- };
615
- const latestText = latestAssistant?.innerText ?? latestAssistant?.textContent ?? undefined;
616
- if (latestText !== undefined)
617
- snapshot.latestText = latestText;
618
- if (latestAssistantTurnIndex !== undefined)
619
- snapshot.latestAssistantTurnIndex = latestAssistantTurnIndex;
620
- return snapshot;
621
- });
622
- }
623
- return fallbackAssistantProgressSnapshot(page, 0);
624
- }
625
820
  async function fallbackAssistantProgressSnapshot(page, previousAssistantTurnCount) {
626
821
  const messages = await readMessages(page, { format: "normalized_text" }).catch(() => undefined);
627
822
  if (messages !== undefined) {
@@ -680,7 +875,7 @@ async function sleep(page, ms) {
680
875
  }
681
876
  await new Promise(resolve => setTimeout(resolve, ms));
682
877
  }
683
- function submitData(userTurnText, turnCount) {
878
+ function submitData(userTurnText, turnCount, submissionState, generation) {
684
879
  const data = { submitted: true };
685
880
  if (userTurnText !== undefined) {
686
881
  data.userTurnText = userTurnText;
@@ -688,9 +883,13 @@ function submitData(userTurnText, turnCount) {
688
883
  if (turnCount !== undefined) {
689
884
  data.turnCount = turnCount;
690
885
  }
886
+ data.submissionState = submissionState;
887
+ data.completionState = completionStateFromGeneration(generation);
888
+ data.generationActive = generation.active;
889
+ data.generationSignals = generation.signals;
691
890
  return data;
692
891
  }
693
- function askReadData(prompt, responseText, complete) {
892
+ function askReadData(prompt, responseText, complete, wait, read) {
694
893
  const data = { prompt };
695
894
  if (responseText !== undefined) {
696
895
  data.responseText = responseText;
@@ -698,8 +897,71 @@ function askReadData(prompt, responseText, complete) {
698
897
  if (complete !== undefined) {
699
898
  data.complete = complete;
700
899
  }
900
+ applyMessageState(data, undefined, wait, read);
701
901
  return data;
702
902
  }
903
+ function applyMessageState(data, submit, wait, read) {
904
+ if (submit?.submissionState !== undefined)
905
+ data.submissionState = submit.submissionState;
906
+ const completionState = wait?.completionState ?? read?.completionState ?? submit?.completionState;
907
+ if (completionState !== undefined)
908
+ data.completionState = completionState;
909
+ const generationActive = wait?.generationActive ?? read?.generationActive ?? submit?.generationActive;
910
+ if (generationActive !== undefined)
911
+ data.generationActive = generationActive;
912
+ const generationSignals = wait?.generationSignals ?? read?.generationSignals ?? submit?.generationSignals;
913
+ if (generationSignals !== undefined)
914
+ data.generationSignals = generationSignals;
915
+ }
916
+ function completionStateFromGeneration(generation, complete, hasPartialText = false) {
917
+ if (complete === true)
918
+ return "complete";
919
+ if (generation.active)
920
+ return "generating";
921
+ if (generation.stopped)
922
+ return "stopped";
923
+ if (complete === false || hasPartialText)
924
+ return "partial";
925
+ return "unknown";
926
+ }
927
+ function normalizeAskPrompt(args) {
928
+ const text = args.text;
929
+ const prompt = args.prompt;
930
+ if (text !== undefined && prompt !== undefined && text !== prompt) {
931
+ return {
932
+ ok: false,
933
+ result: {
934
+ ok: false,
935
+ status: "error",
936
+ warnings: [],
937
+ error: {
938
+ name: "InvalidAskArgs",
939
+ message: "messages.ask received both text and prompt with different values.",
940
+ recoverable: false
941
+ },
942
+ context: { timestamp: new Date().toISOString() }
943
+ }
944
+ };
945
+ }
946
+ const normalized = text ?? prompt;
947
+ if (normalized === undefined || normalized.trim().length === 0) {
948
+ return {
949
+ ok: false,
950
+ result: {
951
+ ok: false,
952
+ status: "error",
953
+ warnings: [],
954
+ error: {
955
+ name: "InvalidAskArgs",
956
+ message: "messages.ask requires a non-empty text or prompt argument.",
957
+ recoverable: false
958
+ },
959
+ context: { timestamp: new Date().toISOString() }
960
+ }
961
+ };
962
+ }
963
+ return { ok: true, text: normalized };
964
+ }
703
965
  function readRole(read) {
704
966
  return typeof read === "object" ? read.role : undefined;
705
967
  }
@@ -1,8 +1,11 @@
1
- import type { CommandResult, RuntimeEnv, SelectToolArgs, SetModeArgs } from "../types.js";
1
+ import type { CommandResult, GetModeArgs, RuntimeEnv, SelectToolArgs, SetModeArgs } from "../types.js";
2
2
  export declare function setMode(env: RuntimeEnv, args: SetModeArgs): Promise<CommandResult<{
3
3
  selected: string[];
4
4
  candidates: string[];
5
5
  }>>;
6
+ export declare function getMode(env: RuntimeEnv, args?: GetModeArgs): Promise<CommandResult<{
7
+ modes: string[];
8
+ }>>;
6
9
  export declare function selectTool(env: RuntimeEnv, args: SelectToolArgs): Promise<CommandResult<{
7
10
  selected?: string;
8
11
  candidates: string[];