@steipete/oracle 0.17.3 → 0.19.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.
- package/README.md +1 -1
- package/dist/bin/oracle-cli.js +75 -64
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +2 -1
- package/dist/src/browser/actions/modelSelection.js +13 -3
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +361 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +267 -15
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +267 -43
- package/dist/src/browser/config.js +15 -3
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +447 -263
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/policies.js +6 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +88 -17
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
- package/dist/src/browser/reattach.js +14 -4
- package/dist/src/browser/sessionRunner.js +71 -11
- package/dist/src/browser/tabLeaseRegistry.js +414 -38
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +40 -15
- package/dist/src/cli/browserDefaults.js +13 -0
- package/dist/src/cli/browserTabs.js +12 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -47
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/projectSources.js +5 -3
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +9 -2
- package/dist/src/cli/sessionRunner.js +91 -7
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +97 -26
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +7 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/remote/client.js +70 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +351 -72
- package/dist/src/sessionManager.js +36 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +25 -23
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -683
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -588
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -595
- package/dist/docs-site/mcp.html +0 -368
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { withoutBrowserCancellation } from "../cancellation.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
1
3
|
import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEND_BUTTON_SELECTORS, STOP_BUTTON_SELECTOR, ASSISTANT_ROLE_SELECTOR, } from "../constants.js";
|
|
2
4
|
import { buildConversationTurnCountExpression, buildConversationTurnListExpression, } from "../conversationTurns.js";
|
|
3
5
|
import { delay } from "../utils.js";
|
|
4
6
|
import { logDomFailure } from "../domDebug.js";
|
|
7
|
+
import { assertComposerNavigationSnapshot, assertComposerPlusStayedInPlace, buildAttachmentNamePattern, composerNavigationIdentityFromUrl, } from "./attachments.js";
|
|
8
|
+
import { buildComposerNavigationValidationExpression } from "./attachmentContext.js";
|
|
5
9
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
10
|
+
import { stageAttachmentPrompt } from "./attachmentPrompt.js";
|
|
6
11
|
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
12
|
+
import { buildAttachmentEvidenceExpression } from "./attachmentEvidence.js";
|
|
13
|
+
import { buildAttachmentProgressExpression } from "./attachmentProgress.js";
|
|
7
14
|
const ENTER_KEY_EVENT = {
|
|
8
15
|
key: "Enter",
|
|
9
16
|
code: "Enter",
|
|
@@ -11,12 +18,33 @@ const ENTER_KEY_EVENT = {
|
|
|
11
18
|
nativeVirtualKeyCode: 13,
|
|
12
19
|
};
|
|
13
20
|
const ENTER_KEY_TEXT = "\r";
|
|
21
|
+
const ESCAPE_KEY_EVENT = {
|
|
22
|
+
key: "Escape",
|
|
23
|
+
code: "Escape",
|
|
24
|
+
windowsVirtualKeyCode: 27,
|
|
25
|
+
nativeVirtualKeyCode: 27,
|
|
26
|
+
};
|
|
14
27
|
export async function submitPrompt(deps, prompt, logger) {
|
|
15
28
|
const { runtime, input } = deps;
|
|
29
|
+
const hasAttachments = Array.isArray(deps.attachmentNames) && deps.attachmentNames.length > 0;
|
|
30
|
+
if (hasAttachments && !deps.attachmentNavigationUrl) {
|
|
31
|
+
throw new BrowserAutomationError("Oracle cannot safely submit attachments without the pre-upload ChatGPT page identity.", {
|
|
32
|
+
stage: "submit-prompt",
|
|
33
|
+
code: "attachment-navigation-identity-unavailable",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (hasAttachments) {
|
|
37
|
+
await assertComposerPlusStayedInPlace(runtime, deps.attachmentNavigationUrl);
|
|
38
|
+
}
|
|
16
39
|
await waitForDomReady(runtime, logger, deps.inputTimeoutMs ?? undefined);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
40
|
+
let observedLength;
|
|
41
|
+
if (hasAttachments) {
|
|
42
|
+
observedLength = await stageAttachmentPrompt(runtime, prompt, deps.attachmentNavigationUrl);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const encodedPrompt = JSON.stringify(prompt);
|
|
46
|
+
const focusResult = await runtime.evaluate({
|
|
47
|
+
expression: `(() => {
|
|
20
48
|
${buildClickDispatcher()}
|
|
21
49
|
const SELECTORS = ${JSON.stringify(INPUT_SELECTORS)};
|
|
22
50
|
const isVisible = (node) => {
|
|
@@ -60,21 +88,21 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
60
88
|
}
|
|
61
89
|
return { focused: false };
|
|
62
90
|
})()`,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
returnByValue: true,
|
|
92
|
+
awaitPromise: true,
|
|
93
|
+
});
|
|
94
|
+
if (!focusResult.result?.value?.focused) {
|
|
95
|
+
await logDomFailure(runtime, logger, "focus-textarea");
|
|
96
|
+
throw new Error("Failed to focus prompt textarea");
|
|
97
|
+
}
|
|
98
|
+
await input.insertText({ text: prompt });
|
|
99
|
+
// Some pages (notably ChatGPT when subscriptions/widgets load) need a brief settle
|
|
100
|
+
// before the send button becomes enabled; give it a short breather to avoid races.
|
|
101
|
+
await delay(500);
|
|
102
|
+
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
103
|
+
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
104
|
+
const verification = await runtime.evaluate({
|
|
105
|
+
expression: `(() => {
|
|
78
106
|
const editor = document.querySelector(${primarySelectorLiteral});
|
|
79
107
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
80
108
|
const inputSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
@@ -98,18 +126,18 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
98
126
|
activeValue: active ? readValue(active) : '',
|
|
99
127
|
};
|
|
100
128
|
})()`,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
returnByValue: true,
|
|
130
|
+
});
|
|
131
|
+
const editorTextRaw = verification.result?.value?.editorText ?? "";
|
|
132
|
+
const fallbackValueRaw = verification.result?.value?.fallbackValue ?? "";
|
|
133
|
+
const activeValueRaw = verification.result?.value?.activeValue ?? "";
|
|
134
|
+
const editorTextTrimmed = editorTextRaw?.trim?.() ?? "";
|
|
135
|
+
const fallbackValueTrimmed = fallbackValueRaw?.trim?.() ?? "";
|
|
136
|
+
const activeValueTrimmed = activeValueRaw?.trim?.() ?? "";
|
|
137
|
+
if (!editorTextTrimmed && !fallbackValueTrimmed && !activeValueTrimmed) {
|
|
138
|
+
// Learned: occasionally Input.insertText doesn't land in the editor; force textContent/value + input events.
|
|
139
|
+
await runtime.evaluate({
|
|
140
|
+
expression: `(() => {
|
|
113
141
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
114
142
|
if (fallback) {
|
|
115
143
|
fallback.value = ${encodedPrompt};
|
|
@@ -123,11 +151,10 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
123
151
|
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
124
152
|
}
|
|
125
153
|
})()`,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
expression: `(() => {
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const postVerification = await runtime.evaluate({
|
|
157
|
+
expression: `(() => {
|
|
131
158
|
const editor = document.querySelector(${primarySelectorLiteral});
|
|
132
159
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
133
160
|
const inputSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
@@ -151,12 +178,14 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
151
178
|
activeValue: active ? readValue(active) : '',
|
|
152
179
|
};
|
|
153
180
|
})()`,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
returnByValue: true,
|
|
182
|
+
});
|
|
183
|
+
const observedEditor = postVerification.result?.value?.editorText ?? "";
|
|
184
|
+
const observedFallback = postVerification.result?.value?.fallbackValue ?? "";
|
|
185
|
+
const observedActive = postVerification.result?.value?.activeValue ?? "";
|
|
186
|
+
observedLength = Math.max(observedEditor.length, observedFallback.length, observedActive.length);
|
|
187
|
+
}
|
|
188
|
+
const promptLength = prompt.length;
|
|
160
189
|
if (promptLength >= 50_000 && observedLength > 0 && observedLength < promptLength - 2_000) {
|
|
161
190
|
// Learned: very large prompts can truncate silently; fail fast so we can fall back to file uploads.
|
|
162
191
|
await logDomFailure(runtime, logger, "prompt-too-large");
|
|
@@ -167,28 +196,31 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
167
196
|
observedLength,
|
|
168
197
|
});
|
|
169
198
|
}
|
|
170
|
-
const clicked = await attemptSendButton(runtime, input, logger, deps?.attachmentNames, deps?.attachmentTimeoutMs);
|
|
199
|
+
const clicked = await attemptSendButton(runtime, input, logger, deps?.attachmentNames, deps?.attachmentTimeoutMs, deps?.page, deps?.attachmentNavigationUrl);
|
|
171
200
|
if (!clicked) {
|
|
172
|
-
await input
|
|
173
|
-
type: "keyDown",
|
|
174
|
-
...ENTER_KEY_EVENT,
|
|
175
|
-
text: ENTER_KEY_TEXT,
|
|
176
|
-
unmodifiedText: ENTER_KEY_TEXT,
|
|
177
|
-
});
|
|
178
|
-
await input.dispatchKeyEvent({
|
|
179
|
-
type: "keyUp",
|
|
180
|
-
...ENTER_KEY_EVENT,
|
|
181
|
-
});
|
|
201
|
+
await dispatchEnterKey(input);
|
|
182
202
|
logger("Submitted prompt via Enter key");
|
|
183
203
|
}
|
|
184
204
|
else {
|
|
185
|
-
logger("
|
|
205
|
+
logger("Activated send button");
|
|
186
206
|
}
|
|
187
207
|
await deps.onPromptSubmitted?.();
|
|
188
208
|
const commitTimeoutMs = Math.max(60_000, deps.inputTimeoutMs ?? 0);
|
|
189
209
|
// Learned: the send button can succeed but the turn doesn't appear immediately; verify commit via turns/stop button.
|
|
190
210
|
return await verifyPromptCommitted(runtime, prompt, commitTimeoutMs, logger, deps.baselineTurns ?? undefined);
|
|
191
211
|
}
|
|
212
|
+
async function dispatchEnterKey(Input) {
|
|
213
|
+
await Input.dispatchKeyEvent({
|
|
214
|
+
type: "keyDown",
|
|
215
|
+
...ENTER_KEY_EVENT,
|
|
216
|
+
text: ENTER_KEY_TEXT,
|
|
217
|
+
unmodifiedText: ENTER_KEY_TEXT,
|
|
218
|
+
});
|
|
219
|
+
await Input.dispatchKeyEvent({
|
|
220
|
+
type: "keyUp",
|
|
221
|
+
...ENTER_KEY_EVENT,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
192
224
|
export async function clearPromptComposer(Runtime, logger) {
|
|
193
225
|
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
194
226
|
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
@@ -287,11 +319,12 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
287
319
|
const attachmentExpectations = attachmentNames.map((attachment) => {
|
|
288
320
|
const name = typeof attachment === "string" ? attachment : attachment.name;
|
|
289
321
|
const normalized = name.toLowerCase().replace(/\s+/g, " ").trim();
|
|
322
|
+
const generatedBundle = typeof attachment === "object" && attachment.generatedBundle === true;
|
|
290
323
|
return {
|
|
291
324
|
name: normalized,
|
|
292
325
|
stem: normalized.replace(/\.[a-z0-9]{1,10}$/i, ""),
|
|
293
|
-
|
|
294
|
-
generatedBundle
|
|
326
|
+
namePattern: buildAttachmentNamePattern(normalized, generatedBundle)?.source ?? "",
|
|
327
|
+
generatedBundle,
|
|
295
328
|
};
|
|
296
329
|
});
|
|
297
330
|
const namesLiteral = JSON.stringify(attachmentExpectations);
|
|
@@ -299,74 +332,13 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
299
332
|
const expected = ${namesLiteral};
|
|
300
333
|
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
301
334
|
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
302
|
-
const hasNameBoundary = (text, name) => {
|
|
303
|
-
if (!name) return false;
|
|
304
|
-
let from = 0;
|
|
305
|
-
while (from < text.length) {
|
|
306
|
-
const index = text.indexOf(name, from);
|
|
307
|
-
if (index === -1) return false;
|
|
308
|
-
const previous = text[index - 1] || '';
|
|
309
|
-
const next = text[index + name.length] || '';
|
|
310
|
-
const previousOk = !previous || !/[a-z0-9._-]/.test(previous);
|
|
311
|
-
const nextOk = !next || !/[a-z0-9._-]/.test(next);
|
|
312
|
-
if (previousOk && nextOk) return true;
|
|
313
|
-
from = index + name.length;
|
|
314
|
-
}
|
|
315
|
-
return false;
|
|
316
|
-
};
|
|
317
|
-
const hasStemFileBoundary = (text, stem) => {
|
|
318
|
-
if (!stem) return false;
|
|
319
|
-
let from = 0;
|
|
320
|
-
while (from < text.length) {
|
|
321
|
-
const index = text.indexOf(stem, from);
|
|
322
|
-
if (index === -1) return false;
|
|
323
|
-
const previous = text[index - 1] || '';
|
|
324
|
-
const next = text[index + stem.length] || '';
|
|
325
|
-
const previousOk = !previous || !/[a-z0-9._-]/.test(previous);
|
|
326
|
-
const nextOk = !next || !/[a-z0-9._-]/.test(next);
|
|
327
|
-
if (previousOk && nextOk) return true;
|
|
328
|
-
from = index + stem.length;
|
|
329
|
-
}
|
|
330
|
-
return false;
|
|
331
|
-
};
|
|
332
|
-
const hasBareStemBoundary = (text, stem) => {
|
|
333
|
-
if (!stem) return false;
|
|
334
|
-
let from = 0;
|
|
335
|
-
while (from < text.length) {
|
|
336
|
-
const index = text.indexOf(stem, from);
|
|
337
|
-
if (index === -1) return false;
|
|
338
|
-
const previous = text[index - 1] || '';
|
|
339
|
-
const next = text[index + stem.length] || '';
|
|
340
|
-
const previousOk = !previous || !/[a-z0-9._-]/.test(previous);
|
|
341
|
-
const nextOk = !next || !/[a-z0-9._(-]/.test(next);
|
|
342
|
-
if (previousOk && nextOk) return true;
|
|
343
|
-
from = index + stem.length;
|
|
344
|
-
}
|
|
345
|
-
return false;
|
|
346
|
-
};
|
|
347
|
-
const hasExtensionBoundary = (text, extension) => {
|
|
348
|
-
if (!extension) return false;
|
|
349
|
-
let from = 0;
|
|
350
|
-
while (from < text.length) {
|
|
351
|
-
const index = text.indexOf(extension, from);
|
|
352
|
-
if (index === -1) return false;
|
|
353
|
-
const next = text[index + extension.length] || '';
|
|
354
|
-
if (!next || !/[a-z0-9]/.test(next)) return true;
|
|
355
|
-
from = index + extension.length;
|
|
356
|
-
}
|
|
357
|
-
return false;
|
|
358
|
-
};
|
|
359
335
|
const matchesExpected = (value, item) => {
|
|
360
336
|
const text = normalize(value);
|
|
361
337
|
if (!text) return false;
|
|
362
|
-
|
|
363
|
-
if (item.generatedBundle && hasBareStemBoundary(text, item.stem)) return true;
|
|
338
|
+
const namePattern = item.namePattern ? new RegExp(item.namePattern, 'iu') : null;
|
|
364
339
|
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
item.extension &&
|
|
368
|
-
text.includes(item.stem + '(') &&
|
|
369
|
-
hasExtensionBoundary(text, item.extension)
|
|
340
|
+
namePattern &&
|
|
341
|
+
String(value || '').split('\\n').some((candidate) => namePattern.test(normalize(candidate)))
|
|
370
342
|
) {
|
|
371
343
|
return true;
|
|
372
344
|
}
|
|
@@ -465,7 +437,7 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
465
437
|
};
|
|
466
438
|
pushAttrs(node);
|
|
467
439
|
pushText(node);
|
|
468
|
-
return pieces.join('
|
|
440
|
+
return pieces.join('\\n').toLowerCase();
|
|
469
441
|
};
|
|
470
442
|
const collectLabelHaystack = (node) => {
|
|
471
443
|
if (!node) return '';
|
|
@@ -478,7 +450,7 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
478
450
|
push(parent);
|
|
479
451
|
const grandparent = parent?.parentElement;
|
|
480
452
|
push(grandparent);
|
|
481
|
-
return pieces.join('
|
|
453
|
+
return pieces.join('\\n').toLowerCase();
|
|
482
454
|
};
|
|
483
455
|
const attachmentRoots = Array.from(new Set([composer])).filter(Boolean);
|
|
484
456
|
const collectChipNodes = () => {
|
|
@@ -499,32 +471,11 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
499
471
|
};
|
|
500
472
|
const chipNodes = collectChipNodes();
|
|
501
473
|
const chipLabels = chipNodes.map((node) => collectLabelHaystack(node));
|
|
502
|
-
const
|
|
503
|
-
const hasEllipsisSuffix = (label) => {
|
|
504
|
-
const marker = label.includes('…') ? '…' : label.includes('...') ? '...' : '';
|
|
505
|
-
if (!marker) return false;
|
|
506
|
-
return normalize(label.split(marker)[1] || '').length > 0;
|
|
507
|
-
};
|
|
508
|
-
const chipOwnLabelsWithVisibleNames = chipOwnLabels.filter((label) =>
|
|
509
|
-
/\\.[a-z][a-z0-9]{0,9}(?:\\b|$)/i.test(label) ||
|
|
510
|
-
hasEllipsisSuffix(label),
|
|
511
|
-
);
|
|
512
|
-
const visibleExtensionLabelsMatchExpected = chipOwnLabelsWithVisibleNames.every((label) =>
|
|
513
|
-
expected.some((item) => matchesExpected(label, item)),
|
|
514
|
-
);
|
|
515
|
-
const visibleStemOnlyMismatch = chipOwnLabels.some((label) =>
|
|
516
|
-
expected.some(
|
|
517
|
-
(item) =>
|
|
518
|
-
!item.generatedBundle &&
|
|
519
|
-
item.stem &&
|
|
520
|
-
hasStemFileBoundary(label, item.stem) &&
|
|
521
|
-
!matchesExpected(label, item),
|
|
522
|
-
),
|
|
523
|
-
);
|
|
524
|
-
|
|
474
|
+
const uploadEvidence = ${buildAttachmentEvidenceExpression(attachmentExpectations.map((item) => item.name))};
|
|
525
475
|
const chipsReady = (() => {
|
|
526
476
|
const used = new Set();
|
|
527
|
-
return expected.every((item) => {
|
|
477
|
+
return expected.every((item, expectedIndex) => {
|
|
478
|
+
if (uploadEvidence[expectedIndex]) return true;
|
|
528
479
|
const index = chipLabels.findIndex((label, candidateIndex) =>
|
|
529
480
|
!used.has(candidateIndex) && matchesExpected(label, item),
|
|
530
481
|
);
|
|
@@ -542,42 +493,20 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
542
493
|
),
|
|
543
494
|
),
|
|
544
495
|
);
|
|
545
|
-
|
|
546
|
-
// the filename out of attribute-readable text into a deeply nested span), but we
|
|
547
|
-
// do see at least as many distinct "Remove" affordances as attachments we
|
|
548
|
-
// uploaded, trust the upload without double-counting nested chip/remove nodes.
|
|
549
|
-
const removeAffordances = [];
|
|
550
|
-
const removeSeen = new Set();
|
|
551
|
-
for (const root of attachmentRoots) {
|
|
552
|
-
for (const node of Array.from(root.querySelectorAll(
|
|
553
|
-
'[aria-label*="Remove" i], [aria-label*="remove" i], button[aria-label*="Remove" i], button[aria-label*="remove" i]',
|
|
554
|
-
))) {
|
|
555
|
-
if (!(node instanceof HTMLElement)) continue;
|
|
556
|
-
if (node.closest('textarea,[contenteditable="true"]')) continue;
|
|
557
|
-
const aria = (node.getAttribute?.('aria-label') ?? '').toLowerCase();
|
|
558
|
-
const fileSpecific = aria.includes('remove file') || aria.includes('remove attachment');
|
|
559
|
-
const attachmentOwner = node.closest(
|
|
560
|
-
'[data-testid*="chip"], [data-testid*="attachment"], [data-testid*="upload"], [data-testid*="file"]',
|
|
561
|
-
);
|
|
562
|
-
if (!fileSpecific && !attachmentOwner) continue;
|
|
563
|
-
if (removeSeen.has(node)) continue;
|
|
564
|
-
removeSeen.add(node);
|
|
565
|
-
removeAffordances.push(node);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
const countReady =
|
|
569
|
-
!visibleStemOnlyMismatch &&
|
|
570
|
-
visibleExtensionLabelsMatchExpected &&
|
|
571
|
-
removeAffordances.length >= expected.length;
|
|
572
|
-
|
|
573
|
-
return chipsReady || inputsReady || countReady;
|
|
496
|
+
return (chipsReady || inputsReady) && !${buildAttachmentProgressExpression("composer")};
|
|
574
497
|
})()`;
|
|
575
498
|
}
|
|
576
499
|
export function buildAttachmentReadyExpressionForTest(attachmentNames) {
|
|
577
500
|
return buildAttachmentReadyExpression(attachmentNames);
|
|
578
501
|
}
|
|
579
|
-
async function attemptSendButton(Runtime, Input,
|
|
502
|
+
async function attemptSendButton(Runtime, Input, logger, attachmentNames, attachmentTimeoutMs, Page, attachmentNavigationUrl) {
|
|
580
503
|
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
504
|
+
if (needAttachment && !attachmentNavigationUrl) {
|
|
505
|
+
throw new BrowserAutomationError("Oracle cannot safely submit attachments without the pre-upload ChatGPT page identity.", {
|
|
506
|
+
stage: "submit-prompt",
|
|
507
|
+
code: "attachment-navigation-identity-unavailable",
|
|
508
|
+
});
|
|
509
|
+
}
|
|
581
510
|
const script = `(() => {
|
|
582
511
|
${buildClickDispatcher()}
|
|
583
512
|
const selectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
@@ -606,10 +535,19 @@ async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attac
|
|
|
606
535
|
}
|
|
607
536
|
const button = candidates.find((node) => isVisible(node) && isEnabled(node)) || null;
|
|
608
537
|
if (!button) return { status: 'missing' };
|
|
609
|
-
button.scrollIntoView({ block: 'center', inline: 'center' });
|
|
610
538
|
const rect = button.getBoundingClientRect();
|
|
611
539
|
if (rect.width > 0 && rect.height > 0) {
|
|
612
|
-
|
|
540
|
+
const x = rect.left + rect.width / 2;
|
|
541
|
+
const y = rect.top + rect.height / 2;
|
|
542
|
+
// Scrolling an already-visible composer can trigger ChatGPT's layout snap.
|
|
543
|
+
// If scrolling is necessary, discard these coordinates and measure later.
|
|
544
|
+
if (x < 0 || y < 0 || x >= window.innerWidth || y >= window.innerHeight) {
|
|
545
|
+
button.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
|
|
546
|
+
return { status: 'settling' };
|
|
547
|
+
}
|
|
548
|
+
const hit = document.elementFromPoint(x, y);
|
|
549
|
+
if (!hit || !button.contains(hit)) return { status: 'settling' };
|
|
550
|
+
return { status: 'point', x, y };
|
|
613
551
|
}
|
|
614
552
|
// Last-resort fallback for unusual DOMs where the button is visible but has no useful rect.
|
|
615
553
|
dispatchClickSequence(button);
|
|
@@ -620,6 +558,10 @@ async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attac
|
|
|
620
558
|
// shorter historical deadline.
|
|
621
559
|
const timeoutMs = sendButtonTimeoutMs(attachmentNames, attachmentTimeoutMs);
|
|
622
560
|
const deadline = Date.now() + timeoutMs;
|
|
561
|
+
let previousPoint;
|
|
562
|
+
let activated = false;
|
|
563
|
+
let foundButton = false;
|
|
564
|
+
let attachmentMenuChecked = !needAttachment;
|
|
623
565
|
while (Date.now() < deadline) {
|
|
624
566
|
if (needAttachment) {
|
|
625
567
|
const ready = await Runtime.evaluate({
|
|
@@ -631,13 +573,45 @@ async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attac
|
|
|
631
573
|
continue;
|
|
632
574
|
}
|
|
633
575
|
}
|
|
576
|
+
if (!attachmentMenuChecked) {
|
|
577
|
+
await activatePageForTrustedInput(Page, logger);
|
|
578
|
+
activated = true;
|
|
579
|
+
await dismissOpenComposerPlusMenu(Runtime, Input, logger);
|
|
580
|
+
attachmentMenuChecked = true;
|
|
581
|
+
previousPoint = undefined;
|
|
582
|
+
await delay(150);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (needAttachment) {
|
|
586
|
+
if (await activateExactAttachmentSendButton(Runtime, Input, logger, attachmentNavigationUrl, attachmentNames)) {
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
previousPoint = undefined;
|
|
590
|
+
await delay(100);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
// Activating the target can trigger a final compositor/layout pass. Do it before
|
|
594
|
+
// measuring the button so the trusted click never uses stale coordinates.
|
|
595
|
+
if (!activated) {
|
|
596
|
+
await activatePageForTrustedInput(Page, logger);
|
|
597
|
+
activated = true;
|
|
598
|
+
}
|
|
634
599
|
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
635
600
|
const value = result.value;
|
|
636
601
|
const status = typeof value === "string" ? value : value?.status;
|
|
602
|
+
if (status === "point" || status === "settling")
|
|
603
|
+
foundButton = true;
|
|
637
604
|
if (status === "point" &&
|
|
638
605
|
typeof value === "object" &&
|
|
639
606
|
typeof value.x === "number" &&
|
|
640
607
|
typeof value.y === "number") {
|
|
608
|
+
if (!previousPoint ||
|
|
609
|
+
Math.abs(previousPoint.x - value.x) > 1 ||
|
|
610
|
+
Math.abs(previousPoint.y - value.y) > 1) {
|
|
611
|
+
previousPoint = { x: value.x, y: value.y };
|
|
612
|
+
await delay(150);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
641
615
|
await clickTrustedPoint(Runtime, Input, value.x, value.y);
|
|
642
616
|
return true;
|
|
643
617
|
}
|
|
@@ -647,18 +621,223 @@ async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attac
|
|
|
647
621
|
if (status === "missing") {
|
|
648
622
|
break;
|
|
649
623
|
}
|
|
624
|
+
previousPoint = undefined;
|
|
650
625
|
await delay(100);
|
|
651
626
|
}
|
|
652
627
|
if (Array.isArray(attachmentNames) && attachmentNames.length > 0) {
|
|
653
|
-
throw new BrowserAutomationError(`Attachments never reached
|
|
628
|
+
throw new BrowserAutomationError(`Attachments never reached the exact ready send button after ${Math.ceil(timeoutMs / 1000)}s; tune --browser-attachment-timeout.`, {
|
|
654
629
|
stage: "submit-prompt",
|
|
655
630
|
code: "attachment-send-not-ready",
|
|
656
631
|
attachmentNames,
|
|
657
632
|
timeoutMs,
|
|
658
633
|
});
|
|
659
634
|
}
|
|
635
|
+
if (foundButton) {
|
|
636
|
+
throw new BrowserAutomationError("Send button did not settle before input timeout.", {
|
|
637
|
+
stage: "submit-prompt",
|
|
638
|
+
code: "send-button-unstable",
|
|
639
|
+
});
|
|
640
|
+
}
|
|
660
641
|
return false;
|
|
661
642
|
}
|
|
643
|
+
async function activateExactAttachmentSendButton(Runtime, Input, logger, attachmentNavigationUrl, attachmentNames = []) {
|
|
644
|
+
const probe = await Runtime.evaluate({
|
|
645
|
+
expression: `(() => {
|
|
646
|
+
const button = document.querySelector('button[data-testid="send-button"]');
|
|
647
|
+
if (!(button instanceof HTMLElement)) return { status: 'absent' };
|
|
648
|
+
const rect = button.getBoundingClientRect();
|
|
649
|
+
const style = window.getComputedStyle(button);
|
|
650
|
+
const enabled = !button.hasAttribute('disabled') &&
|
|
651
|
+
button.getAttribute('aria-disabled') !== 'true' &&
|
|
652
|
+
button.getAttribute('data-disabled') !== 'true' &&
|
|
653
|
+
style.pointerEvents !== 'none' &&
|
|
654
|
+
style.display !== 'none' &&
|
|
655
|
+
style.visibility !== 'hidden';
|
|
656
|
+
if (rect.width <= 0 || rect.height <= 0 || !enabled) return { status: 'unavailable' };
|
|
657
|
+
button.focus({ preventScroll: true });
|
|
658
|
+
return { status: document.activeElement === button ? 'focused' : 'unavailable' };
|
|
659
|
+
})()`,
|
|
660
|
+
returnByValue: true,
|
|
661
|
+
});
|
|
662
|
+
const status = probe?.result?.value?.status;
|
|
663
|
+
if (status !== "focused") {
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
if (!Input || typeof Input.dispatchKeyEvent !== "function") {
|
|
667
|
+
throw new BrowserAutomationError("ChatGPT's attachment send button is ready but Oracle cannot activate it safely.", {
|
|
668
|
+
stage: "submit-prompt",
|
|
669
|
+
code: "attachment-send-keyboard-unavailable",
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
const expectedIdentity = composerNavigationIdentityFromUrl(attachmentNavigationUrl ?? "");
|
|
673
|
+
if (!expectedIdentity || !attachmentNavigationUrl) {
|
|
674
|
+
throw new BrowserAutomationError("Attachment page identity is unavailable before dispatch.", {
|
|
675
|
+
stage: "submit-prompt",
|
|
676
|
+
code: "attachment-navigation-identity-unavailable",
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const guardId = randomUUID();
|
|
680
|
+
// Recheck at event delivery too: a SPA can navigate after the CDP probe returns.
|
|
681
|
+
let delivery;
|
|
682
|
+
try {
|
|
683
|
+
const boundary = await Runtime.evaluate({
|
|
684
|
+
expression: `(() => {
|
|
685
|
+
const button = document.querySelector('button[data-testid="send-button"]');
|
|
686
|
+
const check = () => {
|
|
687
|
+
const navigation = ${buildComposerNavigationValidationExpression(attachmentNavigationUrl)};
|
|
688
|
+
const rect = button?.getBoundingClientRect();
|
|
689
|
+
const style = button ? window.getComputedStyle(button) : null;
|
|
690
|
+
return {
|
|
691
|
+
...navigation,
|
|
692
|
+
focused: button instanceof HTMLElement && document.activeElement === button &&
|
|
693
|
+
document.querySelector('button[data-testid="send-button"]') === button &&
|
|
694
|
+
!button.hasAttribute('disabled') && button.getAttribute('aria-disabled') !== 'true' &&
|
|
695
|
+
button.getAttribute('data-disabled') !== 'true' && rect.width > 0 && rect.height > 0 &&
|
|
696
|
+
style.display !== 'none' && style.visibility !== 'hidden' && style.pointerEvents !== 'none',
|
|
697
|
+
attachmentsReady: ${buildAttachmentReadyExpression(attachmentNames)},
|
|
698
|
+
};
|
|
699
|
+
};
|
|
700
|
+
const safeCheck = () => {
|
|
701
|
+
try { return check(); } catch { return { currentUrl: location.href, contextMatches: false, focused: false, attachmentsReady: false }; }
|
|
702
|
+
};
|
|
703
|
+
const snapshot = safeCheck();
|
|
704
|
+
if (!snapshot.contextMatches || !snapshot.focused || !snapshot.attachmentsReady) return snapshot;
|
|
705
|
+
const existing = window.__oracleAttachmentDispatchGuard;
|
|
706
|
+
if (existing && !existing.finished) return { ...snapshot, focused: false };
|
|
707
|
+
existing?.cleanup?.();
|
|
708
|
+
const guard = { id: ${JSON.stringify(guardId)}, sawKeyDown: false, blocked: null, finished: false };
|
|
709
|
+
const detach = () => {
|
|
710
|
+
window.removeEventListener('keydown', onKeyDown, true);
|
|
711
|
+
window.removeEventListener('keyup', onKeyUp, true);
|
|
712
|
+
window.removeEventListener('click', onClick, true);
|
|
713
|
+
guard.finished = true;
|
|
714
|
+
};
|
|
715
|
+
const cancel = (event, state) => {
|
|
716
|
+
event.preventDefault(); event.stopImmediatePropagation(); guard.blocked = state;
|
|
717
|
+
};
|
|
718
|
+
const onKeyDown = event => {
|
|
719
|
+
if (event.key !== 'Enter' || !event.isTrusted) return;
|
|
720
|
+
guard.sawKeyDown = true;
|
|
721
|
+
const state = safeCheck();
|
|
722
|
+
if (!state.contextMatches || !state.focused || !state.attachmentsReady) cancel(event, state);
|
|
723
|
+
};
|
|
724
|
+
const onKeyUp = event => {
|
|
725
|
+
if (event.key === 'Enter' && event.isTrusted && guard.blocked) { cancel(event, guard.blocked); detach(); }
|
|
726
|
+
};
|
|
727
|
+
const onClick = event => {
|
|
728
|
+
if (!guard.sawKeyDown || !(event.target instanceof Node) ||
|
|
729
|
+
!(button.contains(event.target) || event.target instanceof Element && event.target.closest('button[data-testid="send-button"]'))) return;
|
|
730
|
+
const state = safeCheck();
|
|
731
|
+
if (guard.blocked || !state.contextMatches || !state.focused || !state.attachmentsReady) cancel(event, guard.blocked ?? state);
|
|
732
|
+
detach();
|
|
733
|
+
};
|
|
734
|
+
guard.cleanup = () => { detach(); if (window.__oracleAttachmentDispatchGuard === guard) delete window.__oracleAttachmentDispatchGuard; };
|
|
735
|
+
window.__oracleAttachmentDispatchGuard = guard;
|
|
736
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
737
|
+
window.addEventListener('keyup', onKeyUp, true);
|
|
738
|
+
window.addEventListener('click', onClick, true);
|
|
739
|
+
return snapshot;
|
|
740
|
+
})()`,
|
|
741
|
+
returnByValue: true,
|
|
742
|
+
});
|
|
743
|
+
const snapshot = boundary?.result?.value;
|
|
744
|
+
assertComposerNavigationSnapshot(attachmentNavigationUrl, snapshot);
|
|
745
|
+
if (snapshot?.focused !== true || snapshot.attachmentsReady !== true)
|
|
746
|
+
return false;
|
|
747
|
+
await Input.dispatchKeyEvent({
|
|
748
|
+
type: "keyDown",
|
|
749
|
+
...ENTER_KEY_EVENT,
|
|
750
|
+
text: ENTER_KEY_TEXT,
|
|
751
|
+
unmodifiedText: ENTER_KEY_TEXT,
|
|
752
|
+
});
|
|
753
|
+
await Input.dispatchKeyEvent({ type: "keyUp", ...ENTER_KEY_EVENT });
|
|
754
|
+
}
|
|
755
|
+
finally {
|
|
756
|
+
const result = await withoutBrowserCancellation(() => Runtime.evaluate({
|
|
757
|
+
expression: `(() => {
|
|
758
|
+
const guard = window.__oracleAttachmentDispatchGuard;
|
|
759
|
+
if (guard?.id !== ${JSON.stringify(guardId)}) return null;
|
|
760
|
+
const summary = { sawKeyDown: guard.sawKeyDown, blocked: guard.blocked };
|
|
761
|
+
guard.cleanup(); return summary;
|
|
762
|
+
})()`,
|
|
763
|
+
returnByValue: true,
|
|
764
|
+
}).catch(() => undefined));
|
|
765
|
+
delivery = result?.result?.value;
|
|
766
|
+
}
|
|
767
|
+
if (delivery?.blocked) {
|
|
768
|
+
assertComposerNavigationSnapshot(attachmentNavigationUrl, delivery.blocked);
|
|
769
|
+
throw new BrowserAutomationError("Attachment focus or upload readiness changed at dispatch; the send was stopped.", {
|
|
770
|
+
stage: "submit-prompt",
|
|
771
|
+
code: "attachment-send-not-ready",
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
if (delivery?.sawKeyDown !== true) {
|
|
775
|
+
throw new BrowserAutomationError("Attachment input delivery could not be verified; do not retry automatically.", {
|
|
776
|
+
stage: "submit-prompt",
|
|
777
|
+
code: "attachment-send-ambiguous",
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
logger?.("Activated exact attachment send button via keyboard");
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
async function dismissOpenComposerPlusMenu(Runtime, Input, logger) {
|
|
784
|
+
const probe = await Runtime.evaluate({
|
|
785
|
+
expression: `(() => {
|
|
786
|
+
const selectors = ['#composer-plus-btn', 'button[data-testid="composer-plus-btn"]'];
|
|
787
|
+
const button = selectors
|
|
788
|
+
.map(selector => document.querySelector(selector))
|
|
789
|
+
.find(node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0);
|
|
790
|
+
if (!(button instanceof HTMLElement)) return { status: 'absent' };
|
|
791
|
+
if (button.getAttribute('aria-expanded') !== 'true') return { status: 'closed' };
|
|
792
|
+
button.focus({ preventScroll: true });
|
|
793
|
+
return { status: 'open', focused: document.activeElement === button };
|
|
794
|
+
})()`,
|
|
795
|
+
returnByValue: true,
|
|
796
|
+
});
|
|
797
|
+
const status = probe?.result?.value?.status;
|
|
798
|
+
if (status !== "open") {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
if (!Input || typeof Input.dispatchKeyEvent !== "function") {
|
|
802
|
+
throw new BrowserAutomationError("ChatGPT's attachment menu is still open and Oracle cannot dismiss it safely before sending.", {
|
|
803
|
+
stage: "submit-prompt",
|
|
804
|
+
code: "attachment-menu-dismiss-unavailable",
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
await Input.dispatchKeyEvent({ type: "keyDown", ...ESCAPE_KEY_EVENT });
|
|
808
|
+
await Input.dispatchKeyEvent({ type: "keyUp", ...ESCAPE_KEY_EVENT });
|
|
809
|
+
const deadline = Date.now() + 2_000;
|
|
810
|
+
while (Date.now() < deadline) {
|
|
811
|
+
const state = await Runtime.evaluate({
|
|
812
|
+
expression: `(() => {
|
|
813
|
+
const selectors = ['#composer-plus-btn', 'button[data-testid="composer-plus-btn"]'];
|
|
814
|
+
return !selectors.some(selector =>
|
|
815
|
+
document.querySelector(selector)?.getAttribute?.('aria-expanded') === 'true'
|
|
816
|
+
);
|
|
817
|
+
})()`,
|
|
818
|
+
returnByValue: true,
|
|
819
|
+
});
|
|
820
|
+
if (state?.result?.value === true) {
|
|
821
|
+
logger?.("Closed attachment menu before send");
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
await delay(50);
|
|
825
|
+
}
|
|
826
|
+
throw new BrowserAutomationError("ChatGPT's attachment menu did not close before send; prompt was left staged.", {
|
|
827
|
+
stage: "submit-prompt",
|
|
828
|
+
code: "attachment-menu-stuck-open",
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
async function activatePageForTrustedInput(Page, logger) {
|
|
832
|
+
if (!Page || typeof Page.bringToFront !== "function") {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
await Page.bringToFront().catch((error) => {
|
|
836
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
837
|
+
logger?.(`[browser] Could not bring ChatGPT target to front before trusted click: ${message}`);
|
|
838
|
+
});
|
|
839
|
+
await delay(150);
|
|
840
|
+
}
|
|
662
841
|
async function clickTrustedPoint(Runtime, Input, x, y) {
|
|
663
842
|
if (Input && typeof Input.dispatchMouseEvent === "function") {
|
|
664
843
|
await Input.dispatchMouseEvent({ type: "mouseMoved", x, y });
|
|
@@ -814,14 +993,8 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
814
993
|
logger(`Prompt commit check failed; latest state: ${probe ? JSON.stringify(probe) : "unavailable"}`);
|
|
815
994
|
await logDomFailure(Runtime, logger, "prompt-commit");
|
|
816
995
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
stage: "submit-prompt",
|
|
820
|
-
code: "prompt-too-large",
|
|
821
|
-
promptLength: prompt.trim().length,
|
|
822
|
-
timeoutMs,
|
|
823
|
-
});
|
|
824
|
-
}
|
|
996
|
+
// Input was already dispatched. Even a large staged prompt may commit late;
|
|
997
|
+
// classifying this as truncation would let the runner submit an upload fallback.
|
|
825
998
|
throw new BrowserAutomationError("Prompt did not appear in conversation before timeout (send may have failed)", {
|
|
826
999
|
stage: "submit-prompt",
|
|
827
1000
|
code: "prompt-commit-timeout",
|
|
@@ -849,7 +1022,9 @@ function summarizeCommitProbe(probe) {
|
|
|
849
1022
|
}
|
|
850
1023
|
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
851
1024
|
export const __test__ = {
|
|
1025
|
+
activateExactAttachmentSendButton,
|
|
852
1026
|
attemptSendButton,
|
|
1027
|
+
dismissOpenComposerPlusMenu,
|
|
853
1028
|
sendButtonTimeoutMs,
|
|
854
1029
|
verifyPromptCommitted,
|
|
855
1030
|
};
|