codex-chatgpt-control 0.1.0-alpha.1 → 0.2.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 (242) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +55 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +7 -0
  4. package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
  5. package/contracts/v1/fixtures/command-descriptors.json +192 -15
  6. package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
  7. package/contracts/v1/fixtures/doctor-scenario-preflight.json +108 -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 +72 -6
  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 +5137 -488
  30. package/dist/codex-chatgpt-control.bundle.mjs +5001 -286
  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 +7 -0
  35. package/dist/src/backend/session.js +14 -0
  36. package/dist/src/browser/attach.js +128 -20
  37. package/dist/src/browser/downloads.js +2 -1
  38. package/dist/src/browser/page-state.js +6 -3
  39. package/dist/src/client.d.ts +16 -1
  40. package/dist/src/client.js +86 -7
  41. package/dist/src/commands/artifacts.d.ts +5 -0
  42. package/dist/src/commands/artifacts.js +432 -0
  43. package/dist/src/commands/context.js +5 -2
  44. package/dist/src/commands/doctor.d.ts +10 -2
  45. package/dist/src/commands/doctor.js +242 -14
  46. package/dist/src/commands/files.d.ts +2 -1
  47. package/dist/src/commands/files.js +352 -18
  48. package/dist/src/commands/messages.d.ts +2 -1
  49. package/dist/src/commands/messages.js +232 -40
  50. package/dist/src/commands/modes.js +202 -20
  51. package/dist/src/commands/project-sources.d.ts +10 -0
  52. package/dist/src/commands/project-sources.js +643 -0
  53. package/dist/src/commands/registry.js +76 -14
  54. package/dist/src/commands/reports.d.ts +9 -0
  55. package/dist/src/commands/reports.js +83 -6
  56. package/dist/src/commands/response-actions.js +33 -0
  57. package/dist/src/commands/sequence.js +14 -0
  58. package/dist/src/commands/threads.js +2 -1
  59. package/dist/src/commands/timeouts.d.ts +2 -0
  60. package/dist/src/commands/timeouts.js +18 -0
  61. package/dist/src/diagnostics/blockers.d.ts +54 -0
  62. package/dist/src/diagnostics/blockers.js +299 -0
  63. package/dist/src/dom/artifacts.d.ts +7 -0
  64. package/dist/src/dom/artifacts.js +198 -0
  65. package/dist/src/dom/generation-state.d.ts +8 -0
  66. package/dist/src/dom/generation-state.js +103 -0
  67. package/dist/src/dom/locale/am.d.ts +28 -0
  68. package/dist/src/dom/locale/am.js +28 -0
  69. package/dist/src/dom/locale/ar.d.ts +28 -0
  70. package/dist/src/dom/locale/ar.js +28 -0
  71. package/dist/src/dom/locale/bg.d.ts +28 -0
  72. package/dist/src/dom/locale/bg.js +28 -0
  73. package/dist/src/dom/locale/bn.d.ts +28 -0
  74. package/dist/src/dom/locale/bn.js +28 -0
  75. package/dist/src/dom/locale/bs.d.ts +28 -0
  76. package/dist/src/dom/locale/bs.js +28 -0
  77. package/dist/src/dom/locale/ca.d.ts +28 -0
  78. package/dist/src/dom/locale/ca.js +28 -0
  79. package/dist/src/dom/locale/cs.d.ts +28 -0
  80. package/dist/src/dom/locale/cs.js +28 -0
  81. package/dist/src/dom/locale/da.d.ts +28 -0
  82. package/dist/src/dom/locale/da.js +28 -0
  83. package/dist/src/dom/locale/de.d.ts +30 -0
  84. package/dist/src/dom/locale/de.js +30 -0
  85. package/dist/src/dom/locale/el.d.ts +28 -0
  86. package/dist/src/dom/locale/el.js +28 -0
  87. package/dist/src/dom/locale/en.d.ts +50 -0
  88. package/dist/src/dom/locale/en.js +56 -0
  89. package/dist/src/dom/locale/es-419.d.ts +28 -0
  90. package/dist/src/dom/locale/es-419.js +28 -0
  91. package/dist/src/dom/locale/es-ES.d.ts +28 -0
  92. package/dist/src/dom/locale/es-ES.js +28 -0
  93. package/dist/src/dom/locale/et.d.ts +28 -0
  94. package/dist/src/dom/locale/et.js +28 -0
  95. package/dist/src/dom/locale/fa.d.ts +28 -0
  96. package/dist/src/dom/locale/fa.js +28 -0
  97. package/dist/src/dom/locale/fi.d.ts +28 -0
  98. package/dist/src/dom/locale/fi.js +28 -0
  99. package/dist/src/dom/locale/fr-CA.d.ts +29 -0
  100. package/dist/src/dom/locale/fr-CA.js +29 -0
  101. package/dist/src/dom/locale/fr-FR.d.ts +30 -0
  102. package/dist/src/dom/locale/fr-FR.js +30 -0
  103. package/dist/src/dom/locale/gu.d.ts +28 -0
  104. package/dist/src/dom/locale/gu.js +28 -0
  105. package/dist/src/dom/locale/hi.d.ts +28 -0
  106. package/dist/src/dom/locale/hi.js +28 -0
  107. package/dist/src/dom/locale/hr.d.ts +28 -0
  108. package/dist/src/dom/locale/hr.js +28 -0
  109. package/dist/src/dom/locale/hu.d.ts +28 -0
  110. package/dist/src/dom/locale/hu.js +28 -0
  111. package/dist/src/dom/locale/hy.d.ts +28 -0
  112. package/dist/src/dom/locale/hy.js +28 -0
  113. package/dist/src/dom/locale/id.d.ts +28 -0
  114. package/dist/src/dom/locale/id.js +28 -0
  115. package/dist/src/dom/locale/index.d.ts +79 -0
  116. package/dist/src/dom/locale/index.js +202 -0
  117. package/dist/src/dom/locale/is.d.ts +28 -0
  118. package/dist/src/dom/locale/is.js +28 -0
  119. package/dist/src/dom/locale/it.d.ts +27 -0
  120. package/dist/src/dom/locale/it.js +27 -0
  121. package/dist/src/dom/locale/ja.d.ts +27 -0
  122. package/dist/src/dom/locale/ja.js +27 -0
  123. package/dist/src/dom/locale/ka.d.ts +28 -0
  124. package/dist/src/dom/locale/ka.js +28 -0
  125. package/dist/src/dom/locale/kk.d.ts +28 -0
  126. package/dist/src/dom/locale/kk.js +28 -0
  127. package/dist/src/dom/locale/kn.d.ts +28 -0
  128. package/dist/src/dom/locale/kn.js +28 -0
  129. package/dist/src/dom/locale/ko.d.ts +28 -0
  130. package/dist/src/dom/locale/ko.js +28 -0
  131. package/dist/src/dom/locale/lt.d.ts +28 -0
  132. package/dist/src/dom/locale/lt.js +28 -0
  133. package/dist/src/dom/locale/lv.d.ts +28 -0
  134. package/dist/src/dom/locale/lv.js +28 -0
  135. package/dist/src/dom/locale/mk.d.ts +28 -0
  136. package/dist/src/dom/locale/mk.js +28 -0
  137. package/dist/src/dom/locale/ml.d.ts +28 -0
  138. package/dist/src/dom/locale/ml.js +28 -0
  139. package/dist/src/dom/locale/mn.d.ts +28 -0
  140. package/dist/src/dom/locale/mn.js +28 -0
  141. package/dist/src/dom/locale/mr.d.ts +28 -0
  142. package/dist/src/dom/locale/mr.js +28 -0
  143. package/dist/src/dom/locale/ms.d.ts +28 -0
  144. package/dist/src/dom/locale/ms.js +28 -0
  145. package/dist/src/dom/locale/my.d.ts +28 -0
  146. package/dist/src/dom/locale/my.js +28 -0
  147. package/dist/src/dom/locale/nb.d.ts +28 -0
  148. package/dist/src/dom/locale/nb.js +28 -0
  149. package/dist/src/dom/locale/nl.d.ts +28 -0
  150. package/dist/src/dom/locale/nl.js +28 -0
  151. package/dist/src/dom/locale/pa.d.ts +28 -0
  152. package/dist/src/dom/locale/pa.js +28 -0
  153. package/dist/src/dom/locale/pl.d.ts +28 -0
  154. package/dist/src/dom/locale/pl.js +28 -0
  155. package/dist/src/dom/locale/pt-BR.d.ts +28 -0
  156. package/dist/src/dom/locale/pt-BR.js +28 -0
  157. package/dist/src/dom/locale/pt-PT.d.ts +28 -0
  158. package/dist/src/dom/locale/pt-PT.js +28 -0
  159. package/dist/src/dom/locale/ro.d.ts +28 -0
  160. package/dist/src/dom/locale/ro.js +28 -0
  161. package/dist/src/dom/locale/ru.d.ts +28 -0
  162. package/dist/src/dom/locale/ru.js +28 -0
  163. package/dist/src/dom/locale/sk.d.ts +28 -0
  164. package/dist/src/dom/locale/sk.js +28 -0
  165. package/dist/src/dom/locale/sl.d.ts +28 -0
  166. package/dist/src/dom/locale/sl.js +28 -0
  167. package/dist/src/dom/locale/so.d.ts +28 -0
  168. package/dist/src/dom/locale/so.js +28 -0
  169. package/dist/src/dom/locale/sq.d.ts +28 -0
  170. package/dist/src/dom/locale/sq.js +28 -0
  171. package/dist/src/dom/locale/sr.d.ts +28 -0
  172. package/dist/src/dom/locale/sr.js +28 -0
  173. package/dist/src/dom/locale/sv.d.ts +28 -0
  174. package/dist/src/dom/locale/sv.js +28 -0
  175. package/dist/src/dom/locale/sw.d.ts +28 -0
  176. package/dist/src/dom/locale/sw.js +28 -0
  177. package/dist/src/dom/locale/ta.d.ts +28 -0
  178. package/dist/src/dom/locale/ta.js +28 -0
  179. package/dist/src/dom/locale/te.d.ts +28 -0
  180. package/dist/src/dom/locale/te.js +28 -0
  181. package/dist/src/dom/locale/th.d.ts +28 -0
  182. package/dist/src/dom/locale/th.js +28 -0
  183. package/dist/src/dom/locale/tl.d.ts +28 -0
  184. package/dist/src/dom/locale/tl.js +28 -0
  185. package/dist/src/dom/locale/tr.d.ts +28 -0
  186. package/dist/src/dom/locale/tr.js +28 -0
  187. package/dist/src/dom/locale/types.d.ts +45 -0
  188. package/dist/src/dom/locale/types.js +1 -0
  189. package/dist/src/dom/locale/uk.d.ts +28 -0
  190. package/dist/src/dom/locale/uk.js +28 -0
  191. package/dist/src/dom/locale/ur.d.ts +28 -0
  192. package/dist/src/dom/locale/ur.js +28 -0
  193. package/dist/src/dom/locale/vi.d.ts +28 -0
  194. package/dist/src/dom/locale/vi.js +28 -0
  195. package/dist/src/dom/locale/zh-HK.d.ts +28 -0
  196. package/dist/src/dom/locale/zh-HK.js +28 -0
  197. package/dist/src/dom/locale/zh-Hans.d.ts +28 -0
  198. package/dist/src/dom/locale/zh-Hans.js +28 -0
  199. package/dist/src/dom/locale/zh-TW.d.ts +29 -0
  200. package/dist/src/dom/locale/zh-TW.js +29 -0
  201. package/dist/src/dom/locale-labels.d.ts +26 -0
  202. package/dist/src/dom/locale-labels.js +26 -0
  203. package/dist/src/dom/menus.d.ts +6 -0
  204. package/dist/src/dom/menus.js +34 -8
  205. package/dist/src/dom/message-format.d.ts +2 -1
  206. package/dist/src/dom/message-format.js +67 -10
  207. package/dist/src/dom/messages.d.ts +2 -1
  208. package/dist/src/dom/messages.js +2 -4
  209. package/dist/src/dom/selectors.d.ts +1 -0
  210. package/dist/src/dom/selectors.js +42 -15
  211. package/dist/src/index.d.ts +5 -0
  212. package/dist/src/index.js +5 -0
  213. package/dist/src/platform/local-paths.d.ts +4 -0
  214. package/dist/src/platform/local-paths.js +20 -0
  215. package/dist/src/runner/interruptions.js +15 -4
  216. package/dist/src/runner/responses.js +18 -1
  217. package/dist/src/runner/result.js +15 -0
  218. package/dist/src/runner/resume.js +1 -0
  219. package/dist/src/runner/types.d.ts +3 -0
  220. package/dist/src/safety/risk.d.ts +7 -0
  221. package/dist/src/safety/risk.js +7 -0
  222. package/dist/src/safety/untrusted-output.d.ts +73 -0
  223. package/dist/src/safety/untrusted-output.js +199 -0
  224. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +1 -0
  225. package/dist/src/scripts/apply-intelligence-locale-captures.js +184 -0
  226. package/dist/src/scripts/capture-intelligence-locales.d.ts +25 -0
  227. package/dist/src/scripts/capture-intelligence-locales.js +669 -0
  228. package/dist/src/scripts/check-surface-drift.d.ts +1 -0
  229. package/dist/src/scripts/check-surface-drift.js +11 -0
  230. package/dist/src/scripts/live-smoke/scenarios.js +84 -0
  231. package/dist/src/scripts/locale-capture/language-coverage.d.ts +10 -0
  232. package/dist/src/scripts/locale-capture/language-coverage.js +39 -0
  233. package/dist/src/testing/surface-drift.d.ts +66 -0
  234. package/dist/src/testing/surface-drift.js +363 -0
  235. package/dist/src/types.d.ts +201 -2
  236. package/package.json +2 -1
  237. package/references/backend-protocol.md +93 -1
  238. package/references/language-coverage.md +142 -0
  239. package/references/localization.md +247 -0
  240. package/references/python-parity.md +63 -1
  241. package/references/safety.md +2 -0
  242. package/references/troubleshooting.md +58 -0
@@ -1,7 +1,8 @@
1
1
  import { readPageState } from "../browser/page-state.js";
2
2
  import { resultError, resultOk } from "../errors.js";
3
+ import { latestAssistantTurnHasResponseActions, readAssistantGenerationState } from "../dom/generation-state.js";
3
4
  import { countPageMessages, isTransientAssistantText, readLatestMessage, readLatestMessageText, readLatestMessageTextSnapshot, readMessages } from "../dom/messages.js";
4
- import { composerTextbox, copyResponseButtons, sendButton } from "../dom/selectors.js";
5
+ import { composerTextbox, sendButton } from "../dom/selectors.js";
5
6
  import { normalizeLineBreaks, normalizeWhitespace } from "../dom/visible-text.js";
6
7
  import { contextFromPage } from "./context.js";
7
8
  import { withCommandOutputText } from "./output.js";
@@ -10,7 +11,8 @@ export function isResponseComplete(snapshot) {
10
11
  return snapshot.latestText.trim().length > 0
11
12
  && !isTransientAssistantText(snapshot.latestText)
12
13
  && snapshot.textStableForMs >= snapshot.stableMs
13
- && !snapshot.hasStopButton
14
+ && !snapshot.generation.active
15
+ && !snapshot.generation.stopped
14
16
  && snapshot.hasResponseActions;
15
17
  }
16
18
  export async function composeMessage(env, args) {
@@ -55,13 +57,41 @@ export async function submitMessage(env, args = {}) {
55
57
  const page = env.page;
56
58
  const previousTurnCount = args.previousTurnCount ?? await countPageMessages(page).catch(() => undefined);
57
59
  try {
58
- try {
59
- await sendButton(page).click?.();
60
+ const ready = await waitForSendButtonReady(page, args.timeoutMs ?? 30000);
61
+ if (!ready.ready) {
62
+ const blocker = {
63
+ kind: ready.code === "attachment_processing" ? "upload_failed" : "selector_drift",
64
+ code: ready.code,
65
+ message: ready.message,
66
+ remediation: [
67
+ {
68
+ label: "Wait for composer",
69
+ instruction: "Wait for ChatGPT's composer and attachments to become ready, then retry without manually changing the page.",
70
+ userActionRequired: false
71
+ }
72
+ ],
73
+ resumable: true
74
+ };
75
+ if (ready.visibleText !== undefined) {
76
+ blocker.visibleText = ready.visibleText;
77
+ }
78
+ return {
79
+ ok: false,
80
+ status: "blocked",
81
+ warnings: [],
82
+ blocker,
83
+ context: await contextFromPage(page)
84
+ };
60
85
  }
61
- catch {
62
- await page.keyboard?.press?.("Enter");
86
+ const timeoutMs = args.timeoutMs ?? 30000;
87
+ const startedAt = Date.now();
88
+ await clickSendControl(page);
89
+ let userTurn = await waitForSubmittedUserTurn(page, args.text, previousTurnCount, initialSubmitWaitMs(timeoutMs));
90
+ if (userTurn === undefined && Date.now() - startedAt < timeoutMs && await shouldRetryNoopSubmit(page, args.text)) {
91
+ await sleep(page, 250);
92
+ await clickSendControl(page);
93
+ userTurn = await waitForSubmittedUserTurn(page, args.text, previousTurnCount, Math.max(0, timeoutMs - (Date.now() - startedAt)));
63
94
  }
64
- const userTurn = await waitForSubmittedUserTurn(page, args.text, previousTurnCount, args.timeoutMs ?? 30000);
65
95
  if (userTurn === undefined) {
66
96
  const latestUser = await readLatestMessage(page, "user", "normalized_text");
67
97
  if (submittedUserTurnMatches(latestUser?.text, args.text)) {
@@ -70,7 +100,7 @@ export async function submitMessage(env, args = {}) {
70
100
  return {
71
101
  ok: false,
72
102
  status: "timeout",
73
- warnings: [],
103
+ warnings: await sendTimeoutWarnings(page),
74
104
  error: {
75
105
  name: "SubmitTimeout",
76
106
  message: "No matching submitted user turn appeared before the timeout.",
@@ -85,6 +115,140 @@ export async function submitMessage(env, args = {}) {
85
115
  return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
86
116
  }
87
117
  }
118
+ async function clickSendControl(page) {
119
+ try {
120
+ await sendButton(page).click?.();
121
+ }
122
+ catch {
123
+ await page.keyboard?.press?.("Enter");
124
+ }
125
+ }
126
+ function initialSubmitWaitMs(timeoutMs) {
127
+ return Math.min(3000, Math.max(500, Math.floor(timeoutMs / 3)));
128
+ }
129
+ async function shouldRetryNoopSubmit(page, text) {
130
+ const state = await readSendButtonState(page).catch(() => ({ available: false }));
131
+ if (!isSendButtonReady(state)) {
132
+ return false;
133
+ }
134
+ if (text === undefined) {
135
+ return true;
136
+ }
137
+ const composerText = await readLocatorText(composerTextbox(page)).catch(() => "");
138
+ return submittedUserTurnMatches(composerText, text);
139
+ }
140
+ async function waitForSendButtonReady(page, timeoutMs) {
141
+ const started = Date.now();
142
+ let lastState;
143
+ let lastVisibleText;
144
+ while (Date.now() - started < timeoutMs) {
145
+ const state = await readSendButtonState(page).catch(() => ({ available: true }));
146
+ lastState = state;
147
+ if (isSendButtonReady(state)) {
148
+ return { ready: true };
149
+ }
150
+ const visibleText = await readVisibleTextForSubmit(page).catch(() => undefined);
151
+ if (visibleText !== undefined && /uploading|processing|attaching|preparing|reading|scanning/i.test(visibleText)) {
152
+ lastVisibleText = visibleText.slice(0, 500);
153
+ }
154
+ await sleep(page, 250);
155
+ }
156
+ if (lastVisibleText !== undefined) {
157
+ return {
158
+ ready: false,
159
+ code: "attachment_processing",
160
+ message: "ChatGPT still appears to be processing an attachment, so the send button did not become ready.",
161
+ visibleText: lastVisibleText
162
+ };
163
+ }
164
+ return {
165
+ ready: false,
166
+ code: "send_button_not_ready",
167
+ message: `ChatGPT's send button did not become ready before timeout.${describeSendState(lastState)}`
168
+ };
169
+ }
170
+ function isSendButtonReady(state) {
171
+ if (!state.available)
172
+ return false;
173
+ if (state.visible === false)
174
+ return false;
175
+ if (state.disabled === true)
176
+ return false;
177
+ if (state.busy === true)
178
+ return false;
179
+ return true;
180
+ }
181
+ async function readSendButtonState(page) {
182
+ const locator = sendButton(page);
183
+ if (typeof locator.count === "function" && await locator.count().catch(() => 1) === 0) {
184
+ return { available: false, reason: "not_found" };
185
+ }
186
+ const visible = typeof locator.isVisible === "function" ? await locator.isVisible({ timeoutMs: 500 }).catch(() => undefined) : undefined;
187
+ if (typeof locator.evaluate !== "function") {
188
+ const state = { available: true };
189
+ if (visible !== undefined)
190
+ state.visible = visible;
191
+ return state;
192
+ }
193
+ const evaluated = await locator.evaluate(element => {
194
+ const htmlElement = element;
195
+ const button = element;
196
+ return {
197
+ disabled: button.disabled === true
198
+ || element.getAttribute("disabled") !== null
199
+ || element.getAttribute("aria-disabled") === "true"
200
+ || element.getAttribute("data-disabled") === "true",
201
+ busy: element.getAttribute("aria-busy") === "true"
202
+ || htmlElement.className.toString().toLocaleLowerCase().includes("loading"),
203
+ label: element.getAttribute("aria-label")
204
+ ?? element.getAttribute("title")
205
+ ?? htmlElement.innerText
206
+ ?? element.textContent
207
+ ?? undefined
208
+ };
209
+ });
210
+ const state = {
211
+ available: true,
212
+ disabled: evaluated.disabled,
213
+ busy: evaluated.busy
214
+ };
215
+ if (visible !== undefined)
216
+ state.visible = visible;
217
+ if (evaluated.label !== undefined)
218
+ state.label = evaluated.label;
219
+ return state;
220
+ }
221
+ async function readVisibleTextForSubmit(page) {
222
+ if (typeof page.evaluate !== "function") {
223
+ return undefined;
224
+ }
225
+ return page.evaluate(() => document.body?.innerText ?? "");
226
+ }
227
+ async function sendTimeoutWarnings(page) {
228
+ const state = await readSendButtonState(page).catch(() => undefined);
229
+ if (state === undefined || isSendButtonReady(state)) {
230
+ return [];
231
+ }
232
+ return [`Send button state after submit timeout:${describeSendState(state)}`];
233
+ }
234
+ function describeSendState(state) {
235
+ if (state === undefined)
236
+ return "";
237
+ const parts = [];
238
+ if (!state.available)
239
+ parts.push("available=false");
240
+ if (state.visible !== undefined)
241
+ parts.push(`visible=${state.visible}`);
242
+ if (state.disabled !== undefined)
243
+ parts.push(`disabled=${state.disabled}`);
244
+ if (state.busy !== undefined)
245
+ parts.push(`busy=${state.busy}`);
246
+ if (state.label !== undefined && state.label.trim().length > 0)
247
+ parts.push(`label=${JSON.stringify(state.label.trim().slice(0, 80))}`);
248
+ if (state.reason !== undefined)
249
+ parts.push(`reason=${state.reason}`);
250
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
251
+ }
88
252
  export async function waitForMessage(env, args = {}) {
89
253
  const boot = await ensurePage(env);
90
254
  if (!boot.ok) {
@@ -122,9 +286,26 @@ export async function waitForMessage(env, args = {}) {
122
286
  latestText,
123
287
  stableMs,
124
288
  textStableForMs: Date.now() - lastChangedAt,
125
- hasStopButton: await hasStopControl(page),
126
- hasResponseActions: await hasResponseActions(page)
289
+ generation: await readAssistantGenerationState(page),
290
+ hasResponseActions: await latestAssistantTurnHasResponseActions(page)
127
291
  };
292
+ if (targetReached && snapshot.generation.stopped && latestText.length > 0) {
293
+ return withCommandOutputText({
294
+ ok: false,
295
+ status: "partial",
296
+ data: {
297
+ complete: false,
298
+ responseText: latestText,
299
+ assistantTurnCount: latestAssistantCount,
300
+ elapsedMs: Date.now() - started
301
+ },
302
+ warnings: [
303
+ "ChatGPT generation appears to have been stopped or interrupted before completion.",
304
+ ...snapshot.generation.signals.map(signal => `Generation state signal: ${signal}`)
305
+ ],
306
+ context: await contextFromPage(page)
307
+ });
308
+ }
128
309
  if (targetReached && isResponseComplete(snapshot)) {
129
310
  return withCommandOutputText(resultOk({ complete: true, responseText: latestText, assistantTurnCount: latestAssistantCount, elapsedMs: Date.now() - started }, await contextFromPage(page)));
130
311
  }
@@ -182,6 +363,8 @@ export async function readLatest(env, args = {}) {
182
363
  data.source = latest.source;
183
364
  if (latest.fidelity !== undefined)
184
365
  data.fidelity = latest.fidelity;
366
+ if (latest.captureLimit !== undefined)
367
+ data.captureLimit = latest.captureLimit;
185
368
  if (latest.warnings !== undefined)
186
369
  data.warnings = latest.warnings;
187
370
  if (latest.markdown !== undefined)
@@ -249,11 +432,16 @@ export async function askMessage(env, args) {
249
432
  waitArgs.afterAssistantTurnCount = beforeAssistantTurnCount;
250
433
  }
251
434
  waitResult = await waitForMessage(env, waitArgs);
252
- if (!waitResult.ok && waitResult.status !== "partial") {
253
- if (!readRequested || readRole(args.read) === "user") {
254
- return forwardFailure(waitResult);
435
+ if (!waitResult.ok) {
436
+ if (waitResult.status === "partial") {
437
+ waitFailure = waitResult;
438
+ }
439
+ else {
440
+ if (!readRequested || readRole(args.read) === "user") {
441
+ return forwardFailure(waitResult);
442
+ }
443
+ waitFailure = waitResult;
255
444
  }
256
- waitFailure = waitResult;
257
445
  }
258
446
  }
259
447
  let responseText = waitResult?.data?.responseText;
@@ -265,6 +453,7 @@ export async function askMessage(env, args) {
265
453
  return forwardFailure(waitFailure);
266
454
  }
267
455
  responseText = read.data?.text;
456
+ warnings.push(...read.warnings);
268
457
  if (waitFailure !== undefined) {
269
458
  warnings.push(...waitFailure.warnings, `Assistant response was read after ${waitFailure.status}, but completion was not confirmed by the wait step.`);
270
459
  }
@@ -291,6 +480,19 @@ export async function askMessage(env, args) {
291
480
  if (state?.title !== undefined) {
292
481
  data.title = state.title;
293
482
  }
483
+ if (waitFailure !== undefined) {
484
+ data.complete = false;
485
+ return withCommandOutputText({
486
+ ok: false,
487
+ status: "partial",
488
+ data,
489
+ warnings: [
490
+ ...warnings,
491
+ `Assistant response was read after ${waitFailure.status}, but completion was not confirmed.`
492
+ ],
493
+ context: await contextFromPage(page)
494
+ });
495
+ }
294
496
  return withCommandOutputText(resultOk(data, await contextFromPage(page), warnings));
295
497
  }
296
498
  export async function waitAndRead(env, args = {}) {
@@ -315,7 +517,22 @@ export async function waitAndRead(env, args = {}) {
315
517
  }
316
518
  return forwardFailure(read);
317
519
  }
318
- return withCommandOutputText(resultOk(askReadData("", read.data?.text, wait.data?.complete), read.context, wait.warnings));
520
+ const data = askReadData("", read.data?.text, wait.data?.complete);
521
+ const warnings = [...read.warnings, ...wait.warnings];
522
+ if (!wait.ok && wait.status === "partial") {
523
+ data.complete = false;
524
+ return withCommandOutputText({
525
+ ok: false,
526
+ status: "partial",
527
+ data,
528
+ warnings: [
529
+ ...warnings,
530
+ "Assistant response was read after partial wait, but completion was not confirmed."
531
+ ],
532
+ context: read.context
533
+ });
534
+ }
535
+ return withCommandOutputText(resultOk(data, read.context, warnings));
319
536
  }
320
537
  async function ensurePage(env) {
321
538
  if (env.page !== undefined) {
@@ -384,31 +601,6 @@ function renderSubmittedTurnMarkdownSyntax(text, preserveFenceLanguage = true) {
384
601
  .replace(/\*([^*]+)\*/g, "$1")
385
602
  .replace(/_([^_]+)_/g, "$1");
386
603
  }
387
- async function hasStopControl(page) {
388
- if (typeof page.evaluate === "function") {
389
- return page.evaluate(() => {
390
- const text = document.body?.innerText ?? "";
391
- return /\b(stop generating|stop streaming|cancel)\b/i.test(text);
392
- }).catch(() => false);
393
- }
394
- return false;
395
- }
396
- async function hasResponseActions(page) {
397
- try {
398
- const copyButtons = copyResponseButtons(page);
399
- const count = await copyButtons.count?.();
400
- if (count !== undefined) {
401
- return count > 0;
402
- }
403
- return await copyButtons.isVisible?.() === true;
404
- }
405
- catch {
406
- if (typeof page.evaluate === "function") {
407
- return page.evaluate(() => /\b(Copy response|More actions)\b/i.test(document.body?.innerText ?? "")).catch(() => false);
408
- }
409
- return true;
410
- }
411
- }
412
604
  async function readAssistantProgressSnapshot(page) {
413
605
  if (typeof page.evaluate === "function") {
414
606
  return page.evaluate(() => {
@@ -1,11 +1,21 @@
1
1
  import { resultError, resultOk } from "../errors.js";
2
2
  import { enumerateVisibleMenuItems, findUniqueMenuItem } from "../dom/menus.js";
3
+ import { localeLabels } from "../dom/locale-labels.js";
3
4
  import { normalizeLabel, normalizeWhitespace } from "../dom/visible-text.js";
4
5
  import { contextFromPage } from "./context.js";
5
6
  import { bootstrap } from "./session.js";
6
7
  const DEFAULT_MODE_EFFORT = "Thinking";
7
- const CURRENT_MODE_LABELS = ["Latest", "Instant", "Thinking", "Extended", "Pro"];
8
- const MODE_OPENER_LABELS = [...CURRENT_MODE_LABELS.filter(label => label !== "Pro"), "Configure"];
8
+ const CURRENT_MODE_LABELS = [...localeLabels.modeLabels];
9
+ const MODE_OPENER_LABELS = [...CURRENT_MODE_LABELS.filter(label => label !== "Pro"), ...localeLabels.modeOpenerExtra];
10
+ const MODEL_VERSION_FAMILY_PATTERN = /^gpt[\s-]/i;
11
+ const MODEL_VERSION_LABEL_PATTERN = /^(?:o\d+|\d+(?:\.\d+)?)$/i;
12
+ const CANONICAL_INTELLIGENCE_ORDER = new Map([
13
+ ["instant", 0],
14
+ ["medium", 1],
15
+ ["high", 2],
16
+ ["extra high", 3],
17
+ ["pro", 4],
18
+ ]);
9
19
  export async function setMode(env, args) {
10
20
  const boot = await ensurePage(env);
11
21
  if (!boot.ok) {
@@ -14,8 +24,10 @@ export async function setMode(env, args) {
14
24
  const page = env.page;
15
25
  try {
16
26
  const requested = requestedModeLabels(args);
17
- const opened = await waitForModeMenu(page, requested, args.timeoutMs ?? 30000);
18
- if (opened.alreadySelected.length === requested.length) {
27
+ const requestedVersion = requestedModelVersion(args);
28
+ const requestedForOpening = requestedVersion === undefined ? requested : [...requested, requestedVersion];
29
+ const opened = await waitForModeMenu(page, requestedForOpening, args.timeoutMs ?? 30000);
30
+ if (requestedVersion === undefined && opened.alreadySelected.length === requested.length) {
19
31
  return resultOk({ selected: opened.alreadySelected, candidates: opened.modeButtons }, await contextFromPage(page));
20
32
  }
21
33
  if (!opened.opened) {
@@ -25,7 +37,7 @@ export async function setMode(env, args) {
25
37
  const candidates = await enumerateVisibleMenuItems(page);
26
38
  const selected = [];
27
39
  for (const item of requested) {
28
- const match = findUniqueMenuItem(candidates, item);
40
+ const match = findModeMenuItem(candidates, item);
29
41
  if (match === undefined) {
30
42
  const candidateLabels = candidates.map(candidate => candidate.label);
31
43
  return {
@@ -41,7 +53,22 @@ export async function setMode(env, args) {
41
53
  }
42
54
  selected.push(match.label);
43
55
  }
44
- return resultOk({ selected, candidates: candidates.map(candidate => candidate.label) }, await contextFromPage(page));
56
+ let candidateLabels = candidates.map(candidate => candidate.label);
57
+ if (requestedVersion !== undefined) {
58
+ const versionResult = await selectModelVersion(page, requestedVersion, candidates, args.timeoutMs ?? 30000);
59
+ candidateLabels = dedupeLabels([...candidateLabels, ...versionResult.candidates]);
60
+ if (!versionResult.selected) {
61
+ return {
62
+ ok: false,
63
+ status: "unsupported",
64
+ warnings: [],
65
+ blocker: selectorDriftBlocker(`Model version "${requestedVersion}" was not found or was ambiguous.`, candidateLabels),
66
+ context: await contextFromPage(page)
67
+ };
68
+ }
69
+ selected.push(versionResult.selected);
70
+ }
71
+ return resultOk({ selected, candidates: candidateLabels }, await contextFromPage(page));
45
72
  }
46
73
  catch (error) {
47
74
  return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
@@ -77,14 +104,23 @@ export async function selectTool(env, args) {
77
104
  }
78
105
  const page = env.page;
79
106
  try {
80
- const opened = await clickFirstUniqueButton(page, ["Add files and more", "Add files", "Add photos"]);
107
+ const opened = await clickFirstUniqueButton(page, [...localeLabels.addFilesOpenerCandidates]);
81
108
  if (!opened) {
82
109
  return selectorDrift(page, "No unique ChatGPT tool menu opener was found.");
83
110
  }
84
111
  await page.waitForTimeout?.(250);
85
112
  const candidates = await enumerateVisibleMenuItems(page);
86
- const wanted = toolLabel(args.tool);
87
- const match = findUniqueMenuItem(candidates, wanted);
113
+ const wantedCandidates = toolLabels(args.tool);
114
+ let match;
115
+ let wanted = wantedCandidates[0] ?? args.tool;
116
+ for (const candidate of wantedCandidates) {
117
+ const found = findUniqueMenuItem(candidates, candidate);
118
+ if (found !== undefined) {
119
+ match = found;
120
+ wanted = candidate;
121
+ break;
122
+ }
123
+ }
88
124
  if (match === undefined) {
89
125
  const candidateLabels = candidates.map(candidate => candidate.label);
90
126
  return {
@@ -139,6 +175,9 @@ async function clickMenuItem(page, label) {
139
175
  if (await clickModelSwitcherMenuItem(page, label)) {
140
176
  return true;
141
177
  }
178
+ if (await clickMenuItemByPointer(page, label)) {
179
+ return true;
180
+ }
142
181
  if (await clickMenuItemByDom(page, label)) {
143
182
  return true;
144
183
  }
@@ -149,6 +188,22 @@ async function clickMenuItem(page, label) {
149
188
  const textLocator = page.getByText?.(label, { exact: true });
150
189
  return clickIfUnique(textLocator);
151
190
  }
191
+ async function clickMenuItemByPointer(page, label) {
192
+ const point = await menuItemCenter(page, { label });
193
+ if (point === undefined) {
194
+ return false;
195
+ }
196
+ const pageWithPointer = page;
197
+ if (pageWithPointer.mouse?.click !== undefined) {
198
+ await pageWithPointer.mouse.click(point.x, point.y);
199
+ return true;
200
+ }
201
+ if (pageWithPointer.cua?.click !== undefined) {
202
+ await pageWithPointer.cua.click({ x: point.x, y: point.y });
203
+ return true;
204
+ }
205
+ return false;
206
+ }
152
207
  async function clickModelSwitcherMenuItem(page, label) {
153
208
  if (typeof page.evaluate !== "function" || typeof page.locator !== "function") {
154
209
  return false;
@@ -203,22 +258,36 @@ async function clickIfUnique(locator) {
203
258
  await locator.click();
204
259
  return true;
205
260
  }
206
- function toolLabel(tool) {
207
- switch (tool) {
208
- case "web_search":
209
- return "Web search";
210
- case "deep_research":
211
- return "Deep research";
212
- case "create_image":
213
- return "Create image";
214
- default:
215
- return tool;
261
+ function toolLabels(tool) {
262
+ const known = localeLabels.tools[tool];
263
+ return known !== undefined ? [...known] : [tool];
264
+ }
265
+ function findModeMenuItem(candidates, wanted) {
266
+ const exact = findUniqueMenuItem(candidates, wanted);
267
+ if (exact !== undefined) {
268
+ return exact;
269
+ }
270
+ const wantedIndex = CANONICAL_INTELLIGENCE_ORDER.get(normalizeLabel(wanted));
271
+ if (wantedIndex === undefined) {
272
+ return undefined;
216
273
  }
274
+ const intelligenceItems = candidates.filter(candidate => candidate.role === "menuitemradio"
275
+ && !MODEL_VERSION_LABEL_PATTERN.test(candidate.label)
276
+ && !MODEL_VERSION_FAMILY_PATTERN.test(candidate.label));
277
+ return intelligenceItems.length >= CANONICAL_INTELLIGENCE_ORDER.size
278
+ ? intelligenceItems[wantedIndex]
279
+ : undefined;
217
280
  }
218
281
  function requestedModeLabels(args) {
219
- const requested = [args.model, args.effort].filter((value) => value !== undefined);
282
+ const requested = [args.model, args.intelligence, args.effort].filter((value) => value !== undefined);
283
+ if (requestedModelVersion(args) !== undefined && requested.length === 0) {
284
+ return [];
285
+ }
220
286
  return requested.length > 0 ? requested : [DEFAULT_MODE_EFFORT];
221
287
  }
288
+ function requestedModelVersion(args) {
289
+ return args.modelVersion ?? args.version;
290
+ }
222
291
  function findUniqueVisibleLabel(labels, wanted) {
223
292
  const normalized = normalizeLabel(wanted);
224
293
  const exact = labels.filter(label => normalizeLabel(label) === normalized);
@@ -245,6 +314,119 @@ function findAlreadySelectedModes(visibleButtons, requested) {
245
314
  .map(label => findUniqueVisibleLabel(visibleButtons, label))
246
315
  .filter((label) => label !== undefined);
247
316
  }
317
+ async function selectModelVersion(page, requestedVersion, currentCandidates, timeoutMs) {
318
+ let candidates = await enumerateVisibleMenuItems(page);
319
+ if (!looksLikeModeMenu(candidates.map(candidate => candidate.label))) {
320
+ const opened = await waitForModeMenu(page, [requestedVersion], timeoutMs);
321
+ if (opened.opened) {
322
+ await page.waitForTimeout?.(250);
323
+ candidates = await enumerateVisibleMenuItems(page);
324
+ }
325
+ }
326
+ let exact = findExactMenuItem(candidates, requestedVersion);
327
+ if (exact !== undefined) {
328
+ return await clickMenuItem(page, exact.label)
329
+ ? { selected: exact.label, candidates: candidates.map(candidate => candidate.label) }
330
+ : { candidates: candidates.map(candidate => candidate.label) };
331
+ }
332
+ const opened = await openModelVersionSubmenu(page, currentCandidates);
333
+ candidates = await enumerateVisibleMenuItems(page);
334
+ exact = findExactMenuItem(candidates, requestedVersion);
335
+ if (!opened || exact === undefined) {
336
+ return { candidates: candidates.map(candidate => candidate.label) };
337
+ }
338
+ return await clickMenuItem(page, exact.label)
339
+ ? { selected: exact.label, candidates: candidates.map(candidate => candidate.label) }
340
+ : { candidates: candidates.map(candidate => candidate.label) };
341
+ }
342
+ async function openModelVersionSubmenu(page, candidates) {
343
+ const submenuOpeners = candidates.filter(item => item.hasPopup === true || MODEL_VERSION_FAMILY_PATTERN.test(item.label));
344
+ if (submenuOpeners.length === 0) {
345
+ return false;
346
+ }
347
+ for (const candidate of submenuOpeners) {
348
+ if (await openSubmenuByPointer(page, candidate)) {
349
+ await page.waitForTimeout?.(250);
350
+ if (await modelVersionMenuItemsAreVisible(page)) {
351
+ return true;
352
+ }
353
+ }
354
+ if (await clickMenuItem(page, candidate.label)) {
355
+ await page.waitForTimeout?.(250);
356
+ if (await modelVersionMenuItemsAreVisible(page)) {
357
+ return true;
358
+ }
359
+ }
360
+ }
361
+ return false;
362
+ }
363
+ async function openSubmenuByPointer(page, item) {
364
+ const point = await menuItemCenter(page, item);
365
+ if (point === undefined) {
366
+ return false;
367
+ }
368
+ const pageWithMouse = page;
369
+ if (pageWithMouse.mouse?.move !== undefined) {
370
+ await pageWithMouse.mouse.move(point.x, point.y);
371
+ return true;
372
+ }
373
+ if (pageWithMouse.cua?.move !== undefined) {
374
+ await pageWithMouse.cua.move({ x: point.x, y: point.y });
375
+ return true;
376
+ }
377
+ return false;
378
+ }
379
+ async function menuItemCenter(page, item, roles = ["menuitem", "menuitemradio", "option"]) {
380
+ if (typeof page.evaluate !== "function") {
381
+ return undefined;
382
+ }
383
+ const target = { label: item.label, roles };
384
+ if (item.testId !== undefined) {
385
+ target.testId = item.testId;
386
+ }
387
+ return page.evaluate((target) => {
388
+ const normalize = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
389
+ const normalizedLabel = normalize(target.label);
390
+ const roleSelector = target.roles.map(role => `[role='${role}']`).join(",");
391
+ const matches = Array.from(document.querySelectorAll(roleSelector))
392
+ .filter(node => {
393
+ const element = node;
394
+ if (target.testId !== undefined && element.getAttribute("data-testid") !== target.testId) {
395
+ return false;
396
+ }
397
+ const label = normalize(element.innerText ?? element.textContent ?? "");
398
+ if (label !== normalizedLabel) {
399
+ return false;
400
+ }
401
+ const rect = element.getBoundingClientRect();
402
+ const style = window.getComputedStyle(element);
403
+ return rect.width > 0
404
+ && rect.height > 0
405
+ && style.visibility !== "hidden"
406
+ && style.display !== "none"
407
+ && style.opacity !== "0";
408
+ });
409
+ if (matches.length !== 1)
410
+ return undefined;
411
+ const rect = matches[0].getBoundingClientRect();
412
+ return {
413
+ x: Math.round(rect.left + rect.width / 2),
414
+ y: Math.round(rect.top + rect.height / 2)
415
+ };
416
+ }, target).catch(() => undefined);
417
+ }
418
+ async function modelVersionMenuItemsAreVisible(page) {
419
+ return (await enumerateVisibleMenuItems(page))
420
+ .some(candidate => candidate.role === "menuitemradio" && MODEL_VERSION_LABEL_PATTERN.test(candidate.label));
421
+ }
422
+ function findExactMenuItem(items, wanted) {
423
+ const normalized = normalizeLabel(wanted);
424
+ const matches = items.filter(item => item.normalized === normalized);
425
+ return matches.length === 1 ? matches[0] : undefined;
426
+ }
427
+ function dedupeLabels(labels) {
428
+ return Array.from(new Set(labels));
429
+ }
248
430
  async function selectorDrift(page, message, candidates) {
249
431
  const visibleText = candidates?.join("\n") ?? await visibleButtonLabels(page);
250
432
  return {
@@ -0,0 +1,10 @@
1
+ import type { CommandResult, ProjectSource, ProjectSourcesAddArgs, ProjectSourcesAddData, ProjectSourcesAddPlanData, ProjectSourcesListArgs, ProjectSourcesListData, ProjectSourcesPlanAddArgs, ProjectSourcesUrl, RuntimeEnv } from "../types.js";
2
+ type SafeCandidate = NonNullable<NonNullable<CommandResult["blocker"]>["candidates"]>[number];
3
+ export declare function normalizeProjectSourcesUrl(value: string): ProjectSourcesUrl;
4
+ export declare function buildProjectSourceAddPlan(env: RuntimeEnv, args: ProjectSourcesPlanAddArgs): Promise<CommandResult<ProjectSourcesAddPlanData>>;
5
+ export declare function listProjectSources(env: RuntimeEnv, args: ProjectSourcesListArgs): Promise<CommandResult<ProjectSourcesListData>>;
6
+ export declare function addProjectSources(env: RuntimeEnv, args: ProjectSourcesAddArgs): Promise<CommandResult<ProjectSourcesAddData | ProjectSourcesAddPlanData>>;
7
+ export declare function diffProjectSourceNames(before: ProjectSource[], after: ProjectSource[]): ProjectSource[];
8
+ export declare function extractProjectSourcesFromHtml(html: string): ProjectSource[];
9
+ export declare function safeProjectSourceCandidatesFromHtml(html: string): SafeCandidate[];
10
+ export {};