@steipete/oracle 0.7.6 → 0.8.1
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 +6 -3
- package/dist/bin/oracle-cli.js +4 -0
- package/dist/src/browser/actions/assistantResponse.js +437 -84
- package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
- package/dist/src/browser/actions/attachments.js +1300 -132
- package/dist/src/browser/actions/modelSelection.js +9 -4
- package/dist/src/browser/actions/navigation.js +160 -5
- package/dist/src/browser/actions/promptComposer.js +54 -11
- package/dist/src/browser/actions/remoteFileTransfer.js +5 -156
- package/dist/src/browser/actions/thinkingTime.js +5 -0
- package/dist/src/browser/chromeLifecycle.js +9 -1
- package/dist/src/browser/config.js +11 -3
- package/dist/src/browser/constants.js +4 -1
- package/dist/src/browser/cookies.js +55 -21
- package/dist/src/browser/index.js +342 -69
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +16 -0
- package/dist/src/browser/reattach.js +27 -179
- package/dist/src/browser/reattachHelpers.js +382 -0
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/browserConfig.js +12 -5
- package/dist/src/cli/browserDefaults.js +12 -0
- package/dist/src/cli/sessionDisplay.js +7 -0
- package/dist/src/gemini-web/executor.js +107 -46
- package/dist/src/oracle/oscProgress.js +7 -0
- package/dist/src/oracle/run.js +23 -32
- package/dist/src/remote/server.js +30 -15
- package/package.json +8 -17
|
@@ -2,12 +2,16 @@ import path from 'node:path';
|
|
|
2
2
|
import { CONVERSATION_TURN_SELECTOR, INPUT_SELECTORS, SEND_BUTTON_SELECTORS, UPLOAD_STATUS_SELECTORS } from '../constants.js';
|
|
3
3
|
import { delay } from '../utils.js';
|
|
4
4
|
import { logDomFailure } from '../domDebug.js';
|
|
5
|
-
|
|
5
|
+
import { transferAttachmentViaDataTransfer } from './attachmentDataTransfer.js';
|
|
6
|
+
export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
6
7
|
const { runtime, dom } = deps;
|
|
7
8
|
if (!dom) {
|
|
8
9
|
throw new Error('DOM domain unavailable while uploading attachments.');
|
|
9
10
|
}
|
|
10
|
-
const
|
|
11
|
+
const expectedCount = typeof options?.expectedCount === 'number' && Number.isFinite(options.expectedCount)
|
|
12
|
+
? Math.max(0, Math.floor(options.expectedCount))
|
|
13
|
+
: 0;
|
|
14
|
+
const readAttachmentSignals = async (name) => {
|
|
11
15
|
const check = await runtime.evaluate({
|
|
12
16
|
expression: `(() => {
|
|
13
17
|
const expected = ${JSON.stringify(name)};
|
|
@@ -33,47 +37,240 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
33
37
|
};
|
|
34
38
|
|
|
35
39
|
const promptSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
36
|
-
const
|
|
40
|
+
const findPromptNode = () => {
|
|
41
|
+
for (const selector of promptSelectors) {
|
|
42
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
43
|
+
for (const node of nodes) {
|
|
44
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
45
|
+
const rect = node.getBoundingClientRect();
|
|
46
|
+
if (rect.width > 0 && rect.height > 0) return node;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
for (const selector of promptSelectors) {
|
|
38
50
|
const node = document.querySelector(selector);
|
|
39
|
-
if (
|
|
40
|
-
|
|
51
|
+
if (node) return node;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
56
|
+
const attachmentSelectors = [
|
|
57
|
+
'input[type="file"]',
|
|
58
|
+
'[data-testid*="attachment"]',
|
|
59
|
+
'[data-testid*="upload"]',
|
|
60
|
+
'[aria-label*="Remove"]',
|
|
61
|
+
'[aria-label*="remove"]',
|
|
62
|
+
];
|
|
63
|
+
const locateComposerRoot = () => {
|
|
64
|
+
const promptNode = findPromptNode();
|
|
65
|
+
if (promptNode) {
|
|
66
|
+
const initial =
|
|
67
|
+
promptNode.closest('[data-testid*="composer"]') ??
|
|
68
|
+
promptNode.closest('form') ??
|
|
69
|
+
promptNode.parentElement ??
|
|
70
|
+
document.body;
|
|
71
|
+
let current = initial;
|
|
72
|
+
let fallback = initial;
|
|
73
|
+
while (current && current !== document.body) {
|
|
74
|
+
const hasSend = sendSelectors.some((selector) => current.querySelector(selector));
|
|
75
|
+
if (hasSend) {
|
|
76
|
+
fallback = current;
|
|
77
|
+
const hasAttachment = attachmentSelectors.some((selector) => current.querySelector(selector));
|
|
78
|
+
if (hasAttachment) {
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
current = current.parentElement;
|
|
83
|
+
}
|
|
84
|
+
return fallback ?? initial;
|
|
41
85
|
}
|
|
42
86
|
return document.querySelector('form') ?? document.body;
|
|
43
87
|
};
|
|
44
88
|
const root = locateComposerRoot();
|
|
89
|
+
const scope = (() => {
|
|
90
|
+
if (!root) return document.body;
|
|
91
|
+
const parent = root.parentElement;
|
|
92
|
+
const parentHasSend = parent && sendSelectors.some((selector) => parent.querySelector(selector));
|
|
93
|
+
return parentHasSend ? parent : root;
|
|
94
|
+
})();
|
|
95
|
+
const rootTextRaw = root ? (root.innerText || root.textContent || '') : '';
|
|
45
96
|
const chipSelector = [
|
|
46
97
|
'[data-testid*="attachment"]',
|
|
47
98
|
'[data-testid*="chip"]',
|
|
48
99
|
'[data-testid*="upload"]',
|
|
100
|
+
'[data-testid*="file"]',
|
|
49
101
|
'[aria-label*="Remove"]',
|
|
50
102
|
'button[aria-label*="Remove"]',
|
|
51
103
|
'[aria-label*="remove"]',
|
|
52
104
|
].join(',');
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
105
|
+
const localCandidates = scope ? Array.from(scope.querySelectorAll(chipSelector)) : [];
|
|
106
|
+
const globalCandidates = Array.from(document.querySelectorAll(chipSelector));
|
|
107
|
+
const matchCandidates = localCandidates.length > 0 ? localCandidates : globalCandidates;
|
|
108
|
+
const serializeChip = (node) => {
|
|
109
|
+
const text = node?.textContent ?? '';
|
|
110
|
+
const aria = node?.getAttribute?.('aria-label') ?? '';
|
|
111
|
+
const title = node?.getAttribute?.('title') ?? '';
|
|
112
|
+
const testid = node?.getAttribute?.('data-testid') ?? '';
|
|
113
|
+
return [text, aria, title, testid].map(normalize).join('|');
|
|
114
|
+
};
|
|
115
|
+
const chipSignature = localCandidates.map(serializeChip).join('||');
|
|
116
|
+
let uiMatch = false;
|
|
117
|
+
for (const node of matchCandidates) {
|
|
118
|
+
if (node?.tagName === 'INPUT' && node?.type === 'file') continue;
|
|
56
119
|
const text = node?.textContent ?? '';
|
|
57
120
|
const aria = node?.getAttribute?.('aria-label') ?? '';
|
|
58
121
|
const title = node?.getAttribute?.('title') ?? '';
|
|
59
122
|
if ([text, aria, title].some(matchesExpected)) {
|
|
60
|
-
|
|
123
|
+
uiMatch = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!uiMatch) {
|
|
129
|
+
const removeScope = root ?? document;
|
|
130
|
+
const cardTexts = Array.from(removeScope.querySelectorAll('[aria-label*="Remove"],[aria-label*="remove"]')).map(
|
|
131
|
+
(btn) => btn?.parentElement?.parentElement?.innerText ?? '',
|
|
132
|
+
);
|
|
133
|
+
if (cardTexts.some(matchesExpected)) {
|
|
134
|
+
uiMatch = true;
|
|
61
135
|
}
|
|
62
136
|
}
|
|
63
137
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
);
|
|
67
|
-
|
|
138
|
+
const inputScope = scope ? Array.from(scope.querySelectorAll('input[type="file"]')) : [];
|
|
139
|
+
const inputs = [];
|
|
140
|
+
const inputSeen = new Set();
|
|
141
|
+
for (const el of [...inputScope, ...Array.from(document.querySelectorAll('input[type="file"]'))]) {
|
|
142
|
+
if (!inputSeen.has(el)) {
|
|
143
|
+
inputSeen.add(el);
|
|
144
|
+
inputs.push(el);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const inputNames = [];
|
|
148
|
+
let inputCount = 0;
|
|
149
|
+
for (const el of inputs) {
|
|
150
|
+
if (!(el instanceof HTMLInputElement)) continue;
|
|
151
|
+
const files = Array.from(el.files || []);
|
|
152
|
+
if (files.length > 0) {
|
|
153
|
+
inputCount += files.length;
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
if (file?.name) inputNames.push(file.name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const inputMatch = inputNames.some((file) => matchesExpected(file));
|
|
160
|
+
const uploadingSelectors = ${JSON.stringify(UPLOAD_STATUS_SELECTORS)};
|
|
161
|
+
const uploading = uploadingSelectors.some((selector) => {
|
|
162
|
+
return Array.from(document.querySelectorAll(selector)).some((node) => {
|
|
163
|
+
const ariaBusy = node.getAttribute?.('aria-busy');
|
|
164
|
+
const dataState = node.getAttribute?.('data-state');
|
|
165
|
+
if (ariaBusy === 'true' || dataState === 'loading' || dataState === 'uploading' || dataState === 'pending') {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
const text = node.textContent?.toLowerCase?.() ?? '';
|
|
169
|
+
return /\\buploading\\b/.test(text) || /\\bprocessing\\b/.test(text);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
68
172
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
173
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+files?\\b/;
|
|
174
|
+
const collectFileCount = (candidates) => {
|
|
175
|
+
let count = 0;
|
|
176
|
+
for (const node of candidates) {
|
|
177
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
178
|
+
if (node.matches('textarea,input,[contenteditable="true"]')) continue;
|
|
179
|
+
const dataTestId = node.getAttribute?.('data-testid') ?? '';
|
|
180
|
+
const aria = node.getAttribute?.('aria-label') ?? '';
|
|
181
|
+
const title = node.getAttribute?.('title') ?? '';
|
|
182
|
+
const tooltip =
|
|
183
|
+
node.getAttribute?.('data-tooltip') ?? node.getAttribute?.('data-tooltip-content') ?? '';
|
|
184
|
+
const text = node.textContent ?? '';
|
|
185
|
+
const parent = node.parentElement;
|
|
186
|
+
const parentText = parent?.textContent ?? '';
|
|
187
|
+
const parentAria = parent?.getAttribute?.('aria-label') ?? '';
|
|
188
|
+
const parentTitle = parent?.getAttribute?.('title') ?? '';
|
|
189
|
+
const parentTooltip =
|
|
190
|
+
parent?.getAttribute?.('data-tooltip') ?? parent?.getAttribute?.('data-tooltip-content') ?? '';
|
|
191
|
+
const parentTestId = parent?.getAttribute?.('data-testid') ?? '';
|
|
192
|
+
const values = [
|
|
193
|
+
text,
|
|
194
|
+
aria,
|
|
195
|
+
title,
|
|
196
|
+
tooltip,
|
|
197
|
+
dataTestId,
|
|
198
|
+
parentText,
|
|
199
|
+
parentAria,
|
|
200
|
+
parentTitle,
|
|
201
|
+
parentTooltip,
|
|
202
|
+
parentTestId,
|
|
203
|
+
];
|
|
204
|
+
let hasFileHint = false;
|
|
205
|
+
for (const raw of values) {
|
|
206
|
+
if (!raw) continue;
|
|
207
|
+
if (normalize(raw).includes('file')) {
|
|
208
|
+
hasFileHint = true;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!hasFileHint) continue;
|
|
213
|
+
for (const raw of values) {
|
|
214
|
+
if (!raw) continue;
|
|
215
|
+
const match = normalize(raw).match(countRegex);
|
|
216
|
+
if (match) {
|
|
217
|
+
const parsed = Number(match[1]);
|
|
218
|
+
if (Number.isFinite(parsed)) {
|
|
219
|
+
count = Math.max(count, parsed);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return count;
|
|
225
|
+
};
|
|
226
|
+
const fileCountSelectors = [
|
|
227
|
+
'button',
|
|
228
|
+
'[role="button"]',
|
|
229
|
+
'[data-testid*="file"]',
|
|
230
|
+
'[data-testid*="upload"]',
|
|
231
|
+
'[data-testid*="attachment"]',
|
|
232
|
+
'[data-testid*="chip"]',
|
|
233
|
+
'[aria-label*="file"]',
|
|
234
|
+
'[title*="file"]',
|
|
235
|
+
'[aria-label*="attachment"]',
|
|
236
|
+
'[title*="attachment"]',
|
|
237
|
+
].join(',');
|
|
238
|
+
const fileCountScope = scope ?? root ?? document.body;
|
|
239
|
+
const localFileNodes = fileCountScope
|
|
240
|
+
? Array.from(fileCountScope.querySelectorAll(fileCountSelectors))
|
|
241
|
+
: [];
|
|
242
|
+
const globalFileNodes = Array.from(document.querySelectorAll(fileCountSelectors));
|
|
243
|
+
let fileCount = collectFileCount(localFileNodes);
|
|
244
|
+
if (!fileCount && globalFileNodes.length > 0) {
|
|
245
|
+
fileCount = collectFileCount(globalFileNodes);
|
|
246
|
+
}
|
|
247
|
+
const hasAttachmentSignal = localCandidates.length > 0 || inputCount > 0 || fileCount > 0 || uploading;
|
|
248
|
+
if (!uiMatch && rootTextRaw && hasAttachmentSignal && matchesExpected(rootTextRaw)) {
|
|
249
|
+
uiMatch = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
ui: uiMatch,
|
|
254
|
+
input: inputMatch,
|
|
255
|
+
inputCount,
|
|
256
|
+
chipCount: localCandidates.length,
|
|
257
|
+
chipSignature,
|
|
258
|
+
uploading,
|
|
259
|
+
fileCount,
|
|
260
|
+
};
|
|
73
261
|
})()`,
|
|
74
262
|
returnByValue: true,
|
|
75
263
|
});
|
|
76
|
-
|
|
264
|
+
const value = check?.result?.value;
|
|
265
|
+
return {
|
|
266
|
+
ui: Boolean(value?.ui),
|
|
267
|
+
input: Boolean(value?.input),
|
|
268
|
+
inputCount: typeof value?.inputCount === 'number' ? value?.inputCount : 0,
|
|
269
|
+
chipCount: typeof value?.chipCount === 'number' ? value?.chipCount : 0,
|
|
270
|
+
chipSignature: typeof value?.chipSignature === 'string' ? value?.chipSignature : '',
|
|
271
|
+
uploading: Boolean(value?.uploading),
|
|
272
|
+
fileCount: typeof value?.fileCount === 'number' ? value?.fileCount : 0,
|
|
273
|
+
};
|
|
77
274
|
};
|
|
78
275
|
// New ChatGPT UI hides the real file input behind a composer "+" menu; click it pre-emptively.
|
|
79
276
|
await Promise.resolve(runtime.evaluate({
|
|
@@ -113,26 +310,126 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
113
310
|
})()`,
|
|
114
311
|
returnByValue: true,
|
|
115
312
|
})).catch(() => undefined);
|
|
313
|
+
const normalizeForMatch = (value) => String(value || '')
|
|
314
|
+
.toLowerCase()
|
|
315
|
+
.replace(/\s+/g, ' ')
|
|
316
|
+
.trim();
|
|
116
317
|
const expectedName = path.basename(attachment.path);
|
|
117
|
-
|
|
318
|
+
const expectedNameLower = normalizeForMatch(expectedName);
|
|
319
|
+
const expectedNameNoExt = expectedNameLower.replace(/\.[a-z0-9]{1,10}$/i, '');
|
|
320
|
+
const matchesExpectedName = (value) => {
|
|
321
|
+
const normalized = normalizeForMatch(value);
|
|
322
|
+
if (!normalized)
|
|
323
|
+
return false;
|
|
324
|
+
if (normalized.includes(expectedNameLower))
|
|
325
|
+
return true;
|
|
326
|
+
if (expectedNameNoExt.length >= 6 && normalized.includes(expectedNameNoExt))
|
|
327
|
+
return true;
|
|
328
|
+
return false;
|
|
329
|
+
};
|
|
330
|
+
const isImageAttachment = /\.(png|jpe?g|gif|webp|bmp|svg|heic|heif)$/i.test(expectedName);
|
|
331
|
+
const attachmentUiTimeoutMs = 25_000;
|
|
332
|
+
const attachmentUiSignalWaitMs = 5_000;
|
|
333
|
+
const initialSignals = await readAttachmentSignals(expectedName);
|
|
334
|
+
let inputConfirmed = false;
|
|
335
|
+
if (initialSignals.ui) {
|
|
118
336
|
logger(`Attachment already present: ${path.basename(attachment.path)}`);
|
|
119
|
-
return;
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
const isExpectedSatisfied = (signals) => {
|
|
340
|
+
if (expectedCount <= 0)
|
|
341
|
+
return false;
|
|
342
|
+
const fileCount = typeof signals.fileCount === 'number' ? signals.fileCount : 0;
|
|
343
|
+
const chipCount = typeof signals.chipCount === 'number' ? signals.chipCount : 0;
|
|
344
|
+
if (fileCount >= expectedCount)
|
|
345
|
+
return true;
|
|
346
|
+
return Boolean(signals.ui && chipCount >= expectedCount);
|
|
347
|
+
};
|
|
348
|
+
const initialInputSatisfied = expectedCount > 0 ? initialSignals.inputCount >= expectedCount : Boolean(initialSignals.input);
|
|
349
|
+
if (expectedCount > 0 && (initialSignals.fileCount >= expectedCount || initialSignals.inputCount >= expectedCount)) {
|
|
350
|
+
const satisfiedCount = Math.max(initialSignals.fileCount, initialSignals.inputCount);
|
|
351
|
+
logger(`Attachment already present: composer shows ${satisfiedCount} file${satisfiedCount === 1 ? '' : 's'}`);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if (initialInputSatisfied || initialSignals.input) {
|
|
355
|
+
logger(`Attachment already queued in file input: ${path.basename(attachment.path)}`);
|
|
356
|
+
return true;
|
|
120
357
|
}
|
|
121
358
|
const documentNode = await dom.getDocument();
|
|
122
359
|
const candidateSetup = await runtime.evaluate({
|
|
123
360
|
expression: `(() => {
|
|
124
361
|
const promptSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
125
|
-
const
|
|
362
|
+
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
363
|
+
const findPromptNode = () => {
|
|
364
|
+
for (const selector of promptSelectors) {
|
|
365
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
366
|
+
for (const node of nodes) {
|
|
367
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
368
|
+
const rect = node.getBoundingClientRect();
|
|
369
|
+
if (rect.width > 0 && rect.height > 0) return node;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
126
372
|
for (const selector of promptSelectors) {
|
|
127
373
|
const node = document.querySelector(selector);
|
|
128
|
-
if (
|
|
129
|
-
|
|
374
|
+
if (node) return node;
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
};
|
|
378
|
+
const attachmentSelectors = [
|
|
379
|
+
'input[type="file"]',
|
|
380
|
+
'[data-testid*="attachment"]',
|
|
381
|
+
'[data-testid*="upload"]',
|
|
382
|
+
'[aria-label*="Remove"]',
|
|
383
|
+
'[aria-label*="remove"]',
|
|
384
|
+
];
|
|
385
|
+
const locateComposerRoot = () => {
|
|
386
|
+
const promptNode = findPromptNode();
|
|
387
|
+
if (promptNode) {
|
|
388
|
+
const initial =
|
|
389
|
+
promptNode.closest('[data-testid*="composer"]') ??
|
|
390
|
+
promptNode.closest('form') ??
|
|
391
|
+
promptNode.parentElement ??
|
|
392
|
+
document.body;
|
|
393
|
+
let current = initial;
|
|
394
|
+
let fallback = initial;
|
|
395
|
+
while (current && current !== document.body) {
|
|
396
|
+
const hasSend = sendSelectors.some((selector) => current.querySelector(selector));
|
|
397
|
+
if (hasSend) {
|
|
398
|
+
fallback = current;
|
|
399
|
+
const hasAttachment = attachmentSelectors.some((selector) => current.querySelector(selector));
|
|
400
|
+
if (hasAttachment) {
|
|
401
|
+
return current;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
current = current.parentElement;
|
|
405
|
+
}
|
|
406
|
+
return fallback ?? initial;
|
|
130
407
|
}
|
|
131
408
|
return document.querySelector('form') ?? document.body;
|
|
132
409
|
};
|
|
133
410
|
const root = locateComposerRoot();
|
|
134
|
-
const
|
|
135
|
-
|
|
411
|
+
const scope = (() => {
|
|
412
|
+
if (!root) return document.body;
|
|
413
|
+
const parent = root.parentElement;
|
|
414
|
+
const parentHasSend = parent && sendSelectors.some((selector) => parent.querySelector(selector));
|
|
415
|
+
return parentHasSend ? parent : root;
|
|
416
|
+
})();
|
|
417
|
+
const localInputs = scope ? Array.from(scope.querySelectorAll('input[type="file"]')) : [];
|
|
418
|
+
const globalInputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
419
|
+
const inputs = [];
|
|
420
|
+
const inputSeen = new Set();
|
|
421
|
+
for (const el of [...localInputs, ...globalInputs]) {
|
|
422
|
+
if (!inputSeen.has(el)) {
|
|
423
|
+
inputSeen.add(el);
|
|
424
|
+
inputs.push(el);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const baselineInputCount = inputs.reduce((total, el) => {
|
|
428
|
+
if (!(el instanceof HTMLInputElement)) return total;
|
|
429
|
+
const count = Array.from(el.files || []).length;
|
|
430
|
+
return total + count;
|
|
431
|
+
}, 0);
|
|
432
|
+
const isImageAttachment = ${JSON.stringify(isImageAttachment)};
|
|
136
433
|
const acceptIsImageOnly = (accept) => {
|
|
137
434
|
if (!accept) return false;
|
|
138
435
|
const parts = String(accept)
|
|
@@ -141,53 +438,295 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
141
438
|
.filter(Boolean);
|
|
142
439
|
return parts.length > 0 && parts.every((p) => p.startsWith('image/'));
|
|
143
440
|
};
|
|
144
|
-
const chipContainer =
|
|
145
|
-
|
|
441
|
+
const chipContainer = scope ?? document;
|
|
442
|
+
const chipSelector = '[data-testid*="attachment"],[data-testid*="chip"],[data-testid*="upload"],[data-testid*="file"],[aria-label*="Remove"],[aria-label*="remove"]';
|
|
146
443
|
const baselineChipCount = chipContainer.querySelectorAll(chipSelector).length;
|
|
444
|
+
const baselineChips = Array.from(chipContainer.querySelectorAll(chipSelector))
|
|
445
|
+
.slice(0, 20)
|
|
446
|
+
.map((node) => ({
|
|
447
|
+
text: (node.textContent || '').trim(),
|
|
448
|
+
aria: node.getAttribute?.('aria-label') ?? '',
|
|
449
|
+
title: node.getAttribute?.('title') ?? '',
|
|
450
|
+
testid: node.getAttribute?.('data-testid') ?? '',
|
|
451
|
+
}));
|
|
452
|
+
const uploadingSelectors = ${JSON.stringify(UPLOAD_STATUS_SELECTORS)};
|
|
453
|
+
const baselineUploading = uploadingSelectors.some((selector) => {
|
|
454
|
+
return Array.from(document.querySelectorAll(selector)).some((node) => {
|
|
455
|
+
const ariaBusy = node.getAttribute?.('aria-busy');
|
|
456
|
+
const dataState = node.getAttribute?.('data-state');
|
|
457
|
+
if (ariaBusy === 'true' || dataState === 'loading' || dataState === 'uploading' || dataState === 'pending') {
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
const text = node.textContent?.toLowerCase?.() ?? '';
|
|
461
|
+
return /\\buploading\\b/.test(text) || /\\bprocessing\\b/.test(text);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+files?\\b/;
|
|
465
|
+
const collectFileCount = (candidates) => {
|
|
466
|
+
let count = 0;
|
|
467
|
+
for (const node of candidates) {
|
|
468
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
469
|
+
if (node.matches('textarea,input,[contenteditable="true"]')) continue;
|
|
470
|
+
const dataTestId = node.getAttribute?.('data-testid') ?? '';
|
|
471
|
+
const aria = node.getAttribute?.('aria-label') ?? '';
|
|
472
|
+
const title = node.getAttribute?.('title') ?? '';
|
|
473
|
+
const tooltip =
|
|
474
|
+
node.getAttribute?.('data-tooltip') ?? node.getAttribute?.('data-tooltip-content') ?? '';
|
|
475
|
+
const text = node.textContent ?? '';
|
|
476
|
+
const parent = node.parentElement;
|
|
477
|
+
const parentText = parent?.textContent ?? '';
|
|
478
|
+
const parentAria = parent?.getAttribute?.('aria-label') ?? '';
|
|
479
|
+
const parentTitle = parent?.getAttribute?.('title') ?? '';
|
|
480
|
+
const parentTooltip =
|
|
481
|
+
parent?.getAttribute?.('data-tooltip') ?? parent?.getAttribute?.('data-tooltip-content') ?? '';
|
|
482
|
+
const parentTestId = parent?.getAttribute?.('data-testid') ?? '';
|
|
483
|
+
const values = [
|
|
484
|
+
text,
|
|
485
|
+
aria,
|
|
486
|
+
title,
|
|
487
|
+
tooltip,
|
|
488
|
+
dataTestId,
|
|
489
|
+
parentText,
|
|
490
|
+
parentAria,
|
|
491
|
+
parentTitle,
|
|
492
|
+
parentTooltip,
|
|
493
|
+
parentTestId,
|
|
494
|
+
];
|
|
495
|
+
let hasFileHint = false;
|
|
496
|
+
for (const raw of values) {
|
|
497
|
+
if (!raw) continue;
|
|
498
|
+
if (String(raw).toLowerCase().includes('file')) {
|
|
499
|
+
hasFileHint = true;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (!hasFileHint) continue;
|
|
504
|
+
for (const raw of values) {
|
|
505
|
+
if (!raw) continue;
|
|
506
|
+
const match = String(raw).toLowerCase().match(countRegex);
|
|
507
|
+
if (match) {
|
|
508
|
+
const parsed = Number(match[1]);
|
|
509
|
+
if (Number.isFinite(parsed)) {
|
|
510
|
+
count = Math.max(count, parsed);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return count;
|
|
516
|
+
};
|
|
517
|
+
const fileCountSelectors = [
|
|
518
|
+
'button',
|
|
519
|
+
'[role="button"]',
|
|
520
|
+
'[data-testid*="file"]',
|
|
521
|
+
'[data-testid*="upload"]',
|
|
522
|
+
'[data-testid*="attachment"]',
|
|
523
|
+
'[data-testid*="chip"]',
|
|
524
|
+
'[aria-label*="file"]',
|
|
525
|
+
'[title*="file"]',
|
|
526
|
+
'[aria-label*="attachment"]',
|
|
527
|
+
'[title*="attachment"]',
|
|
528
|
+
].join(',');
|
|
529
|
+
const fileCountScope = scope ?? root ?? document.body;
|
|
530
|
+
const localFileNodes = fileCountScope
|
|
531
|
+
? Array.from(fileCountScope.querySelectorAll(fileCountSelectors))
|
|
532
|
+
: [];
|
|
533
|
+
const globalFileNodes = Array.from(document.querySelectorAll(fileCountSelectors));
|
|
534
|
+
let baselineFileCount = collectFileCount(localFileNodes);
|
|
535
|
+
if (!baselineFileCount && globalFileNodes.length > 0) {
|
|
536
|
+
baselineFileCount = collectFileCount(globalFileNodes);
|
|
537
|
+
}
|
|
147
538
|
|
|
148
539
|
// Mark candidates with stable indices so we can select them via DOM.querySelector.
|
|
149
540
|
let idx = 0;
|
|
150
|
-
|
|
541
|
+
let candidates = inputs.map((el) => {
|
|
151
542
|
const accept = el.getAttribute('accept') || '';
|
|
152
|
-
const
|
|
543
|
+
const imageOnly = acceptIsImageOnly(accept);
|
|
544
|
+
const score =
|
|
545
|
+
(el.hasAttribute('multiple') ? 100 : 0) +
|
|
546
|
+
(!imageOnly ? 20 : isImageAttachment ? 15 : -500);
|
|
153
547
|
el.setAttribute('data-oracle-upload-candidate', 'true');
|
|
154
548
|
el.setAttribute('data-oracle-upload-idx', String(idx));
|
|
155
|
-
return { idx: idx++, score };
|
|
549
|
+
return { idx: idx++, score, imageOnly };
|
|
156
550
|
});
|
|
551
|
+
if (!isImageAttachment) {
|
|
552
|
+
const nonImage = candidates.filter((candidate) => !candidate.imageOnly);
|
|
553
|
+
if (nonImage.length > 0) {
|
|
554
|
+
candidates = nonImage;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
157
557
|
|
|
158
558
|
// Prefer higher scores first.
|
|
159
559
|
candidates.sort((a, b) => b.score - a.score);
|
|
160
|
-
return {
|
|
560
|
+
return {
|
|
561
|
+
ok: candidates.length > 0,
|
|
562
|
+
baselineChipCount,
|
|
563
|
+
baselineChips,
|
|
564
|
+
baselineUploading,
|
|
565
|
+
baselineFileCount,
|
|
566
|
+
baselineInputCount,
|
|
567
|
+
order: candidates.map((c) => c.idx),
|
|
568
|
+
};
|
|
161
569
|
})()`,
|
|
162
570
|
returnByValue: true,
|
|
163
571
|
});
|
|
164
572
|
const candidateValue = candidateSetup?.result?.value;
|
|
165
573
|
const candidateOrder = Array.isArray(candidateValue?.order) ? candidateValue.order : [];
|
|
166
574
|
const baselineChipCount = typeof candidateValue?.baselineChipCount === 'number' ? candidateValue.baselineChipCount : 0;
|
|
575
|
+
const baselineChips = Array.isArray(candidateValue?.baselineChips) ? candidateValue.baselineChips : [];
|
|
576
|
+
const baselineUploading = Boolean(candidateValue?.baselineUploading);
|
|
577
|
+
const baselineFileCount = typeof candidateValue?.baselineFileCount === 'number' ? candidateValue.baselineFileCount : 0;
|
|
578
|
+
const baselineInputCount = typeof candidateValue?.baselineInputCount === 'number' ? candidateValue.baselineInputCount : 0;
|
|
579
|
+
const serializeChips = (chips) => chips
|
|
580
|
+
.map((chip) => [chip.text, chip.aria, chip.title, chip.testid]
|
|
581
|
+
.map((value) => String(value || '').toLowerCase().replace(/\s+/g, ' ').trim())
|
|
582
|
+
.join('|'))
|
|
583
|
+
.join('||');
|
|
584
|
+
const baselineChipSignature = serializeChips(baselineChips);
|
|
167
585
|
if (!candidateValue?.ok || candidateOrder.length === 0) {
|
|
168
586
|
await logDomFailure(runtime, logger, 'file-input-missing');
|
|
169
587
|
throw new Error('Unable to locate ChatGPT file attachment input.');
|
|
170
588
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
589
|
+
const hasChipDelta = (signals) => {
|
|
590
|
+
const chipCount = typeof signals.chipCount === 'number' ? signals.chipCount : 0;
|
|
591
|
+
const chipSignature = typeof signals.chipSignature === 'string' ? signals.chipSignature : '';
|
|
592
|
+
if (chipCount > baselineChipCount)
|
|
593
|
+
return true;
|
|
594
|
+
if (baselineChipSignature && chipSignature && chipSignature !== baselineChipSignature)
|
|
595
|
+
return true;
|
|
596
|
+
return false;
|
|
597
|
+
};
|
|
598
|
+
const hasInputDelta = (signals) => (typeof signals.inputCount === 'number' ? signals.inputCount : 0) > baselineInputCount;
|
|
599
|
+
const hasUploadDelta = (signals) => Boolean(signals.uploading && !baselineUploading);
|
|
600
|
+
const hasFileCountDelta = (signals) => (typeof signals.fileCount === 'number' ? signals.fileCount : 0) > baselineFileCount;
|
|
601
|
+
const waitForAttachmentUiSignal = async (timeoutMs) => {
|
|
602
|
+
const deadline = Date.now() + timeoutMs;
|
|
603
|
+
let sawInputSignal = false;
|
|
604
|
+
let latest = null;
|
|
605
|
+
while (Date.now() < deadline) {
|
|
606
|
+
const signals = await readAttachmentSignals(expectedName);
|
|
607
|
+
const chipDelta = hasChipDelta(signals);
|
|
608
|
+
const inputDelta = hasInputDelta(signals) || signals.input;
|
|
609
|
+
const uploadDelta = hasUploadDelta(signals);
|
|
610
|
+
const fileCountDelta = hasFileCountDelta(signals);
|
|
611
|
+
const expectedSatisfied = isExpectedSatisfied(signals);
|
|
612
|
+
if (inputDelta) {
|
|
613
|
+
sawInputSignal = true;
|
|
614
|
+
}
|
|
615
|
+
latest = { signals, chipDelta, inputDelta: sawInputSignal, uploadDelta, fileCountDelta, expectedSatisfied };
|
|
616
|
+
if (signals.ui || chipDelta || uploadDelta || fileCountDelta || expectedSatisfied) {
|
|
617
|
+
return latest;
|
|
618
|
+
}
|
|
619
|
+
await delay(250);
|
|
620
|
+
}
|
|
621
|
+
return latest;
|
|
622
|
+
};
|
|
623
|
+
const inputSnapshotFor = (idx) => `(() => {
|
|
624
|
+
const input = document.querySelector('input[type="file"][data-oracle-upload-idx="${idx}"]');
|
|
625
|
+
if (!(input instanceof HTMLInputElement)) {
|
|
626
|
+
return { names: [], value: '', count: 0 };
|
|
176
627
|
}
|
|
177
|
-
return
|
|
628
|
+
return {
|
|
629
|
+
names: Array.from(input.files || []).map((file) => file?.name ?? '').filter(Boolean),
|
|
630
|
+
value: input.value || '',
|
|
631
|
+
count: Array.from(input.files || []).length,
|
|
632
|
+
};
|
|
178
633
|
})()`;
|
|
634
|
+
const parseInputSnapshot = (value) => {
|
|
635
|
+
const snapshot = value;
|
|
636
|
+
const names = Array.isArray(snapshot?.names) ? snapshot?.names ?? [] : [];
|
|
637
|
+
const valueText = typeof snapshot?.value === 'string' ? snapshot.value : '';
|
|
638
|
+
const count = typeof snapshot?.count === 'number' ? snapshot.count : names.length;
|
|
639
|
+
return {
|
|
640
|
+
names,
|
|
641
|
+
value: valueText,
|
|
642
|
+
count: Number.isFinite(count) ? count : names.length,
|
|
643
|
+
};
|
|
644
|
+
};
|
|
645
|
+
const readInputSnapshot = async (idx) => {
|
|
646
|
+
const snapshot = await runtime
|
|
647
|
+
.evaluate({ expression: inputSnapshotFor(idx), returnByValue: true })
|
|
648
|
+
.then((res) => parseInputSnapshot(res?.result?.value))
|
|
649
|
+
.catch(() => parseInputSnapshot(undefined));
|
|
650
|
+
return snapshot;
|
|
651
|
+
};
|
|
652
|
+
const snapshotMatchesExpected = (snapshot) => {
|
|
653
|
+
const nameMatch = snapshot.names.some((name) => matchesExpectedName(name));
|
|
654
|
+
return nameMatch || Boolean(snapshot.value && matchesExpectedName(snapshot.value));
|
|
655
|
+
};
|
|
656
|
+
const inputSignalsFor = (baseline, current) => {
|
|
657
|
+
const baselineCount = baseline.count ?? baseline.names.length;
|
|
658
|
+
const currentCount = current.count ?? current.names.length;
|
|
659
|
+
const countDelta = currentCount > baselineCount;
|
|
660
|
+
const valueDelta = Boolean(current.value) && current.value !== baseline.value;
|
|
661
|
+
const baselineEmpty = baselineCount === 0 && !baseline.value;
|
|
662
|
+
const nameMatch = current.names.some((name) => matchesExpectedName(name)) ||
|
|
663
|
+
(current.value && matchesExpectedName(current.value));
|
|
664
|
+
const touched = nameMatch || countDelta || (baselineEmpty && valueDelta);
|
|
665
|
+
return {
|
|
666
|
+
touched,
|
|
667
|
+
nameMatch,
|
|
668
|
+
countDelta,
|
|
669
|
+
valueDelta,
|
|
670
|
+
};
|
|
671
|
+
};
|
|
179
672
|
const composerSnapshotFor = (idx) => `(() => {
|
|
180
673
|
const promptSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
181
|
-
const
|
|
674
|
+
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
675
|
+
const findPromptNode = () => {
|
|
676
|
+
for (const selector of promptSelectors) {
|
|
677
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
678
|
+
for (const node of nodes) {
|
|
679
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
680
|
+
const rect = node.getBoundingClientRect();
|
|
681
|
+
if (rect.width > 0 && rect.height > 0) return node;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
182
684
|
for (const selector of promptSelectors) {
|
|
183
685
|
const node = document.querySelector(selector);
|
|
184
|
-
if (
|
|
185
|
-
|
|
686
|
+
if (node) return node;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
};
|
|
690
|
+
const composerAttachmentSelectors = [
|
|
691
|
+
'input[type="file"]',
|
|
692
|
+
'[data-testid*="attachment"]',
|
|
693
|
+
'[data-testid*="upload"]',
|
|
694
|
+
'[aria-label*="Remove"]',
|
|
695
|
+
'[aria-label*="remove"]',
|
|
696
|
+
];
|
|
697
|
+
const locateComposerRoot = () => {
|
|
698
|
+
const promptNode = findPromptNode();
|
|
699
|
+
if (promptNode) {
|
|
700
|
+
const initial =
|
|
701
|
+
promptNode.closest('[data-testid*="composer"]') ??
|
|
702
|
+
promptNode.closest('form') ??
|
|
703
|
+
promptNode.parentElement ??
|
|
704
|
+
document.body;
|
|
705
|
+
let current = initial;
|
|
706
|
+
let fallback = initial;
|
|
707
|
+
while (current && current !== document.body) {
|
|
708
|
+
const hasSend = sendSelectors.some((selector) => current.querySelector(selector));
|
|
709
|
+
if (hasSend) {
|
|
710
|
+
fallback = current;
|
|
711
|
+
const hasAttachment = composerAttachmentSelectors.some((selector) => current.querySelector(selector));
|
|
712
|
+
if (hasAttachment) {
|
|
713
|
+
return current;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
current = current.parentElement;
|
|
717
|
+
}
|
|
718
|
+
return fallback ?? initial;
|
|
186
719
|
}
|
|
187
720
|
return document.querySelector('form') ?? document.body;
|
|
188
721
|
};
|
|
189
722
|
const root = locateComposerRoot();
|
|
190
|
-
const
|
|
723
|
+
const scope = (() => {
|
|
724
|
+
if (!root) return document.body;
|
|
725
|
+
const parent = root.parentElement;
|
|
726
|
+
const parentHasSend = parent && sendSelectors.some((selector) => parent.querySelector(selector));
|
|
727
|
+
return parentHasSend ? parent : root;
|
|
728
|
+
})();
|
|
729
|
+
const chipContainer = scope ?? document;
|
|
191
730
|
const chipSelector = '[data-testid*="attachment"],[data-testid*="chip"],[data-testid*="upload"],[aria-label*="Remove"],[aria-label*="remove"]';
|
|
192
731
|
const chips = Array.from(chipContainer.querySelectorAll(chipSelector))
|
|
193
732
|
.slice(0, 20)
|
|
@@ -197,92 +736,392 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
197
736
|
title: node.getAttribute?.('title') ?? '',
|
|
198
737
|
testid: node.getAttribute?.('data-testid') ?? '',
|
|
199
738
|
}));
|
|
739
|
+
const uploadingSelectors = ${JSON.stringify(UPLOAD_STATUS_SELECTORS)};
|
|
740
|
+
const uploading = uploadingSelectors.some((selector) => {
|
|
741
|
+
return Array.from(document.querySelectorAll(selector)).some((node) => {
|
|
742
|
+
const ariaBusy = node.getAttribute?.('aria-busy');
|
|
743
|
+
const dataState = node.getAttribute?.('data-state');
|
|
744
|
+
if (ariaBusy === 'true' || dataState === 'loading' || dataState === 'uploading' || dataState === 'pending') {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
const text = node.textContent?.toLowerCase?.() ?? '';
|
|
748
|
+
return /\\buploading\\b/.test(text) || /\\bprocessing\\b/.test(text);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
200
751
|
const input = document.querySelector('input[type="file"][data-oracle-upload-idx="${idx}"]');
|
|
201
752
|
const inputNames =
|
|
202
753
|
input instanceof HTMLInputElement
|
|
203
754
|
? Array.from(input.files || []).map((f) => f?.name ?? '').filter(Boolean)
|
|
204
755
|
: [];
|
|
205
756
|
const composerText = (chipContainer.innerText || '').toLowerCase();
|
|
206
|
-
return {
|
|
757
|
+
return {
|
|
758
|
+
chipCount: chipContainer.querySelectorAll(chipSelector).length,
|
|
759
|
+
chips,
|
|
760
|
+
inputNames,
|
|
761
|
+
composerText,
|
|
762
|
+
uploading,
|
|
763
|
+
};
|
|
207
764
|
})()`;
|
|
765
|
+
let confirmedAttachment = false;
|
|
766
|
+
let lastInputNames = [];
|
|
767
|
+
let lastInputValue = '';
|
|
208
768
|
let finalSnapshot = null;
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!resultNode?.nodeId) {
|
|
215
|
-
continue;
|
|
769
|
+
const resolveInputNameCandidates = () => {
|
|
770
|
+
const snapshot = finalSnapshot;
|
|
771
|
+
const snapshotNames = snapshot?.inputNames;
|
|
772
|
+
if (Array.isArray(snapshotNames) && snapshotNames.length > 0) {
|
|
773
|
+
return snapshotNames;
|
|
216
774
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
775
|
+
return lastInputNames;
|
|
776
|
+
};
|
|
777
|
+
if (!inputConfirmed) {
|
|
778
|
+
for (let orderIndex = 0; orderIndex < candidateOrder.length; orderIndex += 1) {
|
|
779
|
+
const idx = candidateOrder[orderIndex];
|
|
780
|
+
const queuedSignals = await readAttachmentSignals(expectedName);
|
|
781
|
+
if (queuedSignals.ui ||
|
|
782
|
+
isExpectedSatisfied(queuedSignals) ||
|
|
783
|
+
hasChipDelta(queuedSignals) ||
|
|
784
|
+
hasUploadDelta(queuedSignals) ||
|
|
785
|
+
hasFileCountDelta(queuedSignals)) {
|
|
786
|
+
confirmedAttachment = true;
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
if (queuedSignals.input || hasInputDelta(queuedSignals)) {
|
|
790
|
+
inputConfirmed = true;
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
const resultNode = await dom.querySelector({
|
|
794
|
+
nodeId: documentNode.root.nodeId,
|
|
795
|
+
selector: `input[type="file"][data-oracle-upload-idx="${idx}"]`,
|
|
796
|
+
});
|
|
797
|
+
if (!resultNode?.nodeId) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const baselineInputSnapshot = await readInputSnapshot(idx);
|
|
801
|
+
const gatherSignals = async () => {
|
|
802
|
+
const signalResult = await waitForAttachmentUiSignal(attachmentUiSignalWaitMs);
|
|
803
|
+
const postInputSnapshot = await readInputSnapshot(idx);
|
|
804
|
+
const postInputSignals = inputSignalsFor(baselineInputSnapshot, postInputSnapshot);
|
|
805
|
+
const snapshot = await runtime
|
|
806
|
+
.evaluate({ expression: composerSnapshotFor(idx), returnByValue: true })
|
|
807
|
+
.then((res) => res?.result?.value)
|
|
808
|
+
.catch(() => undefined);
|
|
809
|
+
if (snapshot) {
|
|
810
|
+
finalSnapshot = {
|
|
811
|
+
chipCount: Number(snapshot.chipCount ?? 0),
|
|
812
|
+
chips: Array.isArray(snapshot.chips) ? snapshot.chips : [],
|
|
813
|
+
inputNames: Array.isArray(snapshot.inputNames) ? snapshot.inputNames : [],
|
|
814
|
+
composerText: typeof snapshot.composerText === 'string' ? snapshot.composerText : '',
|
|
815
|
+
uploading: Boolean(snapshot.uploading),
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
lastInputNames = postInputSnapshot.names;
|
|
819
|
+
lastInputValue = postInputSnapshot.value;
|
|
820
|
+
return { signalResult, postInputSignals };
|
|
821
|
+
};
|
|
822
|
+
const evaluateSignals = async (signalResult, postInputSignals, immediateInputMatch) => {
|
|
823
|
+
const expectedSatisfied = Boolean(signalResult?.expectedSatisfied) ||
|
|
824
|
+
(signalResult?.signals ? isExpectedSatisfied(signalResult.signals) : false);
|
|
825
|
+
const uiAcknowledged = Boolean(signalResult?.signals?.ui) ||
|
|
826
|
+
Boolean(signalResult?.chipDelta) ||
|
|
827
|
+
Boolean(signalResult?.uploadDelta) ||
|
|
828
|
+
Boolean(signalResult?.fileCountDelta) ||
|
|
829
|
+
expectedSatisfied;
|
|
830
|
+
if (uiAcknowledged) {
|
|
831
|
+
return { status: 'ui' };
|
|
832
|
+
}
|
|
833
|
+
const postSignals = await readAttachmentSignals(expectedName);
|
|
834
|
+
if (postSignals.ui ||
|
|
835
|
+
isExpectedSatisfied(postSignals) ||
|
|
836
|
+
hasChipDelta(postSignals) ||
|
|
837
|
+
hasUploadDelta(postSignals) ||
|
|
838
|
+
hasFileCountDelta(postSignals)) {
|
|
839
|
+
return { status: 'ui' };
|
|
253
840
|
}
|
|
841
|
+
const inputNameCandidates = resolveInputNameCandidates();
|
|
842
|
+
const inputHasFile = inputNameCandidates.some((name) => matchesExpectedName(name)) ||
|
|
843
|
+
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
844
|
+
const inputSignal = immediateInputMatch ||
|
|
845
|
+
postInputSignals.touched ||
|
|
846
|
+
Boolean(signalResult?.signals?.input) ||
|
|
847
|
+
Boolean(signalResult?.inputDelta) ||
|
|
848
|
+
inputHasFile ||
|
|
849
|
+
postSignals.input ||
|
|
850
|
+
hasInputDelta(postSignals);
|
|
851
|
+
if (inputSignal) {
|
|
852
|
+
return { status: 'input' };
|
|
853
|
+
}
|
|
854
|
+
return { status: 'none' };
|
|
855
|
+
};
|
|
856
|
+
const runInputAttempt = async (mode) => {
|
|
857
|
+
let immediateInputSnapshot = await readInputSnapshot(idx);
|
|
858
|
+
let hasExpectedFile = snapshotMatchesExpected(immediateInputSnapshot);
|
|
859
|
+
if (!hasExpectedFile) {
|
|
860
|
+
if (mode === 'set') {
|
|
861
|
+
await dom.setFileInputFiles({ nodeId: resultNode.nodeId, files: [attachment.path] });
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
const selector = `input[type="file"][data-oracle-upload-idx="${idx}"]`;
|
|
865
|
+
try {
|
|
866
|
+
await transferAttachmentViaDataTransfer(runtime, attachment, selector);
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
logger(`Attachment data transfer failed: ${error?.message ?? String(error)}`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
immediateInputSnapshot = await readInputSnapshot(idx);
|
|
873
|
+
hasExpectedFile = snapshotMatchesExpected(immediateInputSnapshot);
|
|
874
|
+
}
|
|
875
|
+
const immediateSignals = inputSignalsFor(baselineInputSnapshot, immediateInputSnapshot);
|
|
876
|
+
lastInputNames = immediateInputSnapshot.names;
|
|
877
|
+
lastInputValue = immediateInputSnapshot.value;
|
|
878
|
+
const immediateInputMatch = immediateSignals.touched || hasExpectedFile;
|
|
879
|
+
if (immediateInputMatch) {
|
|
880
|
+
inputConfirmed = true;
|
|
881
|
+
}
|
|
882
|
+
const signalState = await gatherSignals();
|
|
883
|
+
const evaluation = await evaluateSignals(signalState.signalResult, signalState.postInputSignals, immediateInputMatch);
|
|
884
|
+
return { evaluation, signalState, immediateInputMatch };
|
|
885
|
+
};
|
|
886
|
+
let result = await runInputAttempt('set');
|
|
887
|
+
if (result.evaluation.status === 'ui') {
|
|
888
|
+
confirmedAttachment = true;
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
if (result.evaluation.status === 'input') {
|
|
892
|
+
logger('Attachment input set; proceeding without UI confirmation.');
|
|
893
|
+
inputConfirmed = true;
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
const lateSignals = await readAttachmentSignals(expectedName);
|
|
897
|
+
if (lateSignals.ui ||
|
|
898
|
+
isExpectedSatisfied(lateSignals) ||
|
|
899
|
+
hasChipDelta(lateSignals) ||
|
|
900
|
+
hasUploadDelta(lateSignals) ||
|
|
901
|
+
hasFileCountDelta(lateSignals)) {
|
|
902
|
+
confirmedAttachment = true;
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
if (lateSignals.input || hasInputDelta(lateSignals)) {
|
|
906
|
+
logger('Attachment input set; proceeding without UI confirmation.');
|
|
907
|
+
inputConfirmed = true;
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
logger('Attachment not acknowledged after file input set; retrying with data transfer.');
|
|
911
|
+
result = await runInputAttempt('transfer');
|
|
912
|
+
if (result.evaluation.status === 'ui') {
|
|
913
|
+
confirmedAttachment = true;
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
if (result.evaluation.status === 'input') {
|
|
917
|
+
logger('Attachment input set; proceeding without UI confirmation.');
|
|
918
|
+
inputConfirmed = true;
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
if (orderIndex < candidateOrder.length - 1) {
|
|
922
|
+
await dom.setFileInputFiles({ nodeId: resultNode.nodeId, files: [] }).catch(() => undefined);
|
|
923
|
+
await delay(150);
|
|
254
924
|
}
|
|
255
|
-
await delay(250);
|
|
256
|
-
}
|
|
257
|
-
const inputHasFile = finalSnapshot?.inputNames?.some((name) => name.toLowerCase().includes(expectedName.toLowerCase())) ?? false;
|
|
258
|
-
const uiAcknowledged = (finalSnapshot?.chipCount ?? 0) > baselineChipCount;
|
|
259
|
-
if (inputHasFile && uiAcknowledged) {
|
|
260
|
-
break;
|
|
261
925
|
}
|
|
262
926
|
}
|
|
263
|
-
|
|
264
|
-
|
|
927
|
+
if (confirmedAttachment) {
|
|
928
|
+
const inputNameCandidates = resolveInputNameCandidates();
|
|
929
|
+
const inputHasFile = inputNameCandidates.some((name) => matchesExpectedName(name)) ||
|
|
930
|
+
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
931
|
+
await waitForAttachmentVisible(runtime, expectedName, attachmentUiTimeoutMs, logger);
|
|
932
|
+
logger(inputHasFile ? 'Attachment queued (UI anchored, file input confirmed)' : 'Attachment queued (UI anchored)');
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
const inputNameCandidates = resolveInputNameCandidates();
|
|
936
|
+
const inputHasFile = inputNameCandidates.some((name) => matchesExpectedName(name)) ||
|
|
937
|
+
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
265
938
|
if (await waitForAttachmentAnchored(runtime, expectedName, attachmentUiTimeoutMs)) {
|
|
266
939
|
await waitForAttachmentVisible(runtime, expectedName, attachmentUiTimeoutMs, logger);
|
|
267
940
|
logger(inputHasFile ? 'Attachment queued (UI anchored, file input confirmed)' : 'Attachment queued (UI anchored)');
|
|
268
|
-
return;
|
|
941
|
+
return true;
|
|
269
942
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
await logDomFailure(runtime, logger, 'file-upload-missing');
|
|
274
|
-
throw new Error('Attachment input accepted the file but ChatGPT did not acknowledge it in the composer UI.');
|
|
943
|
+
if (inputConfirmed || inputHasFile) {
|
|
944
|
+
logger('Attachment input accepted the file but UI did not acknowledge it; continuing with input confirmation only.');
|
|
945
|
+
return true;
|
|
275
946
|
}
|
|
276
947
|
await logDomFailure(runtime, logger, 'file-upload-missing');
|
|
277
948
|
throw new Error('Attachment did not register with the ChatGPT composer in time.');
|
|
278
949
|
}
|
|
950
|
+
export async function clearComposerAttachments(Runtime, timeoutMs, logger) {
|
|
951
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
952
|
+
const expression = `(() => {
|
|
953
|
+
const promptSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
954
|
+
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
955
|
+
const findPromptNode = () => {
|
|
956
|
+
for (const selector of promptSelectors) {
|
|
957
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
958
|
+
for (const node of nodes) {
|
|
959
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
960
|
+
const rect = node.getBoundingClientRect();
|
|
961
|
+
if (rect.width > 0 && rect.height > 0) return node;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
for (const selector of promptSelectors) {
|
|
965
|
+
const node = document.querySelector(selector);
|
|
966
|
+
if (node) return node;
|
|
967
|
+
}
|
|
968
|
+
return null;
|
|
969
|
+
};
|
|
970
|
+
const attachmentSelectors = [
|
|
971
|
+
'input[type="file"]',
|
|
972
|
+
'[data-testid*="attachment"]',
|
|
973
|
+
'[data-testid*="upload"]',
|
|
974
|
+
'[aria-label*="Remove"]',
|
|
975
|
+
'[aria-label*="remove"]',
|
|
976
|
+
];
|
|
977
|
+
const locateComposerRoot = () => {
|
|
978
|
+
const promptNode = findPromptNode();
|
|
979
|
+
if (promptNode) {
|
|
980
|
+
const initial =
|
|
981
|
+
promptNode.closest('[data-testid*="composer"]') ??
|
|
982
|
+
promptNode.closest('form') ??
|
|
983
|
+
promptNode.parentElement ??
|
|
984
|
+
document.body;
|
|
985
|
+
let current = initial;
|
|
986
|
+
let fallback = initial;
|
|
987
|
+
while (current && current !== document.body) {
|
|
988
|
+
const hasSend = sendSelectors.some((selector) => current.querySelector(selector));
|
|
989
|
+
if (hasSend) {
|
|
990
|
+
fallback = current;
|
|
991
|
+
const hasAttachment = attachmentSelectors.some((selector) => current.querySelector(selector));
|
|
992
|
+
if (hasAttachment) {
|
|
993
|
+
return current;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
current = current.parentElement;
|
|
997
|
+
}
|
|
998
|
+
return fallback ?? initial;
|
|
999
|
+
}
|
|
1000
|
+
return document.querySelector('form') ?? document.body;
|
|
1001
|
+
};
|
|
1002
|
+
const root = locateComposerRoot();
|
|
1003
|
+
const scope = (() => {
|
|
1004
|
+
if (!root) return document.body;
|
|
1005
|
+
const parent = root.parentElement;
|
|
1006
|
+
const parentHasSend = parent && sendSelectors.some((selector) => parent.querySelector(selector));
|
|
1007
|
+
return parentHasSend ? parent : root;
|
|
1008
|
+
})();
|
|
1009
|
+
const removeSelectors = [
|
|
1010
|
+
'[aria-label*="Remove"]',
|
|
1011
|
+
'[aria-label*="remove"]',
|
|
1012
|
+
'button[aria-label*="Remove"]',
|
|
1013
|
+
'button[aria-label*="remove"]',
|
|
1014
|
+
'[data-testid*="remove"]',
|
|
1015
|
+
'[data-testid*="delete"]',
|
|
1016
|
+
];
|
|
1017
|
+
const visible = (el) => {
|
|
1018
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
1019
|
+
const rect = el.getBoundingClientRect();
|
|
1020
|
+
return rect.width > 0 && rect.height > 0;
|
|
1021
|
+
};
|
|
1022
|
+
const removeButtons = scope
|
|
1023
|
+
? Array.from(scope.querySelectorAll(removeSelectors.join(','))).filter(visible)
|
|
1024
|
+
: [];
|
|
1025
|
+
for (const button of removeButtons.slice(0, 20)) {
|
|
1026
|
+
try { button.click(); } catch {}
|
|
1027
|
+
}
|
|
1028
|
+
const chipSelector = '[data-testid*="attachment"],[data-testid*="chip"],[data-testid*="upload"],[aria-label*="Remove"],[aria-label*="remove"]';
|
|
1029
|
+
const chipCount = scope ? scope.querySelectorAll(chipSelector).length : 0;
|
|
1030
|
+
const inputs = scope ? Array.from(scope.querySelectorAll('input[type="file"]')) : [];
|
|
1031
|
+
let inputCount = 0;
|
|
1032
|
+
for (const input of inputs) {
|
|
1033
|
+
if (!(input instanceof HTMLInputElement)) continue;
|
|
1034
|
+
inputCount += Array.from(input.files || []).length;
|
|
1035
|
+
try { input.value = ''; } catch {}
|
|
1036
|
+
}
|
|
1037
|
+
const hadAttachments = chipCount > 0 || inputCount > 0 || removeButtons.length > 0;
|
|
1038
|
+
return { removeClicks: removeButtons.length, chipCount, inputCount, hadAttachments };
|
|
1039
|
+
})()`;
|
|
1040
|
+
let sawAttachments = false;
|
|
1041
|
+
let lastState = null;
|
|
1042
|
+
while (Date.now() < deadline) {
|
|
1043
|
+
const response = await Runtime.evaluate({ expression, returnByValue: true });
|
|
1044
|
+
const value = response.result?.value;
|
|
1045
|
+
if (value?.hadAttachments) {
|
|
1046
|
+
sawAttachments = true;
|
|
1047
|
+
}
|
|
1048
|
+
const chipCount = typeof value?.chipCount === 'number' ? value.chipCount : 0;
|
|
1049
|
+
const inputCount = typeof value?.inputCount === 'number' ? value.inputCount : 0;
|
|
1050
|
+
lastState = { chipCount, inputCount };
|
|
1051
|
+
if (chipCount === 0 && inputCount === 0) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
await delay(250);
|
|
1055
|
+
}
|
|
1056
|
+
if (sawAttachments) {
|
|
1057
|
+
logger?.(`Attachment cleanup timed out; still saw ${lastState?.chipCount ?? 0} chips and ${lastState?.inputCount ?? 0} inputs.`);
|
|
1058
|
+
throw new Error('Existing attachments still present in composer; aborting to avoid duplicate uploads.');
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
279
1061
|
export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNames = [], logger) {
|
|
280
1062
|
const deadline = Date.now() + timeoutMs;
|
|
281
1063
|
const expectedNormalized = expectedNames.map((name) => name.toLowerCase());
|
|
282
1064
|
let inputMatchSince = null;
|
|
1065
|
+
let sawInputMatch = false;
|
|
283
1066
|
let attachmentMatchSince = null;
|
|
1067
|
+
let lastVerboseLog = 0;
|
|
284
1068
|
const expression = `(() => {
|
|
285
1069
|
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
1070
|
+
const promptSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
1071
|
+
const findPromptNode = () => {
|
|
1072
|
+
for (const selector of promptSelectors) {
|
|
1073
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
1074
|
+
for (const node of nodes) {
|
|
1075
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
1076
|
+
const rect = node.getBoundingClientRect();
|
|
1077
|
+
if (rect.width > 0 && rect.height > 0) return node;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
for (const selector of promptSelectors) {
|
|
1081
|
+
const node = document.querySelector(selector);
|
|
1082
|
+
if (node) return node;
|
|
1083
|
+
}
|
|
1084
|
+
return null;
|
|
1085
|
+
};
|
|
1086
|
+
const attachmentSelectors = [
|
|
1087
|
+
'input[type="file"]',
|
|
1088
|
+
'[data-testid*="attachment"]',
|
|
1089
|
+
'[data-testid*="upload"]',
|
|
1090
|
+
'[aria-label*="Remove"]',
|
|
1091
|
+
'[aria-label*="remove"]',
|
|
1092
|
+
];
|
|
1093
|
+
const locateComposerRoot = () => {
|
|
1094
|
+
const promptNode = findPromptNode();
|
|
1095
|
+
if (promptNode) {
|
|
1096
|
+
const initial =
|
|
1097
|
+
promptNode.closest('[data-testid*="composer"]') ??
|
|
1098
|
+
promptNode.closest('form') ??
|
|
1099
|
+
promptNode.parentElement ??
|
|
1100
|
+
document.body;
|
|
1101
|
+
let current = initial;
|
|
1102
|
+
let fallback = initial;
|
|
1103
|
+
while (current && current !== document.body) {
|
|
1104
|
+
const hasSend = sendSelectors.some((selector) => current.querySelector(selector));
|
|
1105
|
+
if (hasSend) {
|
|
1106
|
+
fallback = current;
|
|
1107
|
+
const hasAttachment = attachmentSelectors.some((selector) => current.querySelector(selector));
|
|
1108
|
+
if (hasAttachment) {
|
|
1109
|
+
return current;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
current = current.parentElement;
|
|
1113
|
+
}
|
|
1114
|
+
return fallback ?? initial;
|
|
1115
|
+
}
|
|
1116
|
+
return document.querySelector('form') ?? document.body;
|
|
1117
|
+
};
|
|
1118
|
+
const composerRoot = locateComposerRoot();
|
|
1119
|
+
const composerScope = (() => {
|
|
1120
|
+
if (!composerRoot) return document;
|
|
1121
|
+
const parent = composerRoot.parentElement;
|
|
1122
|
+
const parentHasSend = parent && sendSelectors.some((selector) => parent.querySelector(selector));
|
|
1123
|
+
return parentHasSend ? parent : composerRoot;
|
|
1124
|
+
})();
|
|
286
1125
|
let button = null;
|
|
287
1126
|
for (const selector of sendSelectors) {
|
|
288
1127
|
button = document.querySelector(selector);
|
|
@@ -307,39 +1146,168 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
307
1146
|
return /\buploading\b/.test(text) || /\bprocessing\b/.test(text);
|
|
308
1147
|
});
|
|
309
1148
|
});
|
|
310
|
-
const
|
|
1149
|
+
const attachmentChipSelectors = [
|
|
1150
|
+
'[data-testid*="chip"]',
|
|
1151
|
+
'[data-testid*="attachment"]',
|
|
1152
|
+
'[data-testid*="upload"]',
|
|
1153
|
+
'[data-testid*="file"]',
|
|
1154
|
+
'[aria-label*="Remove"]',
|
|
1155
|
+
'button[aria-label*="Remove"]',
|
|
1156
|
+
];
|
|
311
1157
|
const attachedNames = [];
|
|
312
|
-
for (const selector of
|
|
313
|
-
for (const node of Array.from(
|
|
314
|
-
|
|
315
|
-
|
|
1158
|
+
for (const selector of attachmentChipSelectors) {
|
|
1159
|
+
for (const node of Array.from(composerScope.querySelectorAll(selector))) {
|
|
1160
|
+
if (!node) continue;
|
|
1161
|
+
const text = node.textContent ?? '';
|
|
1162
|
+
const aria = node.getAttribute?.('aria-label') ?? '';
|
|
1163
|
+
const title = node.getAttribute?.('title') ?? '';
|
|
1164
|
+
const parentText = node.parentElement?.parentElement?.innerText ?? '';
|
|
1165
|
+
for (const value of [text, aria, title, parentText]) {
|
|
1166
|
+
const normalized = value?.toLowerCase?.();
|
|
1167
|
+
if (normalized) attachedNames.push(normalized);
|
|
1168
|
+
}
|
|
316
1169
|
}
|
|
317
1170
|
}
|
|
318
|
-
const cardTexts = Array.from(
|
|
1171
|
+
const cardTexts = Array.from(composerScope.querySelectorAll('[aria-label*="Remove"]')).map((btn) =>
|
|
319
1172
|
btn?.parentElement?.parentElement?.innerText?.toLowerCase?.() ?? '',
|
|
320
1173
|
);
|
|
321
1174
|
attachedNames.push(...cardTexts.filter(Boolean));
|
|
322
1175
|
|
|
323
1176
|
const inputNames = [];
|
|
324
|
-
|
|
1177
|
+
const inputScope = composerScope ? Array.from(composerScope.querySelectorAll('input[type="file"]')) : [];
|
|
1178
|
+
const inputNodes = [];
|
|
1179
|
+
const inputSeen = new Set();
|
|
1180
|
+
for (const el of [...inputScope, ...Array.from(document.querySelectorAll('input[type="file"]'))]) {
|
|
1181
|
+
if (!inputSeen.has(el)) {
|
|
1182
|
+
inputSeen.add(el);
|
|
1183
|
+
inputNodes.push(el);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
for (const input of inputNodes) {
|
|
325
1187
|
if (!(input instanceof HTMLInputElement) || !input.files?.length) continue;
|
|
326
1188
|
for (const file of Array.from(input.files)) {
|
|
327
1189
|
if (file?.name) inputNames.push(file.name.toLowerCase());
|
|
328
1190
|
}
|
|
329
1191
|
}
|
|
330
|
-
const
|
|
331
|
-
|
|
1192
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+files?\\b/;
|
|
1193
|
+
const fileCountSelectors = [
|
|
1194
|
+
'button',
|
|
1195
|
+
'[role="button"]',
|
|
1196
|
+
'[data-testid*="file"]',
|
|
1197
|
+
'[data-testid*="upload"]',
|
|
1198
|
+
'[data-testid*="attachment"]',
|
|
1199
|
+
'[data-testid*="chip"]',
|
|
1200
|
+
'[aria-label*="file"]',
|
|
1201
|
+
'[title*="file"]',
|
|
1202
|
+
'[aria-label*="attachment"]',
|
|
1203
|
+
'[title*="attachment"]',
|
|
1204
|
+
].join(',');
|
|
1205
|
+
const collectFileCount = (nodes) => {
|
|
1206
|
+
let count = 0;
|
|
1207
|
+
for (const node of nodes) {
|
|
1208
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
1209
|
+
if (node.matches('textarea,input,[contenteditable="true"]')) continue;
|
|
1210
|
+
const dataTestId = node.getAttribute?.('data-testid') ?? '';
|
|
1211
|
+
const aria = node.getAttribute?.('aria-label') ?? '';
|
|
1212
|
+
const title = node.getAttribute?.('title') ?? '';
|
|
1213
|
+
const tooltip =
|
|
1214
|
+
node.getAttribute?.('data-tooltip') ?? node.getAttribute?.('data-tooltip-content') ?? '';
|
|
1215
|
+
const text = node.textContent ?? '';
|
|
1216
|
+
const parent = node.parentElement;
|
|
1217
|
+
const parentText = parent?.textContent ?? '';
|
|
1218
|
+
const parentAria = parent?.getAttribute?.('aria-label') ?? '';
|
|
1219
|
+
const parentTitle = parent?.getAttribute?.('title') ?? '';
|
|
1220
|
+
const parentTooltip =
|
|
1221
|
+
parent?.getAttribute?.('data-tooltip') ?? parent?.getAttribute?.('data-tooltip-content') ?? '';
|
|
1222
|
+
const parentTestId = parent?.getAttribute?.('data-testid') ?? '';
|
|
1223
|
+
const candidates = [
|
|
1224
|
+
text,
|
|
1225
|
+
aria,
|
|
1226
|
+
title,
|
|
1227
|
+
tooltip,
|
|
1228
|
+
dataTestId,
|
|
1229
|
+
parentText,
|
|
1230
|
+
parentAria,
|
|
1231
|
+
parentTitle,
|
|
1232
|
+
parentTooltip,
|
|
1233
|
+
parentTestId,
|
|
1234
|
+
];
|
|
1235
|
+
let hasFileHint = false;
|
|
1236
|
+
for (const raw of candidates) {
|
|
1237
|
+
if (!raw) continue;
|
|
1238
|
+
if (String(raw).toLowerCase().includes('file')) {
|
|
1239
|
+
hasFileHint = true;
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (!hasFileHint) continue;
|
|
1244
|
+
for (const raw of candidates) {
|
|
1245
|
+
if (!raw) continue;
|
|
1246
|
+
const match = String(raw).toLowerCase().match(countRegex);
|
|
1247
|
+
if (match) {
|
|
1248
|
+
const parsed = Number(match[1]);
|
|
1249
|
+
if (Number.isFinite(parsed)) {
|
|
1250
|
+
count = Math.max(count, parsed);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return count;
|
|
1256
|
+
};
|
|
1257
|
+
const localFileCountNodes = composerScope
|
|
1258
|
+
? Array.from(composerScope.querySelectorAll(fileCountSelectors))
|
|
1259
|
+
: [];
|
|
1260
|
+
let fileCount = collectFileCount(localFileCountNodes);
|
|
1261
|
+
if (!fileCount) {
|
|
1262
|
+
fileCount = collectFileCount(Array.from(document.querySelectorAll(fileCountSelectors)));
|
|
1263
|
+
}
|
|
1264
|
+
const filesAttached = attachedNames.length > 0 || fileCount > 0;
|
|
1265
|
+
return {
|
|
1266
|
+
state: button ? (disabled ? 'disabled' : 'ready') : 'missing',
|
|
1267
|
+
uploading,
|
|
1268
|
+
filesAttached,
|
|
1269
|
+
attachedNames,
|
|
1270
|
+
inputNames,
|
|
1271
|
+
fileCount,
|
|
1272
|
+
};
|
|
332
1273
|
})()`;
|
|
333
1274
|
while (Date.now() < deadline) {
|
|
334
|
-
const
|
|
1275
|
+
const response = await Runtime.evaluate({ expression, returnByValue: true });
|
|
1276
|
+
const { result } = response;
|
|
335
1277
|
const value = result?.value;
|
|
1278
|
+
if (!value && logger?.verbose) {
|
|
1279
|
+
const exception = response
|
|
1280
|
+
?.exceptionDetails;
|
|
1281
|
+
if (exception) {
|
|
1282
|
+
const details = [exception.text, exception.exception?.description]
|
|
1283
|
+
.filter((part) => Boolean(part))
|
|
1284
|
+
.join(' - ');
|
|
1285
|
+
logger(`Attachment wait eval failed: ${details || 'unknown error'}`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
336
1288
|
if (value) {
|
|
1289
|
+
if (logger?.verbose) {
|
|
1290
|
+
const now = Date.now();
|
|
1291
|
+
if (now - lastVerboseLog > 3000) {
|
|
1292
|
+
lastVerboseLog = now;
|
|
1293
|
+
logger(`Attachment wait state: ${JSON.stringify({
|
|
1294
|
+
state: value.state,
|
|
1295
|
+
uploading: value.uploading,
|
|
1296
|
+
filesAttached: value.filesAttached,
|
|
1297
|
+
attachedNames: (value.attachedNames ?? []).slice(0, 3),
|
|
1298
|
+
inputNames: (value.inputNames ?? []).slice(0, 3),
|
|
1299
|
+
fileCount: value.fileCount ?? 0,
|
|
1300
|
+
})}`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
337
1303
|
const attachedNames = (value.attachedNames ?? [])
|
|
338
1304
|
.map((name) => name.toLowerCase().replace(/\s+/g, ' ').trim())
|
|
339
1305
|
.filter(Boolean);
|
|
340
1306
|
const inputNames = (value.inputNames ?? [])
|
|
341
1307
|
.map((name) => name.toLowerCase().replace(/\s+/g, ' ').trim())
|
|
342
1308
|
.filter(Boolean);
|
|
1309
|
+
const fileCount = typeof value.fileCount === 'number' ? value.fileCount : 0;
|
|
1310
|
+
const fileCountSatisfied = expectedNormalized.length > 0 && fileCount >= expectedNormalized.length;
|
|
343
1311
|
const matchesExpected = (expected) => {
|
|
344
1312
|
const baseName = expected.split('/').pop()?.split('\\').pop() ?? expected;
|
|
345
1313
|
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
@@ -363,24 +1331,23 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
363
1331
|
});
|
|
364
1332
|
};
|
|
365
1333
|
const missing = expectedNormalized.filter((expected) => !matchesExpected(expected));
|
|
366
|
-
if (missing.length === 0) {
|
|
1334
|
+
if (missing.length === 0 || fileCountSatisfied) {
|
|
367
1335
|
const stableThresholdMs = value.uploading ? 3000 : 1500;
|
|
368
|
-
if (
|
|
369
|
-
|
|
370
|
-
attachmentMatchSince = Date.now();
|
|
371
|
-
}
|
|
372
|
-
if (Date.now() - attachmentMatchSince > stableThresholdMs) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
1336
|
+
if (attachmentMatchSince === null) {
|
|
1337
|
+
attachmentMatchSince = Date.now();
|
|
375
1338
|
}
|
|
376
|
-
|
|
377
|
-
|
|
1339
|
+
const stable = Date.now() - attachmentMatchSince > stableThresholdMs;
|
|
1340
|
+
if (stable && value.state === 'ready') {
|
|
1341
|
+
return;
|
|
378
1342
|
}
|
|
379
|
-
if (value.state === '
|
|
1343
|
+
if (stable && value.state === 'disabled' && !value.uploading) {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (value.state === 'missing' && (value.filesAttached || fileCountSatisfied)) {
|
|
380
1347
|
return;
|
|
381
1348
|
}
|
|
382
1349
|
// If files are attached but button isn't ready yet, give it more time but don't fail immediately.
|
|
383
|
-
if (value.filesAttached) {
|
|
1350
|
+
if (value.filesAttached || fileCountSatisfied) {
|
|
384
1351
|
await delay(500);
|
|
385
1352
|
continue;
|
|
386
1353
|
}
|
|
@@ -396,16 +1363,19 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
396
1363
|
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, '');
|
|
397
1364
|
return !inputNames.some((raw) => raw.includes(normalizedExpected) || (expectedNoExt.length >= 6 && raw.includes(expectedNoExt)));
|
|
398
1365
|
});
|
|
399
|
-
|
|
400
|
-
|
|
1366
|
+
const inputStateOk = value.state === 'ready' || value.state === 'missing' || value.state === 'disabled';
|
|
1367
|
+
const inputSeenNow = inputMissing.length === 0 || fileCountSatisfied;
|
|
1368
|
+
const stableThresholdMs = value.uploading ? 3000 : 1500;
|
|
1369
|
+
if (inputSeenNow && inputStateOk) {
|
|
401
1370
|
if (inputMatchSince === null) {
|
|
402
1371
|
inputMatchSince = Date.now();
|
|
403
1372
|
}
|
|
404
|
-
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
1373
|
+
sawInputMatch = true;
|
|
407
1374
|
}
|
|
408
|
-
|
|
1375
|
+
if (inputMatchSince !== null && inputStateOk && Date.now() - inputMatchSince > stableThresholdMs) {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (!inputSeenNow && !sawInputMatch) {
|
|
409
1379
|
inputMatchSince = null;
|
|
410
1380
|
}
|
|
411
1381
|
}
|
|
@@ -417,7 +1387,7 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
417
1387
|
}
|
|
418
1388
|
export async function waitForUserTurnAttachments(Runtime, expectedNames, timeoutMs, logger) {
|
|
419
1389
|
if (!expectedNames || expectedNames.length === 0) {
|
|
420
|
-
return;
|
|
1390
|
+
return true;
|
|
421
1391
|
}
|
|
422
1392
|
const expectedNormalized = expectedNames.map((name) => name.toLowerCase());
|
|
423
1393
|
const conversationSelectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
@@ -437,9 +1407,55 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
437
1407
|
const title = el.getAttribute('title') || '';
|
|
438
1408
|
return (aria + ' ' + title).trim().toLowerCase();
|
|
439
1409
|
}).filter(Boolean);
|
|
440
|
-
|
|
1410
|
+
const attachmentSelectors = [
|
|
1411
|
+
'[data-testid*="attachment"]',
|
|
1412
|
+
'[data-testid*="upload"]',
|
|
1413
|
+
'[data-testid*="chip"]',
|
|
1414
|
+
'[aria-label*="file"]',
|
|
1415
|
+
'[aria-label*="attachment"]',
|
|
1416
|
+
'[title*="file"]',
|
|
1417
|
+
'[title*="attachment"]',
|
|
1418
|
+
];
|
|
1419
|
+
const hasAttachmentUi =
|
|
1420
|
+
lastUser.querySelectorAll(attachmentSelectors.join(',')).length > 0 ||
|
|
1421
|
+
attrs.some((attr) => attr.includes('file') || attr.includes('attachment'));
|
|
1422
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+files?\\b/;
|
|
1423
|
+
const fileCountNodes = Array.from(lastUser.querySelectorAll('button,span,div,[aria-label],[title]'));
|
|
1424
|
+
let fileCount = 0;
|
|
1425
|
+
for (const node of fileCountNodes) {
|
|
1426
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
1427
|
+
if (node.matches('textarea,input,[contenteditable="true"]')) continue;
|
|
1428
|
+
const dataTestId = node.getAttribute?.('data-testid') ?? '';
|
|
1429
|
+
const aria = node.getAttribute?.('aria-label') ?? '';
|
|
1430
|
+
const title = node.getAttribute?.('title') ?? '';
|
|
1431
|
+
const tooltip =
|
|
1432
|
+
node.getAttribute?.('data-tooltip') ?? node.getAttribute?.('data-tooltip-content') ?? '';
|
|
1433
|
+
const nodeText = node.textContent ?? '';
|
|
1434
|
+
const candidates = [nodeText, aria, title, tooltip, dataTestId];
|
|
1435
|
+
let hasFileHint = false;
|
|
1436
|
+
for (const raw of candidates) {
|
|
1437
|
+
if (!raw) continue;
|
|
1438
|
+
if (String(raw).toLowerCase().includes('file')) {
|
|
1439
|
+
hasFileHint = true;
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (!hasFileHint) continue;
|
|
1444
|
+
for (const raw of candidates) {
|
|
1445
|
+
if (!raw) continue;
|
|
1446
|
+
const match = String(raw).toLowerCase().match(countRegex);
|
|
1447
|
+
if (match) {
|
|
1448
|
+
const count = Number(match[1]);
|
|
1449
|
+
if (Number.isFinite(count)) {
|
|
1450
|
+
fileCount = Math.max(fileCount, count);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return { ok: true, text, attrs, fileCount, hasAttachmentUi };
|
|
441
1456
|
})()`;
|
|
442
1457
|
const deadline = Date.now() + timeoutMs;
|
|
1458
|
+
let sawAttachmentUi = false;
|
|
443
1459
|
while (Date.now() < deadline) {
|
|
444
1460
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
445
1461
|
const value = result?.value;
|
|
@@ -447,7 +1463,12 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
447
1463
|
await delay(200);
|
|
448
1464
|
continue;
|
|
449
1465
|
}
|
|
1466
|
+
if (value.hasAttachmentUi) {
|
|
1467
|
+
sawAttachmentUi = true;
|
|
1468
|
+
}
|
|
450
1469
|
const haystack = [value.text ?? '', ...(value.attrs ?? [])].join('\n');
|
|
1470
|
+
const fileCount = typeof value.fileCount === 'number' ? value.fileCount : 0;
|
|
1471
|
+
const fileCountSatisfied = fileCount >= expectedNormalized.length && expectedNormalized.length > 0;
|
|
451
1472
|
const missing = expectedNormalized.filter((expected) => {
|
|
452
1473
|
const baseName = expected.split('/').pop()?.split('\\').pop() ?? expected;
|
|
453
1474
|
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
@@ -458,11 +1479,15 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
458
1479
|
return false;
|
|
459
1480
|
return true;
|
|
460
1481
|
});
|
|
461
|
-
if (missing.length === 0) {
|
|
462
|
-
return;
|
|
1482
|
+
if (missing.length === 0 || fileCountSatisfied) {
|
|
1483
|
+
return true;
|
|
463
1484
|
}
|
|
464
1485
|
await delay(250);
|
|
465
1486
|
}
|
|
1487
|
+
if (!sawAttachmentUi) {
|
|
1488
|
+
logger?.('Sent user message did not expose attachment UI; skipping attachment verification.');
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
466
1491
|
logger?.('Sent user message did not show expected attachment names in time.');
|
|
467
1492
|
await logDomFailure(Runtime, logger ?? (() => { }), 'attachment-missing-user-turn');
|
|
468
1493
|
throw new Error('Attachment was not present on the sent user message.');
|
|
@@ -477,6 +1502,7 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
477
1502
|
const normalizedNoExt = normalized.replace(/\\.[a-z0-9]{1,10}$/i, '');
|
|
478
1503
|
const matchNode = (node) => {
|
|
479
1504
|
if (!node) return false;
|
|
1505
|
+
if (node.tagName === 'INPUT' && node.type === 'file') return false;
|
|
480
1506
|
const text = (node.textContent || '').toLowerCase();
|
|
481
1507
|
const aria = node.getAttribute?.('aria-label')?.toLowerCase?.() ?? '';
|
|
482
1508
|
const title = node.getAttribute?.('title')?.toLowerCase?.() ?? '';
|
|
@@ -486,7 +1512,7 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
486
1512
|
return candidates.some((value) => value.includes(normalized) || (normalizedNoExt.length >= 6 && value.includes(normalizedNoExt)));
|
|
487
1513
|
};
|
|
488
1514
|
|
|
489
|
-
const attachmentSelectors = ['[data-testid*="attachment"]','[data-testid*="chip"]','[data-testid*="upload"]'];
|
|
1515
|
+
const attachmentSelectors = ['[data-testid*="attachment"]','[data-testid*="chip"]','[data-testid*="upload"]','[data-testid*="file"]'];
|
|
490
1516
|
const attachmentMatch = attachmentSelectors.some((selector) =>
|
|
491
1517
|
Array.from(document.querySelectorAll(selector)).some(matchNode),
|
|
492
1518
|
);
|
|
@@ -501,6 +1527,77 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
501
1527
|
return { found: true, source: 'attachment-cards' };
|
|
502
1528
|
}
|
|
503
1529
|
|
|
1530
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+files?\\b/;
|
|
1531
|
+
const fileCountNodes = (() => {
|
|
1532
|
+
const nodes = [];
|
|
1533
|
+
const seen = new Set();
|
|
1534
|
+
const add = (node) => {
|
|
1535
|
+
if (!node || seen.has(node)) return;
|
|
1536
|
+
seen.add(node);
|
|
1537
|
+
nodes.push(node);
|
|
1538
|
+
};
|
|
1539
|
+
const root =
|
|
1540
|
+
document.querySelector('[data-testid*="composer"]') || document.querySelector('form') || document.body;
|
|
1541
|
+
const localNodes = root ? Array.from(root.querySelectorAll('button,span,div,[aria-label],[title]')) : [];
|
|
1542
|
+
for (const node of localNodes) add(node);
|
|
1543
|
+
for (const node of Array.from(document.querySelectorAll('button,span,div,[aria-label],[title]'))) {
|
|
1544
|
+
add(node);
|
|
1545
|
+
}
|
|
1546
|
+
return nodes;
|
|
1547
|
+
})();
|
|
1548
|
+
let fileCount = 0;
|
|
1549
|
+
for (const node of fileCountNodes) {
|
|
1550
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
1551
|
+
if (node.matches('textarea,input,[contenteditable="true"]')) continue;
|
|
1552
|
+
const dataTestId = node.getAttribute?.('data-testid') ?? '';
|
|
1553
|
+
const aria = node.getAttribute?.('aria-label') ?? '';
|
|
1554
|
+
const title = node.getAttribute?.('title') ?? '';
|
|
1555
|
+
const tooltip =
|
|
1556
|
+
node.getAttribute?.('data-tooltip') ?? node.getAttribute?.('data-tooltip-content') ?? '';
|
|
1557
|
+
const text = node.textContent ?? '';
|
|
1558
|
+
const parent = node.parentElement;
|
|
1559
|
+
const parentText = parent?.textContent ?? '';
|
|
1560
|
+
const parentAria = parent?.getAttribute?.('aria-label') ?? '';
|
|
1561
|
+
const parentTitle = parent?.getAttribute?.('title') ?? '';
|
|
1562
|
+
const parentTooltip =
|
|
1563
|
+
parent?.getAttribute?.('data-tooltip') ?? parent?.getAttribute?.('data-tooltip-content') ?? '';
|
|
1564
|
+
const parentTestId = parent?.getAttribute?.('data-testid') ?? '';
|
|
1565
|
+
const candidates = [
|
|
1566
|
+
text,
|
|
1567
|
+
aria,
|
|
1568
|
+
title,
|
|
1569
|
+
tooltip,
|
|
1570
|
+
dataTestId,
|
|
1571
|
+
parentText,
|
|
1572
|
+
parentAria,
|
|
1573
|
+
parentTitle,
|
|
1574
|
+
parentTooltip,
|
|
1575
|
+
parentTestId,
|
|
1576
|
+
];
|
|
1577
|
+
let hasFileHint = false;
|
|
1578
|
+
for (const raw of candidates) {
|
|
1579
|
+
if (!raw) continue;
|
|
1580
|
+
if (String(raw).toLowerCase().includes('file')) {
|
|
1581
|
+
hasFileHint = true;
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (!hasFileHint) continue;
|
|
1586
|
+
for (const raw of candidates) {
|
|
1587
|
+
if (!raw) continue;
|
|
1588
|
+
const match = String(raw).toLowerCase().match(countRegex);
|
|
1589
|
+
if (match) {
|
|
1590
|
+
const count = Number(match[1]);
|
|
1591
|
+
if (Number.isFinite(count)) {
|
|
1592
|
+
fileCount = Math.max(fileCount, count);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
if (fileCount > 0) {
|
|
1598
|
+
return { found: true, source: 'file-count' };
|
|
1599
|
+
}
|
|
1600
|
+
|
|
504
1601
|
return { found: false };
|
|
505
1602
|
})()`;
|
|
506
1603
|
while (Date.now() < deadline) {
|
|
@@ -547,6 +1644,7 @@ async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
|
547
1644
|
];
|
|
548
1645
|
for (const selector of selectors) {
|
|
549
1646
|
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
1647
|
+
if (node?.tagName === 'INPUT' && node?.type === 'file') continue;
|
|
550
1648
|
const text = node?.textContent || '';
|
|
551
1649
|
const aria = node?.getAttribute?.('aria-label') || '';
|
|
552
1650
|
const title = node?.getAttribute?.('title') || '';
|
|
@@ -561,6 +1659,76 @@ async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
|
561
1659
|
if (cards.some(matchesExpected)) {
|
|
562
1660
|
return { found: true, text: cards.find(matchesExpected) };
|
|
563
1661
|
}
|
|
1662
|
+
const countRegex = /(?:^|\\b)(\\d+)\\s+files?\\b/;
|
|
1663
|
+
const fileCountNodes = (() => {
|
|
1664
|
+
const nodes = [];
|
|
1665
|
+
const seen = new Set();
|
|
1666
|
+
const add = (node) => {
|
|
1667
|
+
if (!node || seen.has(node)) return;
|
|
1668
|
+
seen.add(node);
|
|
1669
|
+
nodes.push(node);
|
|
1670
|
+
};
|
|
1671
|
+
const root =
|
|
1672
|
+
document.querySelector('[data-testid*="composer"]') || document.querySelector('form') || document.body;
|
|
1673
|
+
const localNodes = root ? Array.from(root.querySelectorAll('button,span,div,[aria-label],[title]')) : [];
|
|
1674
|
+
for (const node of localNodes) add(node);
|
|
1675
|
+
for (const node of Array.from(document.querySelectorAll('button,span,div,[aria-label],[title]'))) {
|
|
1676
|
+
add(node);
|
|
1677
|
+
}
|
|
1678
|
+
return nodes;
|
|
1679
|
+
})();
|
|
1680
|
+
let fileCount = 0;
|
|
1681
|
+
for (const node of fileCountNodes) {
|
|
1682
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
1683
|
+
if (node.matches('textarea,input,[contenteditable="true"]')) continue;
|
|
1684
|
+
const dataTestId = node.getAttribute?.('data-testid') ?? '';
|
|
1685
|
+
const aria = node.getAttribute?.('aria-label') ?? '';
|
|
1686
|
+
const title = node.getAttribute?.('title') ?? '';
|
|
1687
|
+
const tooltip =
|
|
1688
|
+
node.getAttribute?.('data-tooltip') ?? node.getAttribute?.('data-tooltip-content') ?? '';
|
|
1689
|
+
const text = node.textContent ?? '';
|
|
1690
|
+
const parent = node.parentElement;
|
|
1691
|
+
const parentText = parent?.textContent ?? '';
|
|
1692
|
+
const parentAria = parent?.getAttribute?.('aria-label') ?? '';
|
|
1693
|
+
const parentTitle = parent?.getAttribute?.('title') ?? '';
|
|
1694
|
+
const parentTooltip =
|
|
1695
|
+
parent?.getAttribute?.('data-tooltip') ?? parent?.getAttribute?.('data-tooltip-content') ?? '';
|
|
1696
|
+
const parentTestId = parent?.getAttribute?.('data-testid') ?? '';
|
|
1697
|
+
const candidates = [
|
|
1698
|
+
text,
|
|
1699
|
+
aria,
|
|
1700
|
+
title,
|
|
1701
|
+
tooltip,
|
|
1702
|
+
dataTestId,
|
|
1703
|
+
parentText,
|
|
1704
|
+
parentAria,
|
|
1705
|
+
parentTitle,
|
|
1706
|
+
parentTooltip,
|
|
1707
|
+
parentTestId,
|
|
1708
|
+
];
|
|
1709
|
+
let hasFileHint = false;
|
|
1710
|
+
for (const raw of candidates) {
|
|
1711
|
+
if (!raw) continue;
|
|
1712
|
+
if (String(raw).toLowerCase().includes('file')) {
|
|
1713
|
+
hasFileHint = true;
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (!hasFileHint) continue;
|
|
1718
|
+
for (const raw of candidates) {
|
|
1719
|
+
if (!raw) continue;
|
|
1720
|
+
const match = String(raw).toLowerCase().match(countRegex);
|
|
1721
|
+
if (match) {
|
|
1722
|
+
const count = Number(match[1]);
|
|
1723
|
+
if (Number.isFinite(count)) {
|
|
1724
|
+
fileCount = Math.max(fileCount, count);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (fileCount > 0) {
|
|
1730
|
+
return { found: true, text: 'file-count' };
|
|
1731
|
+
}
|
|
564
1732
|
return { found: false };
|
|
565
1733
|
})()`;
|
|
566
1734
|
while (Date.now() < deadline) {
|