codex-chatgpt-control 0.2.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 (179) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/contracts/v1/fixtures/backend-capabilities.json +1 -0
  3. package/contracts/v1/fixtures/command-descriptors.json +33 -2
  4. package/contracts/v1/fixtures/doctor-scenario-preflight.json +12 -1
  5. package/contracts/v1/fixtures/help-root.json +1 -1
  6. package/contracts/v1/fixtures/reports-create-redacted.json +1 -1
  7. package/contracts/v1/parity-suite.json +8 -1
  8. package/dist/codex-chatgpt-control-backend.mjs +1199 -159
  9. package/dist/codex-chatgpt-control.bundle.mjs +1198 -156
  10. package/dist/src/backend/protocol.d.ts +1 -1
  11. package/dist/src/backend/protocol.js +1 -0
  12. package/dist/src/backend/session.js +2 -0
  13. package/dist/src/browser/clipboard.d.ts +11 -0
  14. package/dist/src/browser/clipboard.js +34 -7
  15. package/dist/src/browser/page-state.js +12 -2
  16. package/dist/src/client.d.ts +2 -1
  17. package/dist/src/client.js +3 -2
  18. package/dist/src/commands/context.js +3 -1
  19. package/dist/src/commands/conversation.d.ts +15 -0
  20. package/dist/src/commands/conversation.js +44 -0
  21. package/dist/src/commands/deadline.d.ts +8 -0
  22. package/dist/src/commands/deadline.js +14 -0
  23. package/dist/src/commands/doctor.js +9 -2
  24. package/dist/src/commands/files.js +62 -8
  25. package/dist/src/commands/messages.js +212 -72
  26. package/dist/src/commands/modes.d.ts +4 -1
  27. package/dist/src/commands/modes.js +215 -30
  28. package/dist/src/commands/probes.d.ts +15 -0
  29. package/dist/src/commands/probes.js +64 -0
  30. package/dist/src/commands/registry.js +34 -2
  31. package/dist/src/commands/reports.js +1 -1
  32. package/dist/src/commands/threads.js +2 -24
  33. package/dist/src/dom/generation-state.d.ts +7 -0
  34. package/dist/src/dom/generation-state.js +7 -1
  35. package/dist/src/dom/label-match.d.ts +3 -0
  36. package/dist/src/dom/label-match.js +25 -0
  37. package/dist/src/dom/locale/am.d.ts +6 -0
  38. package/dist/src/dom/locale/am.js +6 -0
  39. package/dist/src/dom/locale/ar.d.ts +7 -0
  40. package/dist/src/dom/locale/ar.js +7 -0
  41. package/dist/src/dom/locale/bg.d.ts +7 -0
  42. package/dist/src/dom/locale/bg.js +7 -0
  43. package/dist/src/dom/locale/bn.d.ts +7 -0
  44. package/dist/src/dom/locale/bn.js +7 -0
  45. package/dist/src/dom/locale/bs.d.ts +6 -0
  46. package/dist/src/dom/locale/bs.js +6 -0
  47. package/dist/src/dom/locale/ca.d.ts +6 -0
  48. package/dist/src/dom/locale/ca.js +6 -0
  49. package/dist/src/dom/locale/cs.d.ts +6 -0
  50. package/dist/src/dom/locale/cs.js +6 -0
  51. package/dist/src/dom/locale/da.d.ts +5 -0
  52. package/dist/src/dom/locale/da.js +5 -0
  53. package/dist/src/dom/locale/de.d.ts +6 -0
  54. package/dist/src/dom/locale/de.js +6 -0
  55. package/dist/src/dom/locale/el.d.ts +6 -0
  56. package/dist/src/dom/locale/el.js +6 -0
  57. package/dist/src/dom/locale/en.d.ts +14 -0
  58. package/dist/src/dom/locale/en.js +15 -0
  59. package/dist/src/dom/locale/es-419.d.ts +6 -0
  60. package/dist/src/dom/locale/es-419.js +6 -0
  61. package/dist/src/dom/locale/es-ES.d.ts +6 -0
  62. package/dist/src/dom/locale/es-ES.js +6 -0
  63. package/dist/src/dom/locale/et.d.ts +6 -0
  64. package/dist/src/dom/locale/et.js +6 -0
  65. package/dist/src/dom/locale/fa.d.ts +7 -0
  66. package/dist/src/dom/locale/fa.js +7 -0
  67. package/dist/src/dom/locale/fi.d.ts +6 -0
  68. package/dist/src/dom/locale/fi.js +6 -0
  69. package/dist/src/dom/locale/fr-CA.d.ts +6 -0
  70. package/dist/src/dom/locale/fr-CA.js +6 -0
  71. package/dist/src/dom/locale/fr-FR.d.ts +5 -0
  72. package/dist/src/dom/locale/fr-FR.js +5 -0
  73. package/dist/src/dom/locale/gu.d.ts +6 -0
  74. package/dist/src/dom/locale/gu.js +6 -0
  75. package/dist/src/dom/locale/hi.d.ts +6 -0
  76. package/dist/src/dom/locale/hi.js +6 -0
  77. package/dist/src/dom/locale/hr.d.ts +5 -0
  78. package/dist/src/dom/locale/hr.js +5 -0
  79. package/dist/src/dom/locale/hu.d.ts +6 -0
  80. package/dist/src/dom/locale/hu.js +6 -0
  81. package/dist/src/dom/locale/hy.d.ts +7 -0
  82. package/dist/src/dom/locale/hy.js +7 -0
  83. package/dist/src/dom/locale/id.d.ts +6 -0
  84. package/dist/src/dom/locale/id.js +6 -0
  85. package/dist/src/dom/locale/index.d.ts +5 -1
  86. package/dist/src/dom/locale/index.js +37 -0
  87. package/dist/src/dom/locale/is.d.ts +6 -0
  88. package/dist/src/dom/locale/is.js +6 -0
  89. package/dist/src/dom/locale/it.d.ts +6 -0
  90. package/dist/src/dom/locale/it.js +6 -0
  91. package/dist/src/dom/locale/ja.d.ts +6 -0
  92. package/dist/src/dom/locale/ja.js +6 -0
  93. package/dist/src/dom/locale/ka.d.ts +6 -0
  94. package/dist/src/dom/locale/ka.js +6 -0
  95. package/dist/src/dom/locale/kk.d.ts +6 -0
  96. package/dist/src/dom/locale/kk.js +6 -0
  97. package/dist/src/dom/locale/kn.d.ts +7 -0
  98. package/dist/src/dom/locale/kn.js +7 -0
  99. package/dist/src/dom/locale/ko.d.ts +6 -0
  100. package/dist/src/dom/locale/ko.js +6 -0
  101. package/dist/src/dom/locale/lt.d.ts +7 -0
  102. package/dist/src/dom/locale/lt.js +7 -0
  103. package/dist/src/dom/locale/lv.d.ts +6 -0
  104. package/dist/src/dom/locale/lv.js +6 -0
  105. package/dist/src/dom/locale/mk.d.ts +5 -0
  106. package/dist/src/dom/locale/mk.js +5 -0
  107. package/dist/src/dom/locale/ml.d.ts +7 -0
  108. package/dist/src/dom/locale/ml.js +7 -0
  109. package/dist/src/dom/locale/mn.d.ts +7 -0
  110. package/dist/src/dom/locale/mn.js +7 -0
  111. package/dist/src/dom/locale/mr.d.ts +7 -0
  112. package/dist/src/dom/locale/mr.js +7 -0
  113. package/dist/src/dom/locale/ms.d.ts +6 -0
  114. package/dist/src/dom/locale/ms.js +6 -0
  115. package/dist/src/dom/locale/my.d.ts +6 -0
  116. package/dist/src/dom/locale/my.js +6 -0
  117. package/dist/src/dom/locale/nb.d.ts +6 -0
  118. package/dist/src/dom/locale/nb.js +6 -0
  119. package/dist/src/dom/locale/nl.d.ts +6 -0
  120. package/dist/src/dom/locale/nl.js +6 -0
  121. package/dist/src/dom/locale/pa.d.ts +7 -0
  122. package/dist/src/dom/locale/pa.js +7 -0
  123. package/dist/src/dom/locale/pl.d.ts +6 -0
  124. package/dist/src/dom/locale/pl.js +6 -0
  125. package/dist/src/dom/locale/pt-BR.d.ts +6 -0
  126. package/dist/src/dom/locale/pt-BR.js +6 -0
  127. package/dist/src/dom/locale/pt-PT.d.ts +6 -0
  128. package/dist/src/dom/locale/pt-PT.js +6 -0
  129. package/dist/src/dom/locale/ro.d.ts +5 -0
  130. package/dist/src/dom/locale/ro.js +5 -0
  131. package/dist/src/dom/locale/ru.d.ts +3 -0
  132. package/dist/src/dom/locale/ru.js +3 -0
  133. package/dist/src/dom/locale/sk.d.ts +6 -0
  134. package/dist/src/dom/locale/sk.js +6 -0
  135. package/dist/src/dom/locale/sl.d.ts +6 -0
  136. package/dist/src/dom/locale/sl.js +6 -0
  137. package/dist/src/dom/locale/so.d.ts +6 -0
  138. package/dist/src/dom/locale/so.js +6 -0
  139. package/dist/src/dom/locale/sq.d.ts +6 -0
  140. package/dist/src/dom/locale/sq.js +6 -0
  141. package/dist/src/dom/locale/sr.d.ts +3 -0
  142. package/dist/src/dom/locale/sr.js +3 -0
  143. package/dist/src/dom/locale/sv.d.ts +6 -0
  144. package/dist/src/dom/locale/sv.js +6 -0
  145. package/dist/src/dom/locale/sw.d.ts +6 -0
  146. package/dist/src/dom/locale/sw.js +6 -0
  147. package/dist/src/dom/locale/ta.d.ts +7 -0
  148. package/dist/src/dom/locale/ta.js +7 -0
  149. package/dist/src/dom/locale/te.d.ts +7 -0
  150. package/dist/src/dom/locale/te.js +7 -0
  151. package/dist/src/dom/locale/th.d.ts +6 -0
  152. package/dist/src/dom/locale/th.js +6 -0
  153. package/dist/src/dom/locale/tr.d.ts +6 -0
  154. package/dist/src/dom/locale/tr.js +6 -0
  155. package/dist/src/dom/locale/types.d.ts +8 -1
  156. package/dist/src/dom/locale/uk.d.ts +6 -0
  157. package/dist/src/dom/locale/uk.js +6 -0
  158. package/dist/src/dom/locale/ur.d.ts +6 -0
  159. package/dist/src/dom/locale/ur.js +6 -0
  160. package/dist/src/dom/locale/vi.d.ts +6 -0
  161. package/dist/src/dom/locale/vi.js +6 -0
  162. package/dist/src/dom/locale/zh-HK.d.ts +7 -0
  163. package/dist/src/dom/locale/zh-HK.js +7 -0
  164. package/dist/src/dom/locale/zh-Hans.d.ts +7 -0
  165. package/dist/src/dom/locale/zh-Hans.js +7 -0
  166. package/dist/src/dom/locale/zh-TW.d.ts +7 -0
  167. package/dist/src/dom/locale/zh-TW.js +7 -0
  168. package/dist/src/dom/menus.js +13 -2
  169. package/dist/src/dom/wait-snapshot.d.ts +43 -0
  170. package/dist/src/dom/wait-snapshot.js +127 -0
  171. package/dist/src/safety/risk.d.ts +1 -0
  172. package/dist/src/safety/risk.js +1 -0
  173. package/dist/src/scripts/apply-intelligence-locale-captures.js +99 -13
  174. package/dist/src/types.d.ts +28 -0
  175. package/package.json +2 -2
  176. package/references/backend-protocol.md +6 -4
  177. package/references/localization.md +14 -4
  178. package/references/python-parity.md +5 -3
  179. package/references/troubleshooting.md +28 -5
@@ -1,11 +1,15 @@
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";
12
+ import { createSingleFlightProbe } from "./probes.js";
9
13
  import { bootstrap } from "./session.js";
10
14
  export function isResponseComplete(snapshot) {
11
15
  return snapshot.latestText.trim().length > 0
@@ -222,7 +226,16 @@ async function readVisibleTextForSubmit(page) {
222
226
  if (typeof page.evaluate !== "function") {
223
227
  return undefined;
224
228
  }
225
- return page.evaluate(() => document.body?.innerText ?? "");
229
+ // Attachment/upload status renders inside the composer form; prefer that region over
230
+ // the whole page so the not-ready poll does not serialize the full document text.
231
+ return page.evaluate(() => {
232
+ const composerForm = document.querySelector("main form") ?? document.querySelector("form");
233
+ const scopedText = composerForm?.innerText;
234
+ if (scopedText !== undefined && scopedText.trim().length > 0) {
235
+ return scopedText;
236
+ }
237
+ return document.body?.innerText ?? "";
238
+ });
226
239
  }
227
240
  async function sendTimeoutWarnings(page) {
228
241
  const state = await readSendButtonState(page).catch(() => undefined);
@@ -259,76 +272,123 @@ export async function waitForMessage(env, args = {}) {
259
272
  const stableMs = args.stableMs ?? (args.mode === "deep_research" ? 10_000 : 2_000);
260
273
  const pollMs = args.pollMs ?? 750;
261
274
  const started = Date.now();
262
- let lastTargetText = "";
275
+ const deadline = createDeadline(timeoutMs, started);
276
+ const probeTimeoutMs = Math.max(50, Math.min(1000, Math.max(pollMs, Math.floor(timeoutMs / 4))));
277
+ const waitWarnings = new Set();
278
+ // One combined DOM probe per poll: counts, latest-text metadata, generation state, and
279
+ // response actions come back in a single evaluate, sampled from the same DOM instant.
280
+ // The full assistant text never crosses the bridge during polling; it is fetched once
281
+ // at loop exit. Page-state blocker scans run on a coarser cadence below.
282
+ const waitSnapshotProbe = createSingleFlightProbe("wait snapshot", readWaitDomSnapshot);
283
+ const pageStateProbe = createSingleFlightProbe("page state", readPageState);
284
+ const PAGE_STATE_POLL_STRIDE = 4;
285
+ let pollIndex = 0;
286
+ let lastTargetKey = "";
263
287
  let lastChangedAt = Date.now();
288
+ let lastObservedTextLength = 0;
264
289
  let latestAssistantCount = await countPageMessages(page, "assistant").catch(() => 0);
265
290
  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;
291
+ const probeResult = await waitSnapshotProbe(page, deadline, { timeoutMs: probeTimeoutMs });
292
+ addWarnings(waitWarnings, probeResult.warnings);
293
+ const snapshot = await waitSnapshotFromProbeResult(page, probeResult, latestAssistantCount);
294
+ latestAssistantCount = snapshot.assistantTurnCount;
295
+ const targetReached = waitTargetReached(args, snapshot);
296
+ const targetKey = targetReached && snapshot.text.length > 0
297
+ ? `${snapshot.text.length}:${snapshot.text.hash}`
298
+ : "";
299
+ if (targetKey !== lastTargetKey) {
300
+ lastTargetKey = targetKey;
283
301
  lastChangedAt = Date.now();
284
302
  }
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) {
303
+ if (targetReached && snapshot.text.length > 0) {
304
+ lastObservedTextLength = snapshot.text.length;
305
+ }
306
+ if (pollIndex % PAGE_STATE_POLL_STRIDE === 0) {
307
+ const state = await pageStateFromProbe(pageStateProbe(page, deadline, { timeoutMs: probeTimeoutMs }), waitWarnings);
308
+ if (state?.blocker !== undefined && state.blocker.kind !== "modal") {
309
+ return {
310
+ ok: false,
311
+ status: "blocked",
312
+ warnings: [...waitWarnings],
313
+ blocker: state.blocker,
314
+ context: await contextFromPage(page)
315
+ };
316
+ }
317
+ }
318
+ pollIndex += 1;
319
+ const hasResponseActions = await resolveResponseActions(page, snapshot);
320
+ if (targetReached && snapshot.generation.stopped && snapshot.text.length > 0) {
321
+ const stoppedText = normalizeWhitespace(await fetchLatestAssistantText(page) ?? "");
322
+ // A failed re-read must not fabricate an empty capture: real text was observed in
323
+ // the snapshot, so omit response content entirely and tell the caller how to get it.
324
+ const data = stoppedText.length > 0
325
+ ? waitDataFromText(args, false, stoppedText, latestAssistantCount, Date.now() - started)
326
+ : waitDataWithoutText(latestAssistantCount, Date.now() - started);
327
+ if (stoppedText.length === 0) {
328
+ 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.`);
329
+ }
293
330
  return withCommandOutputText({
294
331
  ok: false,
295
332
  status: "partial",
296
- data: {
297
- complete: false,
298
- responseText: latestText,
299
- assistantTurnCount: latestAssistantCount,
300
- elapsedMs: Date.now() - started
301
- },
333
+ data,
302
334
  warnings: [
335
+ ...waitWarnings,
336
+ ...(stoppedText.length > 0 ? responseContentWarnings(args, false) : []),
303
337
  "ChatGPT generation appears to have been stopped or interrupted before completion.",
304
338
  ...snapshot.generation.signals.map(signal => `Generation state signal: ${signal}`)
305
339
  ],
306
340
  context: await contextFromPage(page)
307
341
  });
308
342
  }
309
- if (targetReached && isResponseComplete(snapshot)) {
310
- return withCommandOutputText(resultOk({ complete: true, responseText: latestText, assistantTurnCount: latestAssistantCount, elapsedMs: Date.now() - started }, await contextFromPage(page)));
343
+ const metadataComplete = targetReached
344
+ && snapshot.text.length > 0
345
+ && !snapshot.text.transient
346
+ && Date.now() - lastChangedAt >= stableMs
347
+ && !snapshot.generation.active
348
+ && !snapshot.generation.stopped
349
+ && hasResponseActions;
350
+ if (metadataComplete) {
351
+ // Fetch the text once and confirm it still matches the stable snapshot before
352
+ // declaring completion; a hash mismatch means the answer moved on mid-fetch.
353
+ const latestText = normalizeWhitespace(await fetchLatestAssistantText(page) ?? "");
354
+ const fetchedMetadata = waitTextMetadata(latestText);
355
+ const completionSnapshot = {
356
+ latestText,
357
+ stableMs,
358
+ textStableForMs: Date.now() - lastChangedAt,
359
+ generation: snapshot.generation,
360
+ hasResponseActions
361
+ };
362
+ if (fetchedMetadata.hash === snapshot.text.hash && isResponseComplete(completionSnapshot)) {
363
+ const data = waitDataFromText(args, true, latestText, latestAssistantCount, Date.now() - started);
364
+ return withCommandOutputText(resultOk(data, await contextFromPage(page), [...waitWarnings, ...responseContentWarnings(args, true)]));
365
+ }
366
+ if (latestText.length > 0) {
367
+ lastTargetKey = `${fetchedMetadata.length}:${fetchedMetadata.hash}`;
368
+ lastChangedAt = Date.now();
369
+ lastObservedTextLength = fetchedMetadata.length;
370
+ }
311
371
  }
312
372
  await sleep(page, pollMs);
313
373
  }
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
- });
374
+ if (lastObservedTextLength > 0) {
375
+ const partialText = await fetchLatestAssistantText(page);
376
+ if (partialText !== undefined && normalizeWhitespace(partialText).length > 0) {
377
+ const data = waitDataFromText(args, false, normalizeWhitespace(partialText), latestAssistantCount, Date.now() - started);
378
+ return withCommandOutputText({
379
+ ok: false,
380
+ status: "partial",
381
+ data,
382
+ warnings: [...waitWarnings, ...responseContentWarnings(args, false), "Timed out after receiving partial assistant text."],
383
+ context: await contextFromPage(page)
384
+ });
385
+ }
386
+ waitWarnings.add(`Partial assistant text (${lastObservedTextLength} chars) was observed during polling but could not be re-read at wait exit.`);
327
387
  }
328
388
  return {
329
389
  ok: false,
330
390
  status: "timeout",
331
- warnings: [],
391
+ warnings: [...waitWarnings],
332
392
  error: {
333
393
  name: "WaitTimeout",
334
394
  message: "No assistant response appeared before the timeout.",
@@ -337,6 +397,107 @@ export async function waitForMessage(env, args = {}) {
337
397
  context: await contextFromPage(page)
338
398
  };
339
399
  }
400
+ async function fetchLatestAssistantText(page) {
401
+ const first = await readLatestMessageText(page, "assistant").catch(() => undefined);
402
+ if (first !== undefined) {
403
+ return first;
404
+ }
405
+ // One retry: exit-time reads race DOM reflow/navigation, and a transient failure here
406
+ // would otherwise discard an answer the polling snapshots proved exists.
407
+ await sleep(page, 150);
408
+ return readLatestMessageText(page, "assistant").catch(() => undefined);
409
+ }
410
+ function waitDataWithoutText(assistantTurnCount, elapsedMs) {
411
+ return { complete: false, assistantTurnCount, elapsedMs };
412
+ }
413
+ async function resolveResponseActions(page, snapshot) {
414
+ if (snapshot.hasResponseActions !== undefined) {
415
+ return snapshot.hasResponseActions;
416
+ }
417
+ // No conversation-turn markers: fall back to the structural copy-button locator, as
418
+ // the standalone response-actions probe does.
419
+ try {
420
+ const copyButtons = copyResponseButtons(page);
421
+ const count = await copyButtons.count?.();
422
+ if (count !== undefined) {
423
+ return count > 0;
424
+ }
425
+ return await copyButtons.isVisible?.() === true;
426
+ }
427
+ catch {
428
+ return false;
429
+ }
430
+ }
431
+ async function waitSnapshotFromProbeResult(page, result, previousAssistantTurnCount) {
432
+ if (result.ok && result.value !== undefined) {
433
+ return result.value;
434
+ }
435
+ if (!result.ok && (result.timedOut === true || result.skipped === true)) {
436
+ return {
437
+ turnCount: 0,
438
+ assistantTurnCount: previousAssistantTurnCount,
439
+ text: waitTextMetadata(""),
440
+ generation: EMPTY_GENERATION_STATE,
441
+ hasResponseActions: false
442
+ };
443
+ }
444
+ return fallbackWaitSnapshot(page, previousAssistantTurnCount);
445
+ }
446
+ /**
447
+ * Degraded snapshot for pages where the combined evaluate is unavailable or failed.
448
+ * Reuses the standalone facet probes, which carry their own content/locator fallbacks;
449
+ * text metadata is computed SDK-side from the extracted text.
450
+ */
451
+ async function fallbackWaitSnapshot(page, previousAssistantTurnCount) {
452
+ const progress = await fallbackAssistantProgressSnapshot(page, previousAssistantTurnCount);
453
+ const generation = await readAssistantGenerationState(page).catch(() => EMPTY_GENERATION_STATE);
454
+ const hasResponseActions = await latestAssistantTurnHasResponseActions(page).catch(() => false);
455
+ const snapshot = {
456
+ turnCount: progress.turnCount ?? 0,
457
+ assistantTurnCount: progress.assistantTurnCount,
458
+ text: waitTextMetadata(progress.latestText),
459
+ generation,
460
+ hasResponseActions
461
+ };
462
+ if (progress.latestAssistantTurnIndex !== undefined) {
463
+ snapshot.latestAssistantTurnIndex = progress.latestAssistantTurnIndex;
464
+ }
465
+ return snapshot;
466
+ }
467
+ function waitDataFromText(args, complete, responseText, assistantTurnCount, elapsedMs) {
468
+ const data = {
469
+ complete,
470
+ assistantTurnCount,
471
+ elapsedMs
472
+ };
473
+ if (args.responseContent === "metadata") {
474
+ data.responseContent = "metadata";
475
+ data.responseChars = responseText.length;
476
+ data.responseSha256 = createHash("sha256").update(responseText).digest("hex");
477
+ return data;
478
+ }
479
+ data.responseText = responseText;
480
+ return data;
481
+ }
482
+ function responseContentWarnings(args, complete) {
483
+ if (args.responseContent !== "metadata")
484
+ return [];
485
+ return [
486
+ complete
487
+ ? "Assistant response text was omitted because responseContent is metadata; call readLatest to capture the completed answer."
488
+ : "Partial assistant text was omitted because responseContent is metadata; call wait again on the same thread or readLatest after completion."
489
+ ];
490
+ }
491
+ async function pageStateFromProbe(probe, warnings) {
492
+ const result = await probe;
493
+ addWarnings(warnings, result.warnings);
494
+ return result.ok ? result.value : undefined;
495
+ }
496
+ function addWarnings(target, warnings) {
497
+ for (const warning of warnings) {
498
+ target.add(warning);
499
+ }
500
+ }
340
501
  export async function readLatest(env, args = {}) {
341
502
  const boot = await ensurePage(env);
342
503
  if (!boot.ok) {
@@ -601,27 +762,6 @@ function renderSubmittedTurnMarkdownSyntax(text, preserveFenceLanguage = true) {
601
762
  .replace(/\*([^*]+)\*/g, "$1")
602
763
  .replace(/_([^_]+)_/g, "$1");
603
764
  }
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
765
  async function fallbackAssistantProgressSnapshot(page, previousAssistantTurnCount) {
626
766
  const messages = await readMessages(page, { format: "normalized_text" }).catch(() => undefined);
627
767
  if (messages !== undefined) {
@@ -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[];