@steipete/oracle 0.9.0 → 0.11.0

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 (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,15 +1,15 @@
1
- import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEND_BUTTON_SELECTORS, CONVERSATION_TURN_SELECTOR, STOP_BUTTON_SELECTOR, ASSISTANT_ROLE_SELECTOR, } from '../constants.js';
2
- import { delay } from '../utils.js';
3
- import { logDomFailure } from '../domDebug.js';
4
- import { buildClickDispatcher } from './domEvents.js';
5
- import { BrowserAutomationError } from '../../oracle/errors.js';
1
+ import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEND_BUTTON_SELECTORS, CONVERSATION_TURN_SELECTOR, STOP_BUTTON_SELECTOR, ASSISTANT_ROLE_SELECTOR, } from "../constants.js";
2
+ import { delay } from "../utils.js";
3
+ import { logDomFailure } from "../domDebug.js";
4
+ import { buildClickDispatcher } from "./domEvents.js";
5
+ import { BrowserAutomationError } from "../../oracle/errors.js";
6
6
  const ENTER_KEY_EVENT = {
7
- key: 'Enter',
8
- code: 'Enter',
7
+ key: "Enter",
8
+ code: "Enter",
9
9
  windowsVirtualKeyCode: 13,
10
10
  nativeVirtualKeyCode: 13,
11
11
  };
12
- const ENTER_KEY_TEXT = '\r';
12
+ const ENTER_KEY_TEXT = "\r";
13
13
  export async function submitPrompt(deps, prompt, logger) {
14
14
  const { runtime, input } = deps;
15
15
  await waitForDomReady(runtime, logger, deps.inputTimeoutMs ?? undefined);
@@ -63,8 +63,8 @@ export async function submitPrompt(deps, prompt, logger) {
63
63
  awaitPromise: true,
64
64
  });
65
65
  if (!focusResult.result?.value?.focused) {
66
- await logDomFailure(runtime, logger, 'focus-textarea');
67
- throw new Error('Failed to focus prompt textarea');
66
+ await logDomFailure(runtime, logger, "focus-textarea");
67
+ throw new Error("Failed to focus prompt textarea");
68
68
  }
69
69
  await input.insertText({ text: prompt });
70
70
  // Some pages (notably ChatGPT when subscriptions/widgets load) need a brief settle
@@ -99,12 +99,12 @@ export async function submitPrompt(deps, prompt, logger) {
99
99
  })()`,
100
100
  returnByValue: true,
101
101
  });
102
- const editorTextRaw = verification.result?.value?.editorText ?? '';
103
- const fallbackValueRaw = verification.result?.value?.fallbackValue ?? '';
104
- const activeValueRaw = verification.result?.value?.activeValue ?? '';
105
- const editorTextTrimmed = editorTextRaw?.trim?.() ?? '';
106
- const fallbackValueTrimmed = fallbackValueRaw?.trim?.() ?? '';
107
- const activeValueTrimmed = activeValueRaw?.trim?.() ?? '';
102
+ const editorTextRaw = verification.result?.value?.editorText ?? "";
103
+ const fallbackValueRaw = verification.result?.value?.fallbackValue ?? "";
104
+ const activeValueRaw = verification.result?.value?.activeValue ?? "";
105
+ const editorTextTrimmed = editorTextRaw?.trim?.() ?? "";
106
+ const fallbackValueTrimmed = fallbackValueRaw?.trim?.() ?? "";
107
+ const activeValueTrimmed = activeValueRaw?.trim?.() ?? "";
108
108
  if (!editorTextTrimmed && !fallbackValueTrimmed && !activeValueTrimmed) {
109
109
  // Learned: occasionally Input.insertText doesn't land in the editor; force textContent/value + input events.
110
110
  await runtime.evaluate({
@@ -152,16 +152,16 @@ export async function submitPrompt(deps, prompt, logger) {
152
152
  })()`,
153
153
  returnByValue: true,
154
154
  });
155
- const observedEditor = postVerification.result?.value?.editorText ?? '';
156
- const observedFallback = postVerification.result?.value?.fallbackValue ?? '';
157
- const observedActive = postVerification.result?.value?.activeValue ?? '';
155
+ const observedEditor = postVerification.result?.value?.editorText ?? "";
156
+ const observedFallback = postVerification.result?.value?.fallbackValue ?? "";
157
+ const observedActive = postVerification.result?.value?.activeValue ?? "";
158
158
  const observedLength = Math.max(observedEditor.length, observedFallback.length, observedActive.length);
159
159
  if (promptLength >= 50_000 && observedLength > 0 && observedLength < promptLength - 2_000) {
160
160
  // Learned: very large prompts can truncate silently; fail fast so we can fall back to file uploads.
161
- await logDomFailure(runtime, logger, 'prompt-too-large');
162
- throw new BrowserAutomationError('Prompt appears truncated in the composer (likely too large).', {
163
- stage: 'submit-prompt',
164
- code: 'prompt-too-large',
161
+ await logDomFailure(runtime, logger, "prompt-too-large");
162
+ throw new BrowserAutomationError("Prompt appears truncated in the composer (likely too large).", {
163
+ stage: "submit-prompt",
164
+ code: "prompt-too-large",
165
165
  promptLength,
166
166
  observedLength,
167
167
  });
@@ -169,19 +169,19 @@ export async function submitPrompt(deps, prompt, logger) {
169
169
  const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames);
170
170
  if (!clicked) {
171
171
  await input.dispatchKeyEvent({
172
- type: 'keyDown',
172
+ type: "keyDown",
173
173
  ...ENTER_KEY_EVENT,
174
174
  text: ENTER_KEY_TEXT,
175
175
  unmodifiedText: ENTER_KEY_TEXT,
176
176
  });
177
177
  await input.dispatchKeyEvent({
178
- type: 'keyUp',
178
+ type: "keyUp",
179
179
  ...ENTER_KEY_EVENT,
180
180
  });
181
- logger('Submitted prompt via Enter key');
181
+ logger("Submitted prompt via Enter key");
182
182
  }
183
183
  else {
184
- logger('Clicked send button');
184
+ logger("Clicked send button");
185
185
  }
186
186
  const commitTimeoutMs = Math.max(60_000, deps.inputTimeoutMs ?? 0);
187
187
  // Learned: the send button can succeed but the turn doesn't appear immediately; verify commit via turns/stop button.
@@ -193,46 +193,71 @@ export async function clearPromptComposer(Runtime, logger) {
193
193
  const inputSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
194
194
  const result = await Runtime.evaluate({
195
195
  expression: `(() => {
196
+ const SELECTORS = ${inputSelectorsLiteral};
196
197
  const fallback = document.querySelector(${fallbackSelectorLiteral});
197
198
  const editor = document.querySelector(${primarySelectorLiteral});
198
- const inputSelectors = ${inputSelectorsLiteral};
199
- let cleared = false;
200
- if (fallback) {
201
- fallback.value = '';
202
- fallback.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
203
- fallback.dispatchEvent(new Event('change', { bubbles: true }));
204
- cleared = true;
205
- }
206
- if (editor) {
207
- editor.textContent = '';
208
- editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
209
- cleared = true;
210
- }
211
- const nodes = inputSelectors
212
- .map((selector) => document.querySelector(selector))
213
- .filter((node) => Boolean(node));
214
- for (const node of nodes) {
215
- if (!node) continue;
216
- if (node instanceof HTMLTextAreaElement) {
217
- node.value = '';
199
+ const readValue = (node) => {
200
+ if (!node) return '';
201
+ if (node instanceof HTMLTextAreaElement || node instanceof HTMLInputElement) return node.value ?? '';
202
+ return node.innerText ?? node.textContent ?? '';
203
+ };
204
+ const dispatchClearEvents = (node) => {
205
+ try {
206
+ node.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, data: null, inputType: 'deleteContentBackward' }));
207
+ } catch {}
208
+ try {
218
209
  node.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
219
- node.dispatchEvent(new Event('change', { bubbles: true }));
220
- cleared = true;
221
- continue;
210
+ } catch {
211
+ node.dispatchEvent(new Event('input', { bubbles: true }));
212
+ }
213
+ node.dispatchEvent(new Event('change', { bubbles: true }));
214
+ };
215
+ const clearEditable = (node) => {
216
+ if (!node) return false;
217
+ try {
218
+ node.focus?.();
219
+ } catch {}
220
+ if (node instanceof HTMLTextAreaElement || node instanceof HTMLInputElement) {
221
+ node.value = '';
222
+ dispatchClearEvents(node);
223
+ return true;
222
224
  }
223
225
  if (node.isContentEditable || node.getAttribute('contenteditable') === 'true') {
226
+ try {
227
+ const selection = node.ownerDocument?.getSelection?.();
228
+ const range = node.ownerDocument?.createRange?.();
229
+ if (selection && range) {
230
+ range.selectNodeContents(node);
231
+ selection.removeAllRanges();
232
+ selection.addRange(range);
233
+ node.ownerDocument?.execCommand?.('delete', false);
234
+ }
235
+ } catch {}
224
236
  node.textContent = '';
225
- node.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
226
- cleared = true;
237
+ dispatchClearEvents(node);
238
+ return true;
227
239
  }
240
+ return false;
241
+ };
242
+ let cleared = false;
243
+ const nodes = SELECTORS
244
+ .map((selector) => document.querySelector(selector))
245
+ .filter((node) => Boolean(node));
246
+ for (const node of Array.from(new Set([fallback, editor, ...nodes])).filter(Boolean)) {
247
+ cleared = clearEditable(node) || cleared;
228
248
  }
229
- return { cleared };
249
+ const remaining = Array.from(new Set([fallback, editor, ...nodes]))
250
+ .filter(Boolean)
251
+ .map((node) => readValue(node).trim())
252
+ .filter(Boolean);
253
+ return { cleared, remaining };
230
254
  })()`,
231
255
  returnByValue: true,
232
256
  });
233
- if (!result.result?.value?.cleared) {
234
- await logDomFailure(Runtime, logger, 'clear-composer');
235
- throw new Error('Failed to clear prompt composer');
257
+ const value = result.result?.value;
258
+ if (!value?.cleared || (value.remaining?.length ?? 0) > 0) {
259
+ await logDomFailure(Runtime, logger, "clear-composer");
260
+ throw new Error("Failed to clear prompt composer");
236
261
  }
237
262
  await delay(250);
238
263
  }
@@ -265,24 +290,42 @@ function buildAttachmentReadyExpression(attachmentNames) {
265
290
  document.querySelector('form') ||
266
291
  document.body ||
267
292
  document;
268
- const match = (node, name) => (node?.textContent || '').toLowerCase().includes(name);
293
+ const labelText = (node) =>
294
+ [
295
+ node?.textContent,
296
+ node?.getAttribute?.('aria-label'),
297
+ node?.getAttribute?.('title'),
298
+ node?.getAttribute?.('data-testid'),
299
+ ]
300
+ .filter(Boolean)
301
+ .join(' ')
302
+ .toLowerCase();
303
+ const match = (node, name) => labelText(node).includes(name);
269
304
 
270
305
  // Restrict to attachment affordances; never scan generic div/span nodes (prompt text can contain the file name).
271
306
  const attachmentSelectors = [
272
307
  '[data-testid*="chip"]',
273
308
  '[data-testid*="attachment"]',
274
309
  '[data-testid*="upload"]',
275
- '[aria-label="Remove file"]',
276
- 'button[aria-label="Remove file"]',
310
+ '[data-testid*="file"]',
311
+ '[aria-label*="Remove file"]',
312
+ 'button[aria-label*="Remove file"]',
313
+ '[aria-label*="remove file"]',
314
+ 'button[aria-label*="remove file"]',
277
315
  ];
316
+ const attachmentRoots = Array.from(new Set([composer, document])).filter(Boolean);
278
317
 
279
318
  const chipsReady = names.every((name) =>
280
- Array.from(composer.querySelectorAll(attachmentSelectors.join(','))).some((node) => match(node, name)),
319
+ attachmentRoots.some((root) =>
320
+ Array.from(root.querySelectorAll(attachmentSelectors.join(','))).some((node) => match(node, name)),
321
+ ),
281
322
  );
282
323
  const inputsReady = names.every((name) =>
283
- Array.from(composer.querySelectorAll('input[type="file"]')).some((el) =>
284
- Array.from((el instanceof HTMLInputElement ? el.files : []) || []).some((file) =>
285
- file?.name?.toLowerCase?.().includes(name),
324
+ attachmentRoots.some((root) =>
325
+ Array.from(root.querySelectorAll('input[type="file"]')).some((el) =>
326
+ Array.from((el instanceof HTMLInputElement ? el.files : []) || []).some((file) =>
327
+ file?.name?.toLowerCase?.().includes(name),
328
+ ),
286
329
  ),
287
330
  ),
288
331
  );
@@ -297,28 +340,36 @@ async function attemptSendButton(Runtime, _logger, attachmentNames) {
297
340
  const script = `(() => {
298
341
  ${buildClickDispatcher()}
299
342
  const selectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
300
- let button = null;
343
+ const isVisible = (node) => {
344
+ if (!(node instanceof HTMLElement)) return false;
345
+ const rect = node.getBoundingClientRect();
346
+ if (rect.width <= 0 || rect.height <= 0) return false;
347
+ const style = window.getComputedStyle(node);
348
+ return style.display !== 'none' && style.visibility !== 'hidden';
349
+ };
350
+ const isEnabled = (node) => {
351
+ const ariaDisabled = node.getAttribute('aria-disabled');
352
+ const dataDisabled = node.getAttribute('data-disabled');
353
+ const style = window.getComputedStyle(node);
354
+ return !(
355
+ node.hasAttribute('disabled') ||
356
+ ariaDisabled === 'true' ||
357
+ dataDisabled === 'true' ||
358
+ style.pointerEvents === 'none' ||
359
+ style.display === 'none'
360
+ );
361
+ };
362
+ const candidates = [];
301
363
  for (const selector of selectors) {
302
- button = document.querySelector(selector);
303
- if (button) break;
364
+ candidates.push(...Array.from(document.querySelectorAll(selector)));
304
365
  }
366
+ const button = candidates.find((node) => isVisible(node) && isEnabled(node)) || null;
305
367
  if (!button) return 'missing';
306
- const ariaDisabled = button.getAttribute('aria-disabled');
307
- const dataDisabled = button.getAttribute('data-disabled');
308
- const style = window.getComputedStyle(button);
309
- const disabled =
310
- button.hasAttribute('disabled') ||
311
- ariaDisabled === 'true' ||
312
- dataDisabled === 'true' ||
313
- style.pointerEvents === 'none' ||
314
- style.display === 'none';
315
- // Learned: some send buttons render but are inert; only click when truly enabled.
316
- if (disabled) return 'disabled';
317
368
  // Use unified pointer/mouse sequence to satisfy React handlers.
318
369
  dispatchClickSequence(button);
319
370
  return 'clicked';
320
371
  })()`;
321
- const deadline = Date.now() + 8_000;
372
+ const deadline = Date.now() + 20_000;
322
373
  while (Date.now() < deadline) {
323
374
  const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
324
375
  if (needAttachment) {
@@ -332,14 +383,21 @@ async function attemptSendButton(Runtime, _logger, attachmentNames) {
332
383
  }
333
384
  }
334
385
  const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
335
- if (result.value === 'clicked') {
386
+ if (result.value === "clicked") {
336
387
  return true;
337
388
  }
338
- if (result.value === 'missing') {
389
+ if (result.value === "missing") {
339
390
  break;
340
391
  }
341
392
  await delay(100);
342
393
  }
394
+ if (Array.isArray(attachmentNames) && attachmentNames.length > 0) {
395
+ throw new BrowserAutomationError("Attachments never reached a clickable send button before timeout.", {
396
+ stage: "submit-prompt",
397
+ code: "attachment-send-not-ready",
398
+ attachmentNames,
399
+ });
400
+ }
343
401
  return false;
344
402
  }
345
403
  async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselineTurns) {
@@ -351,7 +409,7 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
351
409
  const stopSelectorLiteral = JSON.stringify(STOP_BUTTON_SELECTOR);
352
410
  const assistantSelectorLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
353
411
  const turnSelectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
354
- let baseline = typeof baselineTurns === 'number' && Number.isFinite(baselineTurns) && baselineTurns >= 0
412
+ let baseline = typeof baselineTurns === "number" && Number.isFinite(baselineTurns) && baselineTurns >= 0
355
413
  ? Math.floor(baselineTurns)
356
414
  : null;
357
415
  if (baseline === null) {
@@ -360,7 +418,7 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
360
418
  expression: `document.querySelectorAll(${turnSelectorLiteral}).length`,
361
419
  returnByValue: true,
362
420
  });
363
- const raw = typeof result?.value === 'number' ? result.value : Number(result?.value);
421
+ const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
364
422
  if (Number.isFinite(raw)) {
365
423
  baseline = Math.max(0, Math.floor(raw));
366
424
  }
@@ -450,15 +508,15 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
450
508
  const info = result.value;
451
509
  const turnsCount = result.value?.turnsCount;
452
510
  const matchesPrompt = Boolean(info?.lastMatched || info?.userMatched || info?.prefixMatched);
453
- const baselineUnknown = typeof info?.baseline === 'number' ? info.baseline < 0 : baselineLiteral < 0;
511
+ const baselineUnknown = typeof info?.baseline === "number" ? info.baseline < 0 : baselineLiteral < 0;
454
512
  if (matchesPrompt && (baselineUnknown || info?.hasNewTurn)) {
455
- return typeof turnsCount === 'number' && Number.isFinite(turnsCount) ? turnsCount : null;
513
+ return typeof turnsCount === "number" && Number.isFinite(turnsCount) ? turnsCount : null;
456
514
  }
457
515
  const fallbackCommit = info?.composerCleared &&
458
516
  Boolean(info?.hasNewTurn) &&
459
517
  ((info?.stopVisible ?? false) || info?.assistantVisible || info?.inConversation);
460
518
  if (fallbackCommit) {
461
- return typeof turnsCount === 'number' && Number.isFinite(turnsCount) ? turnsCount : null;
519
+ return typeof turnsCount === "number" && Number.isFinite(turnsCount) ? turnsCount : null;
462
520
  }
463
521
  await delay(100);
464
522
  }
@@ -466,20 +524,23 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
466
524
  logger(`Prompt commit check failed; latest state: ${await Runtime.evaluate({
467
525
  expression: script,
468
526
  returnByValue: true,
469
- }).then((res) => JSON.stringify(res?.result?.value)).catch(() => 'unavailable')}`);
470
- await logDomFailure(Runtime, logger, 'prompt-commit');
527
+ })
528
+ .then((res) => JSON.stringify(res?.result?.value))
529
+ .catch(() => "unavailable")}`);
530
+ await logDomFailure(Runtime, logger, "prompt-commit");
471
531
  }
472
532
  if (prompt.trim().length >= 50_000) {
473
- throw new BrowserAutomationError('Prompt did not appear in conversation before timeout (likely too large).', {
474
- stage: 'submit-prompt',
475
- code: 'prompt-too-large',
533
+ throw new BrowserAutomationError("Prompt did not appear in conversation before timeout (likely too large).", {
534
+ stage: "submit-prompt",
535
+ code: "prompt-too-large",
476
536
  promptLength: prompt.trim().length,
477
537
  timeoutMs,
478
538
  });
479
539
  }
480
- throw new Error('Prompt did not appear in conversation before timeout (send may have failed)');
540
+ throw new Error("Prompt did not appear in conversation before timeout (send may have failed)");
481
541
  }
482
542
  // biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
483
543
  export const __test__ = {
544
+ attemptSendButton,
484
545
  verifyPromptCommitted,
485
546
  };
@@ -1,9 +1,9 @@
1
- import path from 'node:path';
2
- import { FILE_INPUT_SELECTORS } from '../constants.js';
3
- import { waitForAttachmentVisible } from './attachments.js';
4
- import { delay } from '../utils.js';
5
- import { logDomFailure } from '../domDebug.js';
6
- import { transferAttachmentViaDataTransfer } from './attachmentDataTransfer.js';
1
+ import path from "node:path";
2
+ import { FILE_INPUT_SELECTORS } from "../constants.js";
3
+ import { waitForAttachmentVisible } from "./attachments.js";
4
+ import { delay } from "../utils.js";
5
+ import { logDomFailure } from "../domDebug.js";
6
+ import { transferAttachmentViaDataTransfer } from "./attachmentDataTransfer.js";
7
7
  /**
8
8
  * Upload file to remote Chrome by transferring content via CDP
9
9
  * Used when browser is on a different machine than CLI
@@ -11,7 +11,7 @@ import { transferAttachmentViaDataTransfer } from './attachmentDataTransfer.js';
11
11
  export async function uploadAttachmentViaDataTransfer(deps, attachment, logger) {
12
12
  const { runtime, dom } = deps;
13
13
  if (!dom) {
14
- throw new Error('DOM domain unavailable while uploading attachments.');
14
+ throw new Error("DOM domain unavailable while uploading attachments.");
15
15
  }
16
16
  logger(`Transferring ${path.basename(attachment.path)} to remote browser...`);
17
17
  // Find file input element
@@ -25,13 +25,13 @@ export async function uploadAttachmentViaDataTransfer(deps, attachment, logger)
25
25
  }
26
26
  }
27
27
  if (!fileInputSelector) {
28
- await logDomFailure(runtime, logger, 'file-input');
29
- throw new Error('Unable to locate ChatGPT file attachment input.');
28
+ await logDomFailure(runtime, logger, "file-input");
29
+ throw new Error("Unable to locate ChatGPT file attachment input.");
30
30
  }
31
31
  const transferResult = await transferAttachmentViaDataTransfer(runtime, attachment, fileInputSelector);
32
32
  logger(`File transferred: ${transferResult.fileName} (${transferResult.size} bytes)`);
33
33
  // Give ChatGPT a moment to process the file
34
34
  await delay(500);
35
35
  await waitForAttachmentVisible(runtime, transferResult.fileName, 10_000, logger);
36
- logger('Attachment queued');
36
+ logger("Attachment queued");
37
37
  }