@steipete/oracle 0.18.0 → 0.20.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 +76 -76
- 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 +170 -45
- package/dist/src/browser/actions/modelSelection.js +91 -4
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +364 -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 +156 -16
- package/dist/src/browser/actions/webSearch.js +96 -0
- 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 +272 -47
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +525 -274
- package/dist/src/browser/liveTabs.js +3 -4
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +83 -15
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
- package/dist/src/browser/reattach.js +31 -4
- package/dist/src/browser/recoveryTarget.js +131 -0
- package/dist/src/browser/sessionRunner.js +73 -11
- package/dist/src/browser/tabLeaseRegistry.js +434 -38
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +36 -4
- package/dist/src/cli/browserDefaults.js +10 -0
- package/dist/src/cli/browserTabs.js +15 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -48
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +13 -7
- package/dist/src/cli/sessionRunner.js +95 -12
- 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 +86 -24
- 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 +8 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +73 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +346 -71
- 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,17 @@
|
|
|
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";
|
|
14
|
+
import { activateWebSearch } from "./webSearch.js";
|
|
7
15
|
const ENTER_KEY_EVENT = {
|
|
8
16
|
key: "Enter",
|
|
9
17
|
code: "Enter",
|
|
@@ -11,12 +19,33 @@ const ENTER_KEY_EVENT = {
|
|
|
11
19
|
nativeVirtualKeyCode: 13,
|
|
12
20
|
};
|
|
13
21
|
const ENTER_KEY_TEXT = "\r";
|
|
22
|
+
const ESCAPE_KEY_EVENT = {
|
|
23
|
+
key: "Escape",
|
|
24
|
+
code: "Escape",
|
|
25
|
+
windowsVirtualKeyCode: 27,
|
|
26
|
+
nativeVirtualKeyCode: 27,
|
|
27
|
+
};
|
|
14
28
|
export async function submitPrompt(deps, prompt, logger) {
|
|
15
29
|
const { runtime, input } = deps;
|
|
30
|
+
const hasAttachments = Array.isArray(deps.attachmentNames) && deps.attachmentNames.length > 0;
|
|
31
|
+
if (hasAttachments && !deps.attachmentNavigationUrl) {
|
|
32
|
+
throw new BrowserAutomationError("Oracle cannot safely submit attachments without the pre-upload ChatGPT page identity.", {
|
|
33
|
+
stage: "submit-prompt",
|
|
34
|
+
code: "attachment-navigation-identity-unavailable",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (hasAttachments) {
|
|
38
|
+
await assertComposerPlusStayedInPlace(runtime, deps.attachmentNavigationUrl);
|
|
39
|
+
}
|
|
16
40
|
await waitForDomReady(runtime, logger, deps.inputTimeoutMs ?? undefined);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
41
|
+
let observedLength;
|
|
42
|
+
if (hasAttachments) {
|
|
43
|
+
observedLength = await stageAttachmentPrompt(runtime, prompt, deps.attachmentNavigationUrl);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const encodedPrompt = JSON.stringify(prompt);
|
|
47
|
+
const focusResult = await runtime.evaluate({
|
|
48
|
+
expression: `(() => {
|
|
20
49
|
${buildClickDispatcher()}
|
|
21
50
|
const SELECTORS = ${JSON.stringify(INPUT_SELECTORS)};
|
|
22
51
|
const isVisible = (node) => {
|
|
@@ -60,21 +89,21 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
60
89
|
}
|
|
61
90
|
return { focused: false };
|
|
62
91
|
})()`,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
returnByValue: true,
|
|
93
|
+
awaitPromise: true,
|
|
94
|
+
});
|
|
95
|
+
if (!focusResult.result?.value?.focused) {
|
|
96
|
+
await logDomFailure(runtime, logger, "focus-textarea");
|
|
97
|
+
throw new Error("Failed to focus prompt textarea");
|
|
98
|
+
}
|
|
99
|
+
await input.insertText({ text: prompt });
|
|
100
|
+
// Some pages (notably ChatGPT when subscriptions/widgets load) need a brief settle
|
|
101
|
+
// before the send button becomes enabled; give it a short breather to avoid races.
|
|
102
|
+
await delay(500);
|
|
103
|
+
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
104
|
+
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
105
|
+
const verification = await runtime.evaluate({
|
|
106
|
+
expression: `(() => {
|
|
78
107
|
const editor = document.querySelector(${primarySelectorLiteral});
|
|
79
108
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
80
109
|
const inputSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
@@ -98,18 +127,18 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
98
127
|
activeValue: active ? readValue(active) : '',
|
|
99
128
|
};
|
|
100
129
|
})()`,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
returnByValue: true,
|
|
131
|
+
});
|
|
132
|
+
const editorTextRaw = verification.result?.value?.editorText ?? "";
|
|
133
|
+
const fallbackValueRaw = verification.result?.value?.fallbackValue ?? "";
|
|
134
|
+
const activeValueRaw = verification.result?.value?.activeValue ?? "";
|
|
135
|
+
const editorTextTrimmed = editorTextRaw?.trim?.() ?? "";
|
|
136
|
+
const fallbackValueTrimmed = fallbackValueRaw?.trim?.() ?? "";
|
|
137
|
+
const activeValueTrimmed = activeValueRaw?.trim?.() ?? "";
|
|
138
|
+
if (!editorTextTrimmed && !fallbackValueTrimmed && !activeValueTrimmed) {
|
|
139
|
+
// Learned: occasionally Input.insertText doesn't land in the editor; force textContent/value + input events.
|
|
140
|
+
await runtime.evaluate({
|
|
141
|
+
expression: `(() => {
|
|
113
142
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
114
143
|
if (fallback) {
|
|
115
144
|
fallback.value = ${encodedPrompt};
|
|
@@ -123,11 +152,10 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
123
152
|
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
124
153
|
}
|
|
125
154
|
})()`,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
expression: `(() => {
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const postVerification = await runtime.evaluate({
|
|
158
|
+
expression: `(() => {
|
|
131
159
|
const editor = document.querySelector(${primarySelectorLiteral});
|
|
132
160
|
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
133
161
|
const inputSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
@@ -151,12 +179,14 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
151
179
|
activeValue: active ? readValue(active) : '',
|
|
152
180
|
};
|
|
153
181
|
})()`,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
returnByValue: true,
|
|
183
|
+
});
|
|
184
|
+
const observedEditor = postVerification.result?.value?.editorText ?? "";
|
|
185
|
+
const observedFallback = postVerification.result?.value?.fallbackValue ?? "";
|
|
186
|
+
const observedActive = postVerification.result?.value?.activeValue ?? "";
|
|
187
|
+
observedLength = Math.max(observedEditor.length, observedFallback.length, observedActive.length);
|
|
188
|
+
}
|
|
189
|
+
const promptLength = prompt.length;
|
|
160
190
|
if (promptLength >= 50_000 && observedLength > 0 && observedLength < promptLength - 2_000) {
|
|
161
191
|
// Learned: very large prompts can truncate silently; fail fast so we can fall back to file uploads.
|
|
162
192
|
await logDomFailure(runtime, logger, "prompt-too-large");
|
|
@@ -167,28 +197,33 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
167
197
|
observedLength,
|
|
168
198
|
});
|
|
169
199
|
}
|
|
170
|
-
|
|
200
|
+
if (deps.webSearch)
|
|
201
|
+
await activateWebSearch(runtime, input, prompt, logger);
|
|
202
|
+
const clicked = await attemptSendButton(runtime, input, logger, deps?.attachmentNames, deps?.attachmentTimeoutMs, deps?.page, deps?.attachmentNavigationUrl);
|
|
171
203
|
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
|
-
});
|
|
204
|
+
await dispatchEnterKey(input);
|
|
182
205
|
logger("Submitted prompt via Enter key");
|
|
183
206
|
}
|
|
184
207
|
else {
|
|
185
|
-
logger("
|
|
208
|
+
logger("Activated send button");
|
|
186
209
|
}
|
|
187
210
|
await deps.onPromptSubmitted?.();
|
|
188
211
|
const commitTimeoutMs = Math.max(60_000, deps.inputTimeoutMs ?? 0);
|
|
189
212
|
// Learned: the send button can succeed but the turn doesn't appear immediately; verify commit via turns/stop button.
|
|
190
213
|
return await verifyPromptCommitted(runtime, prompt, commitTimeoutMs, logger, deps.baselineTurns ?? undefined);
|
|
191
214
|
}
|
|
215
|
+
async function dispatchEnterKey(Input) {
|
|
216
|
+
await Input.dispatchKeyEvent({
|
|
217
|
+
type: "keyDown",
|
|
218
|
+
...ENTER_KEY_EVENT,
|
|
219
|
+
text: ENTER_KEY_TEXT,
|
|
220
|
+
unmodifiedText: ENTER_KEY_TEXT,
|
|
221
|
+
});
|
|
222
|
+
await Input.dispatchKeyEvent({
|
|
223
|
+
type: "keyUp",
|
|
224
|
+
...ENTER_KEY_EVENT,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
192
227
|
export async function clearPromptComposer(Runtime, logger) {
|
|
193
228
|
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
194
229
|
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
@@ -287,11 +322,12 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
287
322
|
const attachmentExpectations = attachmentNames.map((attachment) => {
|
|
288
323
|
const name = typeof attachment === "string" ? attachment : attachment.name;
|
|
289
324
|
const normalized = name.toLowerCase().replace(/\s+/g, " ").trim();
|
|
325
|
+
const generatedBundle = typeof attachment === "object" && attachment.generatedBundle === true;
|
|
290
326
|
return {
|
|
291
327
|
name: normalized,
|
|
292
328
|
stem: normalized.replace(/\.[a-z0-9]{1,10}$/i, ""),
|
|
293
|
-
|
|
294
|
-
generatedBundle
|
|
329
|
+
namePattern: buildAttachmentNamePattern(normalized, generatedBundle)?.source ?? "",
|
|
330
|
+
generatedBundle,
|
|
295
331
|
};
|
|
296
332
|
});
|
|
297
333
|
const namesLiteral = JSON.stringify(attachmentExpectations);
|
|
@@ -299,74 +335,13 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
299
335
|
const expected = ${namesLiteral};
|
|
300
336
|
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
301
337
|
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
338
|
const matchesExpected = (value, item) => {
|
|
360
339
|
const text = normalize(value);
|
|
361
340
|
if (!text) return false;
|
|
362
|
-
|
|
363
|
-
if (item.generatedBundle && hasBareStemBoundary(text, item.stem)) return true;
|
|
341
|
+
const namePattern = item.namePattern ? new RegExp(item.namePattern, 'iu') : null;
|
|
364
342
|
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
item.extension &&
|
|
368
|
-
text.includes(item.stem + '(') &&
|
|
369
|
-
hasExtensionBoundary(text, item.extension)
|
|
343
|
+
namePattern &&
|
|
344
|
+
String(value || '').split('\\n').some((candidate) => namePattern.test(normalize(candidate)))
|
|
370
345
|
) {
|
|
371
346
|
return true;
|
|
372
347
|
}
|
|
@@ -465,7 +440,7 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
465
440
|
};
|
|
466
441
|
pushAttrs(node);
|
|
467
442
|
pushText(node);
|
|
468
|
-
return pieces.join('
|
|
443
|
+
return pieces.join('\\n').toLowerCase();
|
|
469
444
|
};
|
|
470
445
|
const collectLabelHaystack = (node) => {
|
|
471
446
|
if (!node) return '';
|
|
@@ -478,7 +453,7 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
478
453
|
push(parent);
|
|
479
454
|
const grandparent = parent?.parentElement;
|
|
480
455
|
push(grandparent);
|
|
481
|
-
return pieces.join('
|
|
456
|
+
return pieces.join('\\n').toLowerCase();
|
|
482
457
|
};
|
|
483
458
|
const attachmentRoots = Array.from(new Set([composer])).filter(Boolean);
|
|
484
459
|
const collectChipNodes = () => {
|
|
@@ -499,32 +474,11 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
499
474
|
};
|
|
500
475
|
const chipNodes = collectChipNodes();
|
|
501
476
|
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
|
-
|
|
477
|
+
const uploadEvidence = ${buildAttachmentEvidenceExpression(attachmentExpectations.map((item) => item.name))};
|
|
525
478
|
const chipsReady = (() => {
|
|
526
479
|
const used = new Set();
|
|
527
|
-
return expected.every((item) => {
|
|
480
|
+
return expected.every((item, expectedIndex) => {
|
|
481
|
+
if (uploadEvidence[expectedIndex]) return true;
|
|
528
482
|
const index = chipLabels.findIndex((label, candidateIndex) =>
|
|
529
483
|
!used.has(candidateIndex) && matchesExpected(label, item),
|
|
530
484
|
);
|
|
@@ -542,42 +496,20 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
542
496
|
),
|
|
543
497
|
),
|
|
544
498
|
);
|
|
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;
|
|
499
|
+
return (chipsReady || inputsReady) && !${buildAttachmentProgressExpression("composer")};
|
|
574
500
|
})()`;
|
|
575
501
|
}
|
|
576
502
|
export function buildAttachmentReadyExpressionForTest(attachmentNames) {
|
|
577
503
|
return buildAttachmentReadyExpression(attachmentNames);
|
|
578
504
|
}
|
|
579
|
-
async function attemptSendButton(Runtime, Input,
|
|
505
|
+
async function attemptSendButton(Runtime, Input, logger, attachmentNames, attachmentTimeoutMs, Page, attachmentNavigationUrl) {
|
|
580
506
|
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
507
|
+
if (needAttachment && !attachmentNavigationUrl) {
|
|
508
|
+
throw new BrowserAutomationError("Oracle cannot safely submit attachments without the pre-upload ChatGPT page identity.", {
|
|
509
|
+
stage: "submit-prompt",
|
|
510
|
+
code: "attachment-navigation-identity-unavailable",
|
|
511
|
+
});
|
|
512
|
+
}
|
|
581
513
|
const script = `(() => {
|
|
582
514
|
${buildClickDispatcher()}
|
|
583
515
|
const selectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
@@ -606,10 +538,19 @@ async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attac
|
|
|
606
538
|
}
|
|
607
539
|
const button = candidates.find((node) => isVisible(node) && isEnabled(node)) || null;
|
|
608
540
|
if (!button) return { status: 'missing' };
|
|
609
|
-
button.scrollIntoView({ block: 'center', inline: 'center' });
|
|
610
541
|
const rect = button.getBoundingClientRect();
|
|
611
542
|
if (rect.width > 0 && rect.height > 0) {
|
|
612
|
-
|
|
543
|
+
const x = rect.left + rect.width / 2;
|
|
544
|
+
const y = rect.top + rect.height / 2;
|
|
545
|
+
// Scrolling an already-visible composer can trigger ChatGPT's layout snap.
|
|
546
|
+
// If scrolling is necessary, discard these coordinates and measure later.
|
|
547
|
+
if (x < 0 || y < 0 || x >= window.innerWidth || y >= window.innerHeight) {
|
|
548
|
+
button.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
|
|
549
|
+
return { status: 'settling' };
|
|
550
|
+
}
|
|
551
|
+
const hit = document.elementFromPoint(x, y);
|
|
552
|
+
if (!hit || !button.contains(hit)) return { status: 'settling' };
|
|
553
|
+
return { status: 'point', x, y };
|
|
613
554
|
}
|
|
614
555
|
// Last-resort fallback for unusual DOMs where the button is visible but has no useful rect.
|
|
615
556
|
dispatchClickSequence(button);
|
|
@@ -620,6 +561,10 @@ async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attac
|
|
|
620
561
|
// shorter historical deadline.
|
|
621
562
|
const timeoutMs = sendButtonTimeoutMs(attachmentNames, attachmentTimeoutMs);
|
|
622
563
|
const deadline = Date.now() + timeoutMs;
|
|
564
|
+
let previousPoint;
|
|
565
|
+
let activated = false;
|
|
566
|
+
let foundButton = false;
|
|
567
|
+
let attachmentMenuChecked = !needAttachment;
|
|
623
568
|
while (Date.now() < deadline) {
|
|
624
569
|
if (needAttachment) {
|
|
625
570
|
const ready = await Runtime.evaluate({
|
|
@@ -631,13 +576,45 @@ async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attac
|
|
|
631
576
|
continue;
|
|
632
577
|
}
|
|
633
578
|
}
|
|
579
|
+
if (!attachmentMenuChecked) {
|
|
580
|
+
await activatePageForTrustedInput(Page, logger);
|
|
581
|
+
activated = true;
|
|
582
|
+
await dismissOpenComposerPlusMenu(Runtime, Input, logger);
|
|
583
|
+
attachmentMenuChecked = true;
|
|
584
|
+
previousPoint = undefined;
|
|
585
|
+
await delay(150);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (needAttachment) {
|
|
589
|
+
if (await activateExactAttachmentSendButton(Runtime, Input, logger, attachmentNavigationUrl, attachmentNames)) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
previousPoint = undefined;
|
|
593
|
+
await delay(100);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
// Activating the target can trigger a final compositor/layout pass. Do it before
|
|
597
|
+
// measuring the button so the trusted click never uses stale coordinates.
|
|
598
|
+
if (!activated) {
|
|
599
|
+
await activatePageForTrustedInput(Page, logger);
|
|
600
|
+
activated = true;
|
|
601
|
+
}
|
|
634
602
|
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
635
603
|
const value = result.value;
|
|
636
604
|
const status = typeof value === "string" ? value : value?.status;
|
|
605
|
+
if (status === "point" || status === "settling")
|
|
606
|
+
foundButton = true;
|
|
637
607
|
if (status === "point" &&
|
|
638
608
|
typeof value === "object" &&
|
|
639
609
|
typeof value.x === "number" &&
|
|
640
610
|
typeof value.y === "number") {
|
|
611
|
+
if (!previousPoint ||
|
|
612
|
+
Math.abs(previousPoint.x - value.x) > 1 ||
|
|
613
|
+
Math.abs(previousPoint.y - value.y) > 1) {
|
|
614
|
+
previousPoint = { x: value.x, y: value.y };
|
|
615
|
+
await delay(150);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
641
618
|
await clickTrustedPoint(Runtime, Input, value.x, value.y);
|
|
642
619
|
return true;
|
|
643
620
|
}
|
|
@@ -647,18 +624,223 @@ async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attac
|
|
|
647
624
|
if (status === "missing") {
|
|
648
625
|
break;
|
|
649
626
|
}
|
|
627
|
+
previousPoint = undefined;
|
|
650
628
|
await delay(100);
|
|
651
629
|
}
|
|
652
630
|
if (Array.isArray(attachmentNames) && attachmentNames.length > 0) {
|
|
653
|
-
throw new BrowserAutomationError(`Attachments never reached
|
|
631
|
+
throw new BrowserAutomationError(`Attachments never reached the exact ready send button after ${Math.ceil(timeoutMs / 1000)}s; tune --browser-attachment-timeout.`, {
|
|
654
632
|
stage: "submit-prompt",
|
|
655
633
|
code: "attachment-send-not-ready",
|
|
656
634
|
attachmentNames,
|
|
657
635
|
timeoutMs,
|
|
658
636
|
});
|
|
659
637
|
}
|
|
638
|
+
if (foundButton) {
|
|
639
|
+
throw new BrowserAutomationError("Send button did not settle before input timeout.", {
|
|
640
|
+
stage: "submit-prompt",
|
|
641
|
+
code: "send-button-unstable",
|
|
642
|
+
});
|
|
643
|
+
}
|
|
660
644
|
return false;
|
|
661
645
|
}
|
|
646
|
+
async function activateExactAttachmentSendButton(Runtime, Input, logger, attachmentNavigationUrl, attachmentNames = []) {
|
|
647
|
+
const probe = await Runtime.evaluate({
|
|
648
|
+
expression: `(() => {
|
|
649
|
+
const button = document.querySelector('button[data-testid="send-button"]');
|
|
650
|
+
if (!(button instanceof HTMLElement)) return { status: 'absent' };
|
|
651
|
+
const rect = button.getBoundingClientRect();
|
|
652
|
+
const style = window.getComputedStyle(button);
|
|
653
|
+
const enabled = !button.hasAttribute('disabled') &&
|
|
654
|
+
button.getAttribute('aria-disabled') !== 'true' &&
|
|
655
|
+
button.getAttribute('data-disabled') !== 'true' &&
|
|
656
|
+
style.pointerEvents !== 'none' &&
|
|
657
|
+
style.display !== 'none' &&
|
|
658
|
+
style.visibility !== 'hidden';
|
|
659
|
+
if (rect.width <= 0 || rect.height <= 0 || !enabled) return { status: 'unavailable' };
|
|
660
|
+
button.focus({ preventScroll: true });
|
|
661
|
+
return { status: document.activeElement === button ? 'focused' : 'unavailable' };
|
|
662
|
+
})()`,
|
|
663
|
+
returnByValue: true,
|
|
664
|
+
});
|
|
665
|
+
const status = probe?.result?.value?.status;
|
|
666
|
+
if (status !== "focused") {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
if (!Input || typeof Input.dispatchKeyEvent !== "function") {
|
|
670
|
+
throw new BrowserAutomationError("ChatGPT's attachment send button is ready but Oracle cannot activate it safely.", {
|
|
671
|
+
stage: "submit-prompt",
|
|
672
|
+
code: "attachment-send-keyboard-unavailable",
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
const expectedIdentity = composerNavigationIdentityFromUrl(attachmentNavigationUrl ?? "");
|
|
676
|
+
if (!expectedIdentity || !attachmentNavigationUrl) {
|
|
677
|
+
throw new BrowserAutomationError("Attachment page identity is unavailable before dispatch.", {
|
|
678
|
+
stage: "submit-prompt",
|
|
679
|
+
code: "attachment-navigation-identity-unavailable",
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const guardId = randomUUID();
|
|
683
|
+
// Recheck at event delivery too: a SPA can navigate after the CDP probe returns.
|
|
684
|
+
let delivery;
|
|
685
|
+
try {
|
|
686
|
+
const boundary = await Runtime.evaluate({
|
|
687
|
+
expression: `(() => {
|
|
688
|
+
const button = document.querySelector('button[data-testid="send-button"]');
|
|
689
|
+
const check = () => {
|
|
690
|
+
const navigation = ${buildComposerNavigationValidationExpression(attachmentNavigationUrl)};
|
|
691
|
+
const rect = button?.getBoundingClientRect();
|
|
692
|
+
const style = button ? window.getComputedStyle(button) : null;
|
|
693
|
+
return {
|
|
694
|
+
...navigation,
|
|
695
|
+
focused: button instanceof HTMLElement && document.activeElement === button &&
|
|
696
|
+
document.querySelector('button[data-testid="send-button"]') === button &&
|
|
697
|
+
!button.hasAttribute('disabled') && button.getAttribute('aria-disabled') !== 'true' &&
|
|
698
|
+
button.getAttribute('data-disabled') !== 'true' && rect.width > 0 && rect.height > 0 &&
|
|
699
|
+
style.display !== 'none' && style.visibility !== 'hidden' && style.pointerEvents !== 'none',
|
|
700
|
+
attachmentsReady: ${buildAttachmentReadyExpression(attachmentNames)},
|
|
701
|
+
};
|
|
702
|
+
};
|
|
703
|
+
const safeCheck = () => {
|
|
704
|
+
try { return check(); } catch { return { currentUrl: location.href, contextMatches: false, focused: false, attachmentsReady: false }; }
|
|
705
|
+
};
|
|
706
|
+
const snapshot = safeCheck();
|
|
707
|
+
if (!snapshot.contextMatches || !snapshot.focused || !snapshot.attachmentsReady) return snapshot;
|
|
708
|
+
const existing = window.__oracleAttachmentDispatchGuard;
|
|
709
|
+
if (existing && !existing.finished) return { ...snapshot, focused: false };
|
|
710
|
+
existing?.cleanup?.();
|
|
711
|
+
const guard = { id: ${JSON.stringify(guardId)}, sawKeyDown: false, blocked: null, finished: false };
|
|
712
|
+
const detach = () => {
|
|
713
|
+
window.removeEventListener('keydown', onKeyDown, true);
|
|
714
|
+
window.removeEventListener('keyup', onKeyUp, true);
|
|
715
|
+
window.removeEventListener('click', onClick, true);
|
|
716
|
+
guard.finished = true;
|
|
717
|
+
};
|
|
718
|
+
const cancel = (event, state) => {
|
|
719
|
+
event.preventDefault(); event.stopImmediatePropagation(); guard.blocked = state;
|
|
720
|
+
};
|
|
721
|
+
const onKeyDown = event => {
|
|
722
|
+
if (event.key !== 'Enter' || !event.isTrusted) return;
|
|
723
|
+
guard.sawKeyDown = true;
|
|
724
|
+
const state = safeCheck();
|
|
725
|
+
if (!state.contextMatches || !state.focused || !state.attachmentsReady) cancel(event, state);
|
|
726
|
+
};
|
|
727
|
+
const onKeyUp = event => {
|
|
728
|
+
if (event.key === 'Enter' && event.isTrusted && guard.blocked) { cancel(event, guard.blocked); detach(); }
|
|
729
|
+
};
|
|
730
|
+
const onClick = event => {
|
|
731
|
+
if (!guard.sawKeyDown || !(event.target instanceof Node) ||
|
|
732
|
+
!(button.contains(event.target) || event.target instanceof Element && event.target.closest('button[data-testid="send-button"]'))) return;
|
|
733
|
+
const state = safeCheck();
|
|
734
|
+
if (guard.blocked || !state.contextMatches || !state.focused || !state.attachmentsReady) cancel(event, guard.blocked ?? state);
|
|
735
|
+
detach();
|
|
736
|
+
};
|
|
737
|
+
guard.cleanup = () => { detach(); if (window.__oracleAttachmentDispatchGuard === guard) delete window.__oracleAttachmentDispatchGuard; };
|
|
738
|
+
window.__oracleAttachmentDispatchGuard = guard;
|
|
739
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
740
|
+
window.addEventListener('keyup', onKeyUp, true);
|
|
741
|
+
window.addEventListener('click', onClick, true);
|
|
742
|
+
return snapshot;
|
|
743
|
+
})()`,
|
|
744
|
+
returnByValue: true,
|
|
745
|
+
});
|
|
746
|
+
const snapshot = boundary?.result?.value;
|
|
747
|
+
assertComposerNavigationSnapshot(attachmentNavigationUrl, snapshot);
|
|
748
|
+
if (snapshot?.focused !== true || snapshot.attachmentsReady !== true)
|
|
749
|
+
return false;
|
|
750
|
+
await Input.dispatchKeyEvent({
|
|
751
|
+
type: "keyDown",
|
|
752
|
+
...ENTER_KEY_EVENT,
|
|
753
|
+
text: ENTER_KEY_TEXT,
|
|
754
|
+
unmodifiedText: ENTER_KEY_TEXT,
|
|
755
|
+
});
|
|
756
|
+
await Input.dispatchKeyEvent({ type: "keyUp", ...ENTER_KEY_EVENT });
|
|
757
|
+
}
|
|
758
|
+
finally {
|
|
759
|
+
const result = await withoutBrowserCancellation(() => Runtime.evaluate({
|
|
760
|
+
expression: `(() => {
|
|
761
|
+
const guard = window.__oracleAttachmentDispatchGuard;
|
|
762
|
+
if (guard?.id !== ${JSON.stringify(guardId)}) return null;
|
|
763
|
+
const summary = { sawKeyDown: guard.sawKeyDown, blocked: guard.blocked };
|
|
764
|
+
guard.cleanup(); return summary;
|
|
765
|
+
})()`,
|
|
766
|
+
returnByValue: true,
|
|
767
|
+
}).catch(() => undefined));
|
|
768
|
+
delivery = result?.result?.value;
|
|
769
|
+
}
|
|
770
|
+
if (delivery?.blocked) {
|
|
771
|
+
assertComposerNavigationSnapshot(attachmentNavigationUrl, delivery.blocked);
|
|
772
|
+
throw new BrowserAutomationError("Attachment focus or upload readiness changed at dispatch; the send was stopped.", {
|
|
773
|
+
stage: "submit-prompt",
|
|
774
|
+
code: "attachment-send-not-ready",
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (delivery?.sawKeyDown !== true) {
|
|
778
|
+
throw new BrowserAutomationError("Attachment input delivery could not be verified; do not retry automatically.", {
|
|
779
|
+
stage: "submit-prompt",
|
|
780
|
+
code: "attachment-send-ambiguous",
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
logger?.("Activated exact attachment send button via keyboard");
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
async function dismissOpenComposerPlusMenu(Runtime, Input, logger) {
|
|
787
|
+
const probe = await Runtime.evaluate({
|
|
788
|
+
expression: `(() => {
|
|
789
|
+
const selectors = ['#composer-plus-btn', 'button[data-testid="composer-plus-btn"]'];
|
|
790
|
+
const button = selectors
|
|
791
|
+
.map(selector => document.querySelector(selector))
|
|
792
|
+
.find(node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0);
|
|
793
|
+
if (!(button instanceof HTMLElement)) return { status: 'absent' };
|
|
794
|
+
if (button.getAttribute('aria-expanded') !== 'true') return { status: 'closed' };
|
|
795
|
+
button.focus({ preventScroll: true });
|
|
796
|
+
return { status: 'open', focused: document.activeElement === button };
|
|
797
|
+
})()`,
|
|
798
|
+
returnByValue: true,
|
|
799
|
+
});
|
|
800
|
+
const status = probe?.result?.value?.status;
|
|
801
|
+
if (status !== "open") {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
if (!Input || typeof Input.dispatchKeyEvent !== "function") {
|
|
805
|
+
throw new BrowserAutomationError("ChatGPT's attachment menu is still open and Oracle cannot dismiss it safely before sending.", {
|
|
806
|
+
stage: "submit-prompt",
|
|
807
|
+
code: "attachment-menu-dismiss-unavailable",
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
await Input.dispatchKeyEvent({ type: "keyDown", ...ESCAPE_KEY_EVENT });
|
|
811
|
+
await Input.dispatchKeyEvent({ type: "keyUp", ...ESCAPE_KEY_EVENT });
|
|
812
|
+
const deadline = Date.now() + 2_000;
|
|
813
|
+
while (Date.now() < deadline) {
|
|
814
|
+
const state = await Runtime.evaluate({
|
|
815
|
+
expression: `(() => {
|
|
816
|
+
const selectors = ['#composer-plus-btn', 'button[data-testid="composer-plus-btn"]'];
|
|
817
|
+
return !selectors.some(selector =>
|
|
818
|
+
document.querySelector(selector)?.getAttribute?.('aria-expanded') === 'true'
|
|
819
|
+
);
|
|
820
|
+
})()`,
|
|
821
|
+
returnByValue: true,
|
|
822
|
+
});
|
|
823
|
+
if (state?.result?.value === true) {
|
|
824
|
+
logger?.("Closed attachment menu before send");
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
await delay(50);
|
|
828
|
+
}
|
|
829
|
+
throw new BrowserAutomationError("ChatGPT's attachment menu did not close before send; prompt was left staged.", {
|
|
830
|
+
stage: "submit-prompt",
|
|
831
|
+
code: "attachment-menu-stuck-open",
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async function activatePageForTrustedInput(Page, logger) {
|
|
835
|
+
if (!Page || typeof Page.bringToFront !== "function") {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
await Page.bringToFront().catch((error) => {
|
|
839
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
840
|
+
logger?.(`[browser] Could not bring ChatGPT target to front before trusted click: ${message}`);
|
|
841
|
+
});
|
|
842
|
+
await delay(150);
|
|
843
|
+
}
|
|
662
844
|
async function clickTrustedPoint(Runtime, Input, x, y) {
|
|
663
845
|
if (Input && typeof Input.dispatchMouseEvent === "function") {
|
|
664
846
|
await Input.dispatchMouseEvent({ type: "mouseMoved", x, y });
|
|
@@ -814,14 +996,8 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
814
996
|
logger(`Prompt commit check failed; latest state: ${probe ? JSON.stringify(probe) : "unavailable"}`);
|
|
815
997
|
await logDomFailure(Runtime, logger, "prompt-commit");
|
|
816
998
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
stage: "submit-prompt",
|
|
820
|
-
code: "prompt-too-large",
|
|
821
|
-
promptLength: prompt.trim().length,
|
|
822
|
-
timeoutMs,
|
|
823
|
-
});
|
|
824
|
-
}
|
|
999
|
+
// Input was already dispatched. Even a large staged prompt may commit late;
|
|
1000
|
+
// classifying this as truncation would let the runner submit an upload fallback.
|
|
825
1001
|
throw new BrowserAutomationError("Prompt did not appear in conversation before timeout (send may have failed)", {
|
|
826
1002
|
stage: "submit-prompt",
|
|
827
1003
|
code: "prompt-commit-timeout",
|
|
@@ -849,7 +1025,9 @@ function summarizeCommitProbe(probe) {
|
|
|
849
1025
|
}
|
|
850
1026
|
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
851
1027
|
export const __test__ = {
|
|
1028
|
+
activateExactAttachmentSendButton,
|
|
852
1029
|
attemptSendButton,
|
|
1030
|
+
dismissOpenComposerPlusMenu,
|
|
853
1031
|
sendButtonTimeoutMs,
|
|
854
1032
|
verifyPromptCommitted,
|
|
855
1033
|
};
|