@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.
- package/LICENSE +1 -1
- package/README.md +107 -49
- package/dist/bin/oracle-cli.js +551 -410
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/archiveConversation.js +224 -0
- package/dist/src/browser/actions/assistantResponse.js +175 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +342 -119
- package/dist/src/browser/actions/navigation.js +183 -137
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +152 -91
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +276 -63
- package/dist/src/browser/config.js +59 -16
- package/dist/src/browser/constants.js +25 -12
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +250 -77
- package/dist/src/browser/domDebug.js +50 -1
- package/dist/src/browser/index.js +1559 -692
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +127 -42
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +178 -73
- package/dist/src/browser/reattachHelpers.js +32 -27
- package/dist/src/browser/sessionRunner.js +89 -25
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +24 -20
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +102 -48
- package/dist/src/cli/browserDefaults.js +51 -26
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +62 -26
- package/dist/src/cli/duplicatePromptGuard.js +12 -4
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +131 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +82 -21
- package/dist/src/cli/sessionDisplay.js +213 -87
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +149 -95
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +18 -12
- package/dist/src/mcp/tools/consult.js +246 -67
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +12 -5
- package/dist/src/mcp/utils.js +21 -8
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +160 -135
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +78 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +67 -62
- package/vendor/oracle-notifier/README.md +2 -0
- 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/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/profileSync.js +0 -141
- /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
|
|
2
|
-
import { delay } from
|
|
3
|
-
import { logDomFailure } from
|
|
4
|
-
import { buildClickDispatcher } from
|
|
5
|
-
import { BrowserAutomationError } from
|
|
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:
|
|
8
|
-
code:
|
|
7
|
+
key: "Enter",
|
|
8
|
+
code: "Enter",
|
|
9
9
|
windowsVirtualKeyCode: 13,
|
|
10
10
|
nativeVirtualKeyCode: 13,
|
|
11
11
|
};
|
|
12
|
-
const ENTER_KEY_TEXT =
|
|
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,
|
|
67
|
-
throw new Error(
|
|
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,
|
|
162
|
-
throw new BrowserAutomationError(
|
|
163
|
-
stage:
|
|
164
|
-
code:
|
|
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:
|
|
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:
|
|
178
|
+
type: "keyUp",
|
|
179
179
|
...ENTER_KEY_EVENT,
|
|
180
180
|
});
|
|
181
|
-
logger(
|
|
181
|
+
logger("Submitted prompt via Enter key");
|
|
182
182
|
}
|
|
183
183
|
else {
|
|
184
|
-
logger(
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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
|
-
'[
|
|
276
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
Array.from((
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() +
|
|
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 ===
|
|
386
|
+
if (result.value === "clicked") {
|
|
336
387
|
return true;
|
|
337
388
|
}
|
|
338
|
-
if (result.value ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
511
|
+
const baselineUnknown = typeof info?.baseline === "number" ? info.baseline < 0 : baselineLiteral < 0;
|
|
454
512
|
if (matchesPrompt && (baselineUnknown || info?.hasNewTurn)) {
|
|
455
|
-
return typeof turnsCount ===
|
|
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 ===
|
|
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
|
-
})
|
|
470
|
-
|
|
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(
|
|
474
|
-
stage:
|
|
475
|
-
code:
|
|
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(
|
|
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
|
|
2
|
-
import { FILE_INPUT_SELECTORS } from
|
|
3
|
-
import { waitForAttachmentVisible } from
|
|
4
|
-
import { delay } from
|
|
5
|
-
import { logDomFailure } from
|
|
6
|
-
import { transferAttachmentViaDataTransfer } from
|
|
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(
|
|
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,
|
|
29
|
-
throw new Error(
|
|
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(
|
|
36
|
+
logger("Attachment queued");
|
|
37
37
|
}
|