ask-pro 0.1.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/.codex-plugin/plugin.json +30 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/assets/ask-pro_logo.png +0 -0
- package/dist/bin/ask-pro-cli.js +507 -0
- package/dist/scripts/run-cli.js +27 -0
- package/dist/src/ask-pro/atomicWrite.js +26 -0
- package/dist/src/ask-pro/browserRunner.js +796 -0
- package/dist/src/ask-pro/responseZip.js +349 -0
- package/dist/src/ask-pro/session.js +662 -0
- package/dist/src/ask-pro/sessionControllerLease.js +64 -0
- package/dist/src/ask-pro/toon.js +26 -0
- package/dist/src/ask-pro/zip.js +85 -0
- package/dist/src/browser/actions/assistantResponse.js +1245 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
- package/dist/src/browser/actions/attachments.js +1720 -0
- package/dist/src/browser/actions/composerSendReadiness.js +369 -0
- package/dist/src/browser/actions/domEvents.js +31 -0
- package/dist/src/browser/actions/inputGuard.js +52 -0
- package/dist/src/browser/actions/modelPickerDom.js +68 -0
- package/dist/src/browser/actions/modelSelection.js +576 -0
- package/dist/src/browser/actions/navigation.js +510 -0
- package/dist/src/browser/actions/promptComposer.js +824 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
- package/dist/src/browser/actions/thinkingStatus.js +408 -0
- package/dist/src/browser/actions/thinkingTime.js +635 -0
- package/dist/src/browser/actions/windowState.js +47 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptModelCatalog.js +321 -0
- package/dist/src/browser/chromeLifecycle.js +807 -0
- package/dist/src/browser/config.js +110 -0
- package/dist/src/browser/constants.js +85 -0
- package/dist/src/browser/cookies.js +191 -0
- package/dist/src/browser/detect.js +337 -0
- package/dist/src/browser/domDebug.js +72 -0
- package/dist/src/browser/errors.js +20 -0
- package/dist/src/browser/format.js +16 -0
- package/dist/src/browser/index.js +2631 -0
- package/dist/src/browser/language.js +97 -0
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/profilePaths.js +282 -0
- package/dist/src/browser/profileState.js +413 -0
- package/dist/src/browser/providerDomFlow.js +17 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +50 -0
- package/dist/src/browser/reattach.js +534 -0
- package/dist/src/browser/reattachHelpers.js +387 -0
- package/dist/src/browser/utils.js +122 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/version.js +39 -0
- package/package.json +114 -0
- package/scripts/refresh-local-plugin.mjs +179 -0
- package/scripts/refresh-local-plugin.ps1 +93 -0
- package/skills/ask-pro/SKILL.md +181 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, 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 "../errors.js";
|
|
6
|
+
import { buildComposerSendClickExpression, buildComposerSendReadinessExpression, hasAttachmentCompletionEvidence, readComposerSendReadiness, summarizeComposerSendReadiness, } from "./composerSendReadiness.js";
|
|
7
|
+
const ENTER_KEY_EVENT = {
|
|
8
|
+
key: "Enter",
|
|
9
|
+
code: "Enter",
|
|
10
|
+
windowsVirtualKeyCode: 13,
|
|
11
|
+
nativeVirtualKeyCode: 13,
|
|
12
|
+
};
|
|
13
|
+
const ENTER_KEY_TEXT = "\r";
|
|
14
|
+
const PROMPT_TRUNCATION_CHECK_THRESHOLD = 20_000;
|
|
15
|
+
const PROMPT_TRUNCATION_TOLERANCE = 500;
|
|
16
|
+
const COMPOSER_HEALTH_SENTINEL = "__ask_pro_healthcheck__";
|
|
17
|
+
function normalizeComposerSnapshot(value) {
|
|
18
|
+
return {
|
|
19
|
+
editorText: typeof value?.editorText === "string" ? value.editorText : "",
|
|
20
|
+
fallbackValue: typeof value?.fallbackValue === "string" ? value.fallbackValue : "",
|
|
21
|
+
activeValue: typeof value?.activeValue === "string" ? value.activeValue : "",
|
|
22
|
+
activeExists: Boolean(value?.activeExists),
|
|
23
|
+
activeVisible: Boolean(value?.activeVisible),
|
|
24
|
+
activeDisabled: Boolean(value?.activeDisabled),
|
|
25
|
+
activeReadOnly: Boolean(value?.activeReadOnly),
|
|
26
|
+
activeTagName: typeof value?.activeTagName === "string" ? value.activeTagName : "",
|
|
27
|
+
activeRole: typeof value?.activeRole === "string" ? value.activeRole : "",
|
|
28
|
+
href: typeof value?.href === "string" ? value.href : "",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function buildComposerSnapshotExpression() {
|
|
32
|
+
return `(() => {
|
|
33
|
+
const editor = document.querySelector(${JSON.stringify(PROMPT_PRIMARY_SELECTOR)});
|
|
34
|
+
const fallback = document.querySelector(${JSON.stringify(PROMPT_FALLBACK_SELECTOR)});
|
|
35
|
+
const inputSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
36
|
+
const readValue = (node) => {
|
|
37
|
+
if (!node) return '';
|
|
38
|
+
if (node instanceof HTMLTextAreaElement) return node.value ?? '';
|
|
39
|
+
return node.innerText ?? '';
|
|
40
|
+
};
|
|
41
|
+
const isVisible = (node) => {
|
|
42
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') return false;
|
|
43
|
+
const rect = node.getBoundingClientRect();
|
|
44
|
+
return rect.width > 0 && rect.height > 0;
|
|
45
|
+
};
|
|
46
|
+
const candidates = inputSelectors
|
|
47
|
+
.map((selector) => document.querySelector(selector))
|
|
48
|
+
.filter((node) => Boolean(node));
|
|
49
|
+
const active = candidates.find((node) => isVisible(node)) || candidates[0] || null;
|
|
50
|
+
return {
|
|
51
|
+
editorText: editor?.innerText ?? '',
|
|
52
|
+
fallbackValue: fallback instanceof HTMLTextAreaElement ? fallback.value ?? '' : '',
|
|
53
|
+
activeValue: active ? readValue(active) : '',
|
|
54
|
+
activeExists: Boolean(active),
|
|
55
|
+
activeVisible: isVisible(active),
|
|
56
|
+
activeDisabled: Boolean(
|
|
57
|
+
active &&
|
|
58
|
+
(((active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) &&
|
|
59
|
+
active.disabled) ||
|
|
60
|
+
active.getAttribute?.('aria-disabled') === 'true' ||
|
|
61
|
+
active.getAttribute?.('disabled') !== null),
|
|
62
|
+
),
|
|
63
|
+
activeReadOnly: Boolean(
|
|
64
|
+
active &&
|
|
65
|
+
(((active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) &&
|
|
66
|
+
active.readOnly) ||
|
|
67
|
+
active.getAttribute?.('readonly') !== null ||
|
|
68
|
+
active.getAttribute?.('contenteditable') === 'false'),
|
|
69
|
+
),
|
|
70
|
+
activeTagName: active?.tagName?.toLowerCase?.() ?? '',
|
|
71
|
+
activeRole: active?.getAttribute?.('role') ?? '',
|
|
72
|
+
href: typeof location === 'object' && location.href ? location.href : '',
|
|
73
|
+
};
|
|
74
|
+
})()`;
|
|
75
|
+
}
|
|
76
|
+
export async function readComposerSnapshot(runtime) {
|
|
77
|
+
const result = await runtime.evaluate({
|
|
78
|
+
expression: buildComposerSnapshotExpression(),
|
|
79
|
+
returnByValue: true,
|
|
80
|
+
});
|
|
81
|
+
return normalizeComposerSnapshot(result.result?.value);
|
|
82
|
+
}
|
|
83
|
+
async function ensureComposerHealthy(runtime, logger) {
|
|
84
|
+
const result = await runtime.evaluate({
|
|
85
|
+
expression: `(() => {
|
|
86
|
+
${buildClickDispatcher()}
|
|
87
|
+
const selectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
88
|
+
const sentinel = ${JSON.stringify(COMPOSER_HEALTH_SENTINEL)};
|
|
89
|
+
const isVisible = (node) => {
|
|
90
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') return false;
|
|
91
|
+
const rect = node.getBoundingClientRect();
|
|
92
|
+
return rect.width > 0 && rect.height > 0;
|
|
93
|
+
};
|
|
94
|
+
const readValue = (node) => {
|
|
95
|
+
if (!node) return '';
|
|
96
|
+
if (node instanceof HTMLTextAreaElement) return node.value ?? '';
|
|
97
|
+
return node.innerText ?? '';
|
|
98
|
+
};
|
|
99
|
+
const candidates = selectors
|
|
100
|
+
.map((selector) => document.querySelector(selector))
|
|
101
|
+
.filter((node) => Boolean(node));
|
|
102
|
+
const active = candidates.find((node) => isVisible(node)) || candidates[0] || null;
|
|
103
|
+
const href = typeof location === 'object' && location.href ? location.href : '';
|
|
104
|
+
if (!active) {
|
|
105
|
+
return {
|
|
106
|
+
healthy: false,
|
|
107
|
+
reason: 'missing-active-input',
|
|
108
|
+
activeExists: false,
|
|
109
|
+
activeVisible: false,
|
|
110
|
+
activeDisabled: false,
|
|
111
|
+
activeReadOnly: false,
|
|
112
|
+
activeTagName: '',
|
|
113
|
+
activeRole: '',
|
|
114
|
+
href,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
dispatchClickSequence(active);
|
|
118
|
+
if (typeof active.focus === 'function') {
|
|
119
|
+
active.focus();
|
|
120
|
+
}
|
|
121
|
+
const activeDisabled =
|
|
122
|
+
((active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) &&
|
|
123
|
+
active.disabled) ||
|
|
124
|
+
active.getAttribute?.('aria-disabled') === 'true' ||
|
|
125
|
+
active.getAttribute?.('disabled') !== null;
|
|
126
|
+
const activeReadOnly =
|
|
127
|
+
((active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) &&
|
|
128
|
+
active.readOnly) ||
|
|
129
|
+
active.getAttribute?.('readonly') !== null ||
|
|
130
|
+
active.getAttribute?.('contenteditable') === 'false';
|
|
131
|
+
if (activeDisabled || activeReadOnly) {
|
|
132
|
+
return {
|
|
133
|
+
healthy: false,
|
|
134
|
+
reason: activeDisabled ? 'active-input-disabled' : 'active-input-readonly',
|
|
135
|
+
activeExists: true,
|
|
136
|
+
activeVisible: isVisible(active),
|
|
137
|
+
activeDisabled: Boolean(activeDisabled),
|
|
138
|
+
activeReadOnly: Boolean(activeReadOnly),
|
|
139
|
+
activeTagName: active.tagName?.toLowerCase?.() ?? '',
|
|
140
|
+
activeRole: active.getAttribute?.('role') ?? '',
|
|
141
|
+
href,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const before = readValue(active);
|
|
145
|
+
const probe = before + sentinel;
|
|
146
|
+
let after = before;
|
|
147
|
+
try {
|
|
148
|
+
if (active instanceof HTMLTextAreaElement) {
|
|
149
|
+
active.value = probe;
|
|
150
|
+
active.dispatchEvent(new InputEvent('input', { bubbles: true, data: sentinel, inputType: 'insertText' }));
|
|
151
|
+
active.dispatchEvent(new Event('change', { bubbles: true }));
|
|
152
|
+
after = active.value ?? '';
|
|
153
|
+
active.value = before;
|
|
154
|
+
active.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
155
|
+
active.dispatchEvent(new Event('change', { bubbles: true }));
|
|
156
|
+
} else {
|
|
157
|
+
active.textContent = probe;
|
|
158
|
+
active.dispatchEvent(new InputEvent('input', { bubbles: true, data: sentinel, inputType: 'insertText' }));
|
|
159
|
+
after = readValue(active);
|
|
160
|
+
active.textContent = before;
|
|
161
|
+
active.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return {
|
|
165
|
+
healthy: false,
|
|
166
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
167
|
+
activeExists: true,
|
|
168
|
+
activeVisible: isVisible(active),
|
|
169
|
+
activeDisabled: Boolean(activeDisabled),
|
|
170
|
+
activeReadOnly: Boolean(activeReadOnly),
|
|
171
|
+
activeTagName: active.tagName?.toLowerCase?.() ?? '',
|
|
172
|
+
activeRole: active.getAttribute?.('role') ?? '',
|
|
173
|
+
href,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
healthy: typeof after === 'string' && after.includes(sentinel),
|
|
178
|
+
reason:
|
|
179
|
+
typeof after === 'string' && after.includes(sentinel)
|
|
180
|
+
? ''
|
|
181
|
+
: 'active-input-roundtrip-failed',
|
|
182
|
+
activeExists: true,
|
|
183
|
+
activeVisible: isVisible(active),
|
|
184
|
+
activeDisabled: Boolean(activeDisabled),
|
|
185
|
+
activeReadOnly: Boolean(activeReadOnly),
|
|
186
|
+
activeTagName: active.tagName?.toLowerCase?.() ?? '',
|
|
187
|
+
activeRole: active.getAttribute?.('role') ?? '',
|
|
188
|
+
href,
|
|
189
|
+
};
|
|
190
|
+
})()`,
|
|
191
|
+
returnByValue: true,
|
|
192
|
+
awaitPromise: true,
|
|
193
|
+
});
|
|
194
|
+
const state = result.result?.value;
|
|
195
|
+
if (state?.healthy) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await logDomFailure(runtime, logger, "dead-composer");
|
|
199
|
+
throw new BrowserAutomationError("ChatGPT composer is present but not accepting input.", {
|
|
200
|
+
stage: "submit-prompt",
|
|
201
|
+
code: "dead-composer",
|
|
202
|
+
composerState: {
|
|
203
|
+
reason: state?.reason ?? "unknown",
|
|
204
|
+
activeExists: Boolean(state?.activeExists),
|
|
205
|
+
activeVisible: Boolean(state?.activeVisible),
|
|
206
|
+
activeDisabled: Boolean(state?.activeDisabled),
|
|
207
|
+
activeReadOnly: Boolean(state?.activeReadOnly),
|
|
208
|
+
activeTagName: state?.activeTagName ?? "",
|
|
209
|
+
activeRole: state?.activeRole ?? "",
|
|
210
|
+
href: state?.href ?? "",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function isPromptTooLarge(promptLength, observedLength) {
|
|
215
|
+
return (promptLength >= PROMPT_TRUNCATION_CHECK_THRESHOLD &&
|
|
216
|
+
observedLength > 0 &&
|
|
217
|
+
observedLength < promptLength - PROMPT_TRUNCATION_TOLERANCE);
|
|
218
|
+
}
|
|
219
|
+
export async function submitPrompt(deps, prompt, logger) {
|
|
220
|
+
const { runtime, input } = deps;
|
|
221
|
+
await waitForDomReady(runtime, logger, deps.inputTimeoutMs ?? undefined);
|
|
222
|
+
await ensureComposerHealthy(runtime, logger);
|
|
223
|
+
const encodedPrompt = JSON.stringify(prompt);
|
|
224
|
+
const focusResult = await runtime.evaluate({
|
|
225
|
+
expression: `(() => {
|
|
226
|
+
${buildClickDispatcher()}
|
|
227
|
+
const SELECTORS = ${JSON.stringify(INPUT_SELECTORS)};
|
|
228
|
+
const isVisible = (node) => {
|
|
229
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
const rect = node.getBoundingClientRect();
|
|
233
|
+
return rect.width > 0 && rect.height > 0;
|
|
234
|
+
};
|
|
235
|
+
const focusNode = (node) => {
|
|
236
|
+
if (!node) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
// Learned: React/ProseMirror require a real click + focus + selection for inserts to stick.
|
|
240
|
+
dispatchClickSequence(node);
|
|
241
|
+
if (typeof node.focus === 'function') {
|
|
242
|
+
node.focus();
|
|
243
|
+
}
|
|
244
|
+
const doc = node.ownerDocument;
|
|
245
|
+
const selection = doc?.getSelection?.();
|
|
246
|
+
if (selection) {
|
|
247
|
+
const range = doc.createRange();
|
|
248
|
+
range.selectNodeContents(node);
|
|
249
|
+
range.collapse(false);
|
|
250
|
+
selection.removeAllRanges();
|
|
251
|
+
selection.addRange(range);
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const candidates = [];
|
|
257
|
+
for (const selector of SELECTORS) {
|
|
258
|
+
const node = document.querySelector(selector);
|
|
259
|
+
if (node) {
|
|
260
|
+
candidates.push(node);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const preferred = candidates.find((node) => isVisible(node)) || candidates[0];
|
|
264
|
+
if (preferred && focusNode(preferred)) {
|
|
265
|
+
return { focused: true };
|
|
266
|
+
}
|
|
267
|
+
return { focused: false };
|
|
268
|
+
})()`,
|
|
269
|
+
returnByValue: true,
|
|
270
|
+
awaitPromise: true,
|
|
271
|
+
});
|
|
272
|
+
if (!focusResult.result?.value?.focused) {
|
|
273
|
+
await logDomFailure(runtime, logger, "focus-textarea");
|
|
274
|
+
throw new Error("Failed to focus prompt textarea");
|
|
275
|
+
}
|
|
276
|
+
await input.insertText({ text: prompt });
|
|
277
|
+
// Some pages (notably ChatGPT when subscriptions/widgets load) need a brief settle
|
|
278
|
+
// before the send button becomes enabled; give it a short breather to avoid races.
|
|
279
|
+
await delay(500);
|
|
280
|
+
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
281
|
+
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
282
|
+
const verification = await readComposerSnapshot(runtime);
|
|
283
|
+
const editorTextTrimmed = verification.editorText.trim();
|
|
284
|
+
const fallbackValueTrimmed = verification.fallbackValue.trim();
|
|
285
|
+
const activeValueTrimmed = verification.activeValue.trim();
|
|
286
|
+
let usedDirectDomWrite = false;
|
|
287
|
+
if (!editorTextTrimmed && !fallbackValueTrimmed && !activeValueTrimmed) {
|
|
288
|
+
// Learned: occasionally Input.insertText doesn't land in the editor; force textContent/value + input events.
|
|
289
|
+
usedDirectDomWrite = true;
|
|
290
|
+
await runtime.evaluate({
|
|
291
|
+
expression: `(() => {
|
|
292
|
+
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
293
|
+
if (fallback) {
|
|
294
|
+
fallback.value = ${encodedPrompt};
|
|
295
|
+
fallback.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
296
|
+
fallback.dispatchEvent(new Event('change', { bubbles: true }));
|
|
297
|
+
}
|
|
298
|
+
const editor = document.querySelector(${primarySelectorLiteral});
|
|
299
|
+
if (editor) {
|
|
300
|
+
editor.textContent = ${encodedPrompt};
|
|
301
|
+
// Nudge ProseMirror to register the textContent write so its state/send-button updates
|
|
302
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
303
|
+
}
|
|
304
|
+
})()`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const promptLength = prompt.length;
|
|
308
|
+
const postVerification = await readComposerSnapshot(runtime);
|
|
309
|
+
const observedEditor = postVerification.editorText;
|
|
310
|
+
const observedFallback = postVerification.fallbackValue;
|
|
311
|
+
const observedActive = postVerification.activeValue;
|
|
312
|
+
const observedLength = Math.max(observedEditor.length, observedFallback.length, observedActive.length);
|
|
313
|
+
const trustedObservedLength = usedDirectDomWrite ? observedActive.length : observedLength;
|
|
314
|
+
if (usedDirectDomWrite && trustedObservedLength === 0 && observedLength > 0) {
|
|
315
|
+
await logDomFailure(runtime, logger, "dead-composer");
|
|
316
|
+
throw new BrowserAutomationError("Prompt text only reached the DOM fallback, not the active composer.", {
|
|
317
|
+
stage: "submit-prompt",
|
|
318
|
+
code: "dead-composer",
|
|
319
|
+
promptLength,
|
|
320
|
+
composerState: postVerification,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (isPromptTooLarge(promptLength, trustedObservedLength)) {
|
|
324
|
+
// Learned: very large prompts can truncate silently; fail fast so we can fall back to file uploads.
|
|
325
|
+
await logDomFailure(runtime, logger, "prompt-too-large");
|
|
326
|
+
throw new BrowserAutomationError("Prompt appears truncated in the composer (likely too large).", {
|
|
327
|
+
stage: "submit-prompt",
|
|
328
|
+
code: "prompt-too-large",
|
|
329
|
+
promptLength,
|
|
330
|
+
observedLength: trustedObservedLength,
|
|
331
|
+
usedDirectDomWrite,
|
|
332
|
+
composerState: postVerification,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames, deps.inputTimeoutMs ?? undefined);
|
|
336
|
+
if (!clicked) {
|
|
337
|
+
const canSubmitViaEnter = await canSubmitPromptViaEnter(runtime);
|
|
338
|
+
if (!canSubmitViaEnter) {
|
|
339
|
+
await logDomFailure(runtime, logger, "unsafe-enter-submit");
|
|
340
|
+
throw new BrowserAutomationError("Refusing Enter-key submit because ChatGPT is showing a stop control or the composer is not focused.", {
|
|
341
|
+
stage: "submit-prompt",
|
|
342
|
+
code: "unsafe-enter-submit",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
await input.dispatchKeyEvent({
|
|
346
|
+
type: "keyDown",
|
|
347
|
+
...ENTER_KEY_EVENT,
|
|
348
|
+
text: ENTER_KEY_TEXT,
|
|
349
|
+
unmodifiedText: ENTER_KEY_TEXT,
|
|
350
|
+
});
|
|
351
|
+
await input.dispatchKeyEvent({
|
|
352
|
+
type: "keyUp",
|
|
353
|
+
...ENTER_KEY_EVENT,
|
|
354
|
+
});
|
|
355
|
+
logger("Submitted prompt via Enter key");
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
logger("Clicked send button");
|
|
359
|
+
}
|
|
360
|
+
await runPostSubmitProtection(runtime, input, logger, deps.afterSubmit);
|
|
361
|
+
const commitTimeoutMs = Math.max(60_000, deps.inputTimeoutMs ?? 0);
|
|
362
|
+
// Learned: the send button can succeed but the turn doesn't appear immediately; verify commit via turns/stop button.
|
|
363
|
+
return await verifyPromptCommitted(runtime, prompt, commitTimeoutMs, logger, deps.baselineTurns ?? undefined);
|
|
364
|
+
}
|
|
365
|
+
async function runPostSubmitProtection(Runtime, Input, logger, afterSubmit) {
|
|
366
|
+
await afterSubmit?.();
|
|
367
|
+
await defocusStopButtonAfterSubmit(Runtime, logger);
|
|
368
|
+
await movePointerAwayFromStopControl(Input, logger);
|
|
369
|
+
}
|
|
370
|
+
async function defocusStopButtonAfterSubmit(Runtime, logger) {
|
|
371
|
+
const stopSelectorLiteral = JSON.stringify(STOP_BUTTON_SELECTOR);
|
|
372
|
+
const deadline = Date.now() + 2_000;
|
|
373
|
+
while (Date.now() < deadline) {
|
|
374
|
+
const { result } = await Runtime.evaluate({
|
|
375
|
+
expression: `(() => {
|
|
376
|
+
const stop = document.querySelector(${stopSelectorLiteral});
|
|
377
|
+
const isVisible = (node) => {
|
|
378
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') return false;
|
|
379
|
+
const rect = node.getBoundingClientRect();
|
|
380
|
+
const style = window.getComputedStyle?.(node);
|
|
381
|
+
return rect.width > 0 && rect.height > 0 && style?.visibility !== 'hidden' && style?.display !== 'none';
|
|
382
|
+
};
|
|
383
|
+
if (!stop || !isVisible(stop)) {
|
|
384
|
+
return { changed: false, reason: 'no-visible-stop' };
|
|
385
|
+
}
|
|
386
|
+
let sink = document.getElementById('__ask_pro_focus_sink__');
|
|
387
|
+
if (!sink) {
|
|
388
|
+
sink = document.createElement('div');
|
|
389
|
+
sink.id = '__ask_pro_focus_sink__';
|
|
390
|
+
sink.tabIndex = -1;
|
|
391
|
+
sink.setAttribute('aria-hidden', 'true');
|
|
392
|
+
sink.style.position = 'fixed';
|
|
393
|
+
sink.style.left = '-10000px';
|
|
394
|
+
sink.style.top = '-10000px';
|
|
395
|
+
sink.style.width = '1px';
|
|
396
|
+
sink.style.height = '1px';
|
|
397
|
+
sink.style.opacity = '0';
|
|
398
|
+
sink.style.pointerEvents = 'none';
|
|
399
|
+
document.body.appendChild(sink);
|
|
400
|
+
}
|
|
401
|
+
const activeBefore = document.activeElement;
|
|
402
|
+
if (activeBefore && typeof activeBefore.blur === 'function') {
|
|
403
|
+
activeBefore.blur();
|
|
404
|
+
}
|
|
405
|
+
sink.focus({ preventScroll: true });
|
|
406
|
+
const active = document.activeElement;
|
|
407
|
+
const stopFocused = Boolean(active && (active === stop || stop.contains(active)));
|
|
408
|
+
const activeLabel = active
|
|
409
|
+
? [active.getAttribute?.('aria-label'), active.getAttribute?.('title'), active.textContent]
|
|
410
|
+
.filter(Boolean)
|
|
411
|
+
.join(' ')
|
|
412
|
+
.trim()
|
|
413
|
+
: '';
|
|
414
|
+
return { changed: active === sink, stopFocused, activeLabel };
|
|
415
|
+
})()`,
|
|
416
|
+
returnByValue: true,
|
|
417
|
+
});
|
|
418
|
+
const value = result.value;
|
|
419
|
+
if (value?.changed) {
|
|
420
|
+
logger?.(value.stopFocused
|
|
421
|
+
? "Moved focus sink after submit, but ChatGPT stop button still reports focused"
|
|
422
|
+
: "Moved focus away from ChatGPT stop button");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (value?.stopFocused) {
|
|
426
|
+
logger?.(`ChatGPT stop button remained focused after defocus attempt${value.activeLabel ? ` (${value.activeLabel})` : ""}`);
|
|
427
|
+
}
|
|
428
|
+
if (value?.reason === "no-visible-stop") {
|
|
429
|
+
await delay(100);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function movePointerAwayFromStopControl(Input, logger) {
|
|
436
|
+
try {
|
|
437
|
+
await Input.dispatchMouseEvent({ type: "mouseMoved", x: 1, y: 1 });
|
|
438
|
+
logger?.("Moved pointer away from ChatGPT stop control");
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// Non-fatal: the CDP input guard is the stronger stop-safety layer.
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async function canSubmitPromptViaEnter(Runtime) {
|
|
445
|
+
const stopSelectorLiteral = JSON.stringify(STOP_BUTTON_SELECTOR);
|
|
446
|
+
const promptSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
|
|
447
|
+
const { result } = await Runtime.evaluate({
|
|
448
|
+
expression: `(() => {
|
|
449
|
+
const stopSelector = ${stopSelectorLiteral};
|
|
450
|
+
const promptSelectors = ${promptSelectorsLiteral};
|
|
451
|
+
const isVisible = (node) => {
|
|
452
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') return false;
|
|
453
|
+
const rect = node.getBoundingClientRect();
|
|
454
|
+
const style = window.getComputedStyle?.(node);
|
|
455
|
+
return rect.width > 0 && rect.height > 0 && style?.visibility !== 'hidden' && style?.display !== 'none';
|
|
456
|
+
};
|
|
457
|
+
if (Array.from(document.querySelectorAll(stopSelector)).some((node) => isVisible(node))) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
const active = document.activeElement;
|
|
461
|
+
if (!active) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
return promptSelectors.some((selector) => active.matches?.(selector) || active.closest?.(selector));
|
|
465
|
+
})()`,
|
|
466
|
+
returnByValue: true,
|
|
467
|
+
});
|
|
468
|
+
return result.value === true;
|
|
469
|
+
}
|
|
470
|
+
export async function clearPromptComposer(Runtime, logger) {
|
|
471
|
+
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
472
|
+
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
473
|
+
const inputSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
|
|
474
|
+
const result = await Runtime.evaluate({
|
|
475
|
+
expression: `(() => {
|
|
476
|
+
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
477
|
+
const editor = document.querySelector(${primarySelectorLiteral});
|
|
478
|
+
const inputSelectors = ${inputSelectorsLiteral};
|
|
479
|
+
let cleared = false;
|
|
480
|
+
if (fallback) {
|
|
481
|
+
fallback.value = '';
|
|
482
|
+
fallback.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
483
|
+
fallback.dispatchEvent(new Event('change', { bubbles: true }));
|
|
484
|
+
cleared = true;
|
|
485
|
+
}
|
|
486
|
+
if (editor) {
|
|
487
|
+
editor.textContent = '';
|
|
488
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
489
|
+
cleared = true;
|
|
490
|
+
}
|
|
491
|
+
const nodes = inputSelectors
|
|
492
|
+
.map((selector) => document.querySelector(selector))
|
|
493
|
+
.filter((node) => Boolean(node));
|
|
494
|
+
for (const node of nodes) {
|
|
495
|
+
if (!node) continue;
|
|
496
|
+
if (node instanceof HTMLTextAreaElement) {
|
|
497
|
+
node.value = '';
|
|
498
|
+
node.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
499
|
+
node.dispatchEvent(new Event('change', { bubbles: true }));
|
|
500
|
+
cleared = true;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (node.isContentEditable || node.getAttribute('contenteditable') === 'true') {
|
|
504
|
+
node.textContent = '';
|
|
505
|
+
node.dispatchEvent(new InputEvent('input', { bubbles: true, data: '', inputType: 'deleteByCut' }));
|
|
506
|
+
cleared = true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return { cleared };
|
|
510
|
+
})()`,
|
|
511
|
+
returnByValue: true,
|
|
512
|
+
});
|
|
513
|
+
if (!result.result?.value?.cleared) {
|
|
514
|
+
await logDomFailure(Runtime, logger, "clear-composer");
|
|
515
|
+
throw new Error("Failed to clear prompt composer");
|
|
516
|
+
}
|
|
517
|
+
await delay(250);
|
|
518
|
+
}
|
|
519
|
+
async function waitForDomReady(Runtime, logger, timeoutMs = 10_000) {
|
|
520
|
+
const deadline = Date.now() + timeoutMs;
|
|
521
|
+
while (Date.now() < deadline) {
|
|
522
|
+
const { result } = await Runtime.evaluate({
|
|
523
|
+
expression: `(() => {
|
|
524
|
+
const ready = document.readyState === 'complete';
|
|
525
|
+
const composer = document.querySelector('[data-testid*="composer"]') || document.querySelector('form');
|
|
526
|
+
const fileInput = document.querySelector('input[type="file"]');
|
|
527
|
+
return { ready, composer: Boolean(composer), fileInput: Boolean(fileInput) };
|
|
528
|
+
})()`,
|
|
529
|
+
returnByValue: true,
|
|
530
|
+
});
|
|
531
|
+
const value = result?.value;
|
|
532
|
+
if (value?.ready && value.composer) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
await delay(150);
|
|
536
|
+
}
|
|
537
|
+
logger?.(`Page did not reach ready/composer state within ${timeoutMs}ms; continuing cautiously.`);
|
|
538
|
+
}
|
|
539
|
+
export function buildComposerSendReadinessExpressionForTest() {
|
|
540
|
+
return buildComposerSendReadinessExpression();
|
|
541
|
+
}
|
|
542
|
+
async function attemptSendButton(Runtime, logger, attachmentNames, inputTimeoutMs) {
|
|
543
|
+
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
544
|
+
const deadline = Date.now() +
|
|
545
|
+
(needAttachment ? Math.max(15_000, Math.min(inputTimeoutMs ?? 30_000, 30_000)) : 8_000);
|
|
546
|
+
let readinessStableSince = null;
|
|
547
|
+
let lastReadinessLog = 0;
|
|
548
|
+
while (Date.now() < deadline) {
|
|
549
|
+
if (needAttachment) {
|
|
550
|
+
const readiness = await readComposerSendReadiness(Runtime);
|
|
551
|
+
if (logger?.verbose) {
|
|
552
|
+
const now = Date.now();
|
|
553
|
+
if (now - lastReadinessLog > 3000) {
|
|
554
|
+
lastReadinessLog = now;
|
|
555
|
+
logger(`Attachment send readiness: ${JSON.stringify(summarizeComposerSendReadiness(readiness, attachmentNames))}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const canSend = readiness &&
|
|
559
|
+
readiness.state === "ready" &&
|
|
560
|
+
hasAttachmentCompletionEvidence(readiness, attachmentNames);
|
|
561
|
+
if (!canSend) {
|
|
562
|
+
readinessStableSince = null;
|
|
563
|
+
await delay(150);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (readinessStableSince === null) {
|
|
567
|
+
readinessStableSince = Date.now();
|
|
568
|
+
}
|
|
569
|
+
const stableThresholdMs = readiness.uploading ? 3000 : 750;
|
|
570
|
+
if (Date.now() - readinessStableSince < stableThresholdMs) {
|
|
571
|
+
await delay(150);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const { result } = await Runtime.evaluate({
|
|
576
|
+
expression: buildComposerSendClickExpression(),
|
|
577
|
+
returnByValue: true,
|
|
578
|
+
});
|
|
579
|
+
if (result.value === "clicked") {
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
if (result.value === "stop-button") {
|
|
583
|
+
throw new BrowserAutomationError("Refusing to click ChatGPT stop control as a send button.", {
|
|
584
|
+
stage: "submit-prompt",
|
|
585
|
+
code: "unsafe-stop-click",
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
if (!needAttachment && result.value === "missing") {
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
await delay(100);
|
|
592
|
+
}
|
|
593
|
+
if (needAttachment) {
|
|
594
|
+
const readiness = await readComposerSendReadiness(Runtime).catch(() => null);
|
|
595
|
+
logger?.(`Attachment send readiness timed out: ${JSON.stringify(summarizeComposerSendReadiness(readiness, attachmentNames))}`);
|
|
596
|
+
throw new BrowserAutomationError("Attachments never reached a send-ready state before timeout.", {
|
|
597
|
+
stage: "submit-prompt",
|
|
598
|
+
code: "attachment-send-not-ready",
|
|
599
|
+
readiness: summarizeComposerSendReadiness(readiness, attachmentNames),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselineTurns) {
|
|
605
|
+
const deadline = Date.now() + timeoutMs;
|
|
606
|
+
const encodedPrompt = JSON.stringify(prompt.trim());
|
|
607
|
+
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
608
|
+
const fallbackSelectorLiteral = JSON.stringify(PROMPT_FALLBACK_SELECTOR);
|
|
609
|
+
const inputSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
|
|
610
|
+
const stopSelectorLiteral = JSON.stringify(STOP_BUTTON_SELECTOR);
|
|
611
|
+
const assistantSelectorLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
612
|
+
const turnSelectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
613
|
+
let baseline = typeof baselineTurns === "number" && Number.isFinite(baselineTurns) && baselineTurns >= 0
|
|
614
|
+
? Math.floor(baselineTurns)
|
|
615
|
+
: null;
|
|
616
|
+
if (baseline === null) {
|
|
617
|
+
try {
|
|
618
|
+
const { result } = await Runtime.evaluate({
|
|
619
|
+
expression: `document.querySelectorAll(${turnSelectorLiteral}).length`,
|
|
620
|
+
returnByValue: true,
|
|
621
|
+
});
|
|
622
|
+
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
623
|
+
if (Number.isFinite(raw)) {
|
|
624
|
+
baseline = Math.max(0, Math.floor(raw));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
// ignore; baseline stays unknown
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const baselineLiteral = baseline ?? -1;
|
|
632
|
+
// Learned: ChatGPT can echo/format text; normalize markdown and use prefix matches to detect the sent prompt.
|
|
633
|
+
const script = `(() => {
|
|
634
|
+
const editor = document.querySelector(${primarySelectorLiteral});
|
|
635
|
+
const fallback = document.querySelector(${fallbackSelectorLiteral});
|
|
636
|
+
const inputSelectors = ${inputSelectorsLiteral};
|
|
637
|
+
const normalize = (value) => {
|
|
638
|
+
let text = value?.toLowerCase?.() ?? '';
|
|
639
|
+
// Strip markdown *markers* but keep content (ChatGPT renders fence markers differently).
|
|
640
|
+
text = text.replace(/\`\`\`[^\\n]*\\n([\\s\\S]*?)\`\`\`/g, ' $1 ');
|
|
641
|
+
text = text.replace(/\`\`\`/g, ' ');
|
|
642
|
+
text = text.replace(/\`([^\`]*)\`/g, '$1');
|
|
643
|
+
return text.replace(/\\s+/g, ' ').trim();
|
|
644
|
+
};
|
|
645
|
+
const normalizedPrompt = normalize(${encodedPrompt});
|
|
646
|
+
const normalizedPromptPrefix = normalizedPrompt.slice(0, 120);
|
|
647
|
+
const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
|
|
648
|
+
const articles = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
649
|
+
const normalizedTurns = articles.map((node) => normalize(node?.innerText));
|
|
650
|
+
const readValue = (node) => {
|
|
651
|
+
if (!node) return '';
|
|
652
|
+
if (node instanceof HTMLTextAreaElement) return node.value ?? '';
|
|
653
|
+
return node.innerText ?? '';
|
|
654
|
+
};
|
|
655
|
+
const isVisible = (node) => {
|
|
656
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') return false;
|
|
657
|
+
const rect = node.getBoundingClientRect();
|
|
658
|
+
return rect.width > 0 && rect.height > 0;
|
|
659
|
+
};
|
|
660
|
+
const inputs = inputSelectors
|
|
661
|
+
.map((selector) => document.querySelector(selector))
|
|
662
|
+
.filter((node) => Boolean(node));
|
|
663
|
+
const visibleInputs = inputs.filter((node) => isVisible(node));
|
|
664
|
+
const activeInputs = visibleInputs.length > 0 ? visibleInputs : inputs;
|
|
665
|
+
const userMatched =
|
|
666
|
+
normalizedPrompt.length > 0 && normalizedTurns.some((text) => text.includes(normalizedPrompt));
|
|
667
|
+
const prefixMatched =
|
|
668
|
+
normalizedPromptPrefix.length > 30 &&
|
|
669
|
+
normalizedTurns.some((text) => text.includes(normalizedPromptPrefix));
|
|
670
|
+
const lastTurn = normalizedTurns[normalizedTurns.length - 1] ?? '';
|
|
671
|
+
const lastMatched =
|
|
672
|
+
normalizedPrompt.length > 0 &&
|
|
673
|
+
(lastTurn.includes(normalizedPrompt) ||
|
|
674
|
+
(normalizedPromptPrefix.length > 30 && lastTurn.includes(normalizedPromptPrefix)));
|
|
675
|
+
const baseline = ${baselineLiteral};
|
|
676
|
+
const hasNewTurn = baseline < 0 ? false : normalizedTurns.length > baseline;
|
|
677
|
+
const stopVisible = Boolean(document.querySelector(${stopSelectorLiteral}));
|
|
678
|
+
const assistantVisible = Boolean(
|
|
679
|
+
document.querySelector(${assistantSelectorLiteral}) ||
|
|
680
|
+
document.querySelector('[data-testid*="assistant"]'),
|
|
681
|
+
);
|
|
682
|
+
// Learned: composer clearing + stop button or assistant presence is a reliable fallback signal.
|
|
683
|
+
const editorValue = editor?.innerText ?? '';
|
|
684
|
+
const fallbackValue = fallback?.value ?? '';
|
|
685
|
+
const activeEmpty =
|
|
686
|
+
activeInputs.length === 0 ? null : activeInputs.every((node) => !String(readValue(node)).trim());
|
|
687
|
+
const composerCleared = activeEmpty ?? !(String(editorValue).trim() || String(fallbackValue).trim());
|
|
688
|
+
const activeInput = activeInputs[0] ?? null;
|
|
689
|
+
const activeInputValue = activeInput ? String(readValue(activeInput)) : '';
|
|
690
|
+
const activeInputDisabled = Boolean(
|
|
691
|
+
activeInput &&
|
|
692
|
+
(((activeInput instanceof HTMLInputElement || activeInput instanceof HTMLTextAreaElement) &&
|
|
693
|
+
activeInput.disabled) ||
|
|
694
|
+
activeInput.getAttribute?.('aria-disabled') === 'true' ||
|
|
695
|
+
activeInput.getAttribute?.('disabled') !== null)
|
|
696
|
+
);
|
|
697
|
+
const activeInputReadOnly = Boolean(
|
|
698
|
+
activeInput &&
|
|
699
|
+
(((activeInput instanceof HTMLInputElement || activeInput instanceof HTMLTextAreaElement) &&
|
|
700
|
+
activeInput.readOnly) ||
|
|
701
|
+
activeInput.getAttribute?.('readonly') !== null ||
|
|
702
|
+
activeInput.getAttribute?.('contenteditable') === 'false')
|
|
703
|
+
);
|
|
704
|
+
const href = typeof location === 'object' && location.href ? location.href : '';
|
|
705
|
+
const inConversation = /\\/c\\//.test(href);
|
|
706
|
+
return {
|
|
707
|
+
baseline,
|
|
708
|
+
userMatched,
|
|
709
|
+
prefixMatched,
|
|
710
|
+
lastMatched,
|
|
711
|
+
hasNewTurn,
|
|
712
|
+
stopVisible,
|
|
713
|
+
assistantVisible,
|
|
714
|
+
composerCleared,
|
|
715
|
+
inConversation,
|
|
716
|
+
href,
|
|
717
|
+
activeInputExists: Boolean(activeInput),
|
|
718
|
+
activeInputVisible: Boolean(activeInput && isVisible(activeInput)),
|
|
719
|
+
activeInputDisabled,
|
|
720
|
+
activeInputReadOnly,
|
|
721
|
+
activeInputValueLength: activeInputValue.length,
|
|
722
|
+
activeInputTagName: activeInput?.tagName?.toLowerCase?.() ?? '',
|
|
723
|
+
activeInputRole: activeInput?.getAttribute?.('role') ?? '',
|
|
724
|
+
fallbackValue,
|
|
725
|
+
editorValue,
|
|
726
|
+
lastTurn,
|
|
727
|
+
turnsCount: normalizedTurns.length,
|
|
728
|
+
};
|
|
729
|
+
})()`;
|
|
730
|
+
while (Date.now() < deadline) {
|
|
731
|
+
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
732
|
+
const info = result.value;
|
|
733
|
+
const turnsCount = result.value?.turnsCount;
|
|
734
|
+
const matchesPrompt = Boolean(info?.lastMatched || info?.userMatched || info?.prefixMatched);
|
|
735
|
+
const baselineUnknown = typeof info?.baseline === "number" ? info.baseline < 0 : baselineLiteral < 0;
|
|
736
|
+
if (matchesPrompt && (baselineUnknown || info?.hasNewTurn)) {
|
|
737
|
+
return typeof turnsCount === "number" && Number.isFinite(turnsCount) ? turnsCount : null;
|
|
738
|
+
}
|
|
739
|
+
const fallbackCommit = info?.composerCleared &&
|
|
740
|
+
Boolean(info?.hasNewTurn) &&
|
|
741
|
+
((info?.stopVisible ?? false) || info?.assistantVisible || info?.inConversation);
|
|
742
|
+
if (fallbackCommit) {
|
|
743
|
+
return typeof turnsCount === "number" && Number.isFinite(turnsCount) ? turnsCount : null;
|
|
744
|
+
}
|
|
745
|
+
await delay(100);
|
|
746
|
+
}
|
|
747
|
+
if (logger) {
|
|
748
|
+
logger(`Prompt commit check failed; latest state: ${await Runtime.evaluate({
|
|
749
|
+
expression: script,
|
|
750
|
+
returnByValue: true,
|
|
751
|
+
})
|
|
752
|
+
.then((res) => JSON.stringify(res?.result?.value))
|
|
753
|
+
.catch(() => "unavailable")}`);
|
|
754
|
+
await logDomFailure(Runtime, logger, "prompt-commit");
|
|
755
|
+
}
|
|
756
|
+
const finalStateResult = await Runtime.evaluate({
|
|
757
|
+
expression: script,
|
|
758
|
+
returnByValue: true,
|
|
759
|
+
}).catch(() => null);
|
|
760
|
+
const finalState = (finalStateResult?.result?.value ?? {});
|
|
761
|
+
const composerState = {
|
|
762
|
+
href: typeof finalState.href === "string" ? finalState.href : "",
|
|
763
|
+
turnsCount: typeof finalState.turnsCount === "number" && Number.isFinite(finalState.turnsCount)
|
|
764
|
+
? finalState.turnsCount
|
|
765
|
+
: null,
|
|
766
|
+
composerCleared: Boolean(finalState.composerCleared),
|
|
767
|
+
stopVisible: Boolean(finalState.stopVisible),
|
|
768
|
+
assistantVisible: Boolean(finalState.assistantVisible),
|
|
769
|
+
hasNewTurn: Boolean(finalState.hasNewTurn),
|
|
770
|
+
activeInputExists: Boolean(finalState.activeInputExists),
|
|
771
|
+
activeInputVisible: Boolean(finalState.activeInputVisible),
|
|
772
|
+
activeInputDisabled: Boolean(finalState.activeInputDisabled),
|
|
773
|
+
activeInputReadOnly: Boolean(finalState.activeInputReadOnly),
|
|
774
|
+
activeInputValueLength: typeof finalState.activeInputValueLength === "number" &&
|
|
775
|
+
Number.isFinite(finalState.activeInputValueLength)
|
|
776
|
+
? finalState.activeInputValueLength
|
|
777
|
+
: 0,
|
|
778
|
+
activeInputTagName: finalState.activeInputTagName ?? "",
|
|
779
|
+
activeInputRole: finalState.activeInputRole ?? "",
|
|
780
|
+
};
|
|
781
|
+
const likelyDeadComposer = composerState.activeInputExists &&
|
|
782
|
+
(!composerState.activeInputVisible ||
|
|
783
|
+
composerState.activeInputDisabled ||
|
|
784
|
+
composerState.activeInputReadOnly ||
|
|
785
|
+
(!composerState.hasNewTurn &&
|
|
786
|
+
!composerState.stopVisible &&
|
|
787
|
+
!composerState.assistantVisible &&
|
|
788
|
+
!composerState.composerCleared));
|
|
789
|
+
if (prompt.trim().length >= PROMPT_TRUNCATION_CHECK_THRESHOLD) {
|
|
790
|
+
throw new BrowserAutomationError("Prompt did not appear in conversation before timeout (likely too large).", {
|
|
791
|
+
stage: "submit-prompt",
|
|
792
|
+
code: "prompt-too-large",
|
|
793
|
+
promptLength: prompt.trim().length,
|
|
794
|
+
timeoutMs,
|
|
795
|
+
composerState,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
if (likelyDeadComposer) {
|
|
799
|
+
throw new BrowserAutomationError("Prompt did not appear because the ChatGPT composer is unresponsive.", {
|
|
800
|
+
stage: "submit-prompt",
|
|
801
|
+
code: "dead-composer",
|
|
802
|
+
timeoutMs,
|
|
803
|
+
composerState,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
throw new BrowserAutomationError("Prompt did not appear in conversation before timeout (send may have failed).", {
|
|
807
|
+
stage: "submit-prompt",
|
|
808
|
+
code: "prompt-commit-timeout",
|
|
809
|
+
timeoutMs,
|
|
810
|
+
composerState,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
814
|
+
export const __test__ = {
|
|
815
|
+
attemptSendButton,
|
|
816
|
+
ensureComposerHealthy,
|
|
817
|
+
readComposerSnapshot,
|
|
818
|
+
isPromptTooLarge,
|
|
819
|
+
runPostSubmitProtection,
|
|
820
|
+
defocusStopButtonAfterSubmit,
|
|
821
|
+
movePointerAwayFromStopControl,
|
|
822
|
+
canSubmitPromptViaEnter,
|
|
823
|
+
verifyPromptCommitted,
|
|
824
|
+
};
|