@steipete/oracle 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -61
- package/dist/bin/oracle-cli.js +77 -68
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +400 -0
- package/dist/docs-site/browser-mode.html +593 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +452 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +387 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +388 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +323 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/src/browser/actions/assistantResponse.js +91 -9
- package/dist/src/browser/actions/deepResearch.js +180 -52
- package/dist/src/browser/actions/modelSelection.js +111 -24
- package/dist/src/browser/actions/navigation.js +358 -67
- package/dist/src/browser/actions/promptComposer.js +241 -46
- package/dist/src/browser/actions/thinkingTime.js +308 -89
- package/dist/src/browser/artifacts.js +19 -0
- package/dist/src/browser/chatgptFiles.js +797 -0
- package/dist/src/browser/chatgptImages.js +130 -3
- package/dist/src/browser/chromeLifecycle.js +4 -0
- package/dist/src/browser/config.js +4 -0
- package/dist/src/browser/index.js +353 -41
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/policies.js +2 -6
- package/dist/src/browser/projectSourcesRunner.js +14 -2
- package/dist/src/browser/prompt.js +11 -15
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +6 -2
- package/dist/src/browser/reattachability.js +22 -10
- package/dist/src/browser/recoverConversation.js +73 -0
- package/dist/src/browser/sessionRunner.js +4 -1
- package/dist/src/cli/bridge/doctor.js +7 -2
- package/dist/src/cli/browserConfig.js +13 -2
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +129 -54
- package/dist/src/cli/engine.js +6 -2
- package/dist/src/cli/followup.js +72 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +20 -0
- package/dist/src/cli/reattachGuidance.js +8 -0
- package/dist/src/cli/runOptions.js +10 -31
- package/dist/src/cli/sessionCommand.js +4 -0
- package/dist/src/cli/sessionDisplay.js +17 -3
- package/dist/src/cli/sessionLineage.js +7 -4
- package/dist/src/cli/sessionRunner.js +16 -1
- package/dist/src/config.js +164 -9
- package/dist/src/gemini-web/client.js +5 -10
- package/dist/src/gemini-web/executor.js +1 -24
- package/dist/src/gemini-web/models.js +83 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/chatgptImage.js +127 -0
- package/dist/src/mcp/tools/consult.js +261 -167
- package/dist/src/mcp/types.js +2 -0
- package/dist/src/mcp/utils.js +87 -1
- package/dist/src/oracle/config.js +27 -1
- package/dist/src/oracle/files.js +4 -6
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/markdown.js +36 -3
- package/dist/src/oracle/modelResolver.js +21 -13
- package/dist/src/oracle/promptAssembly.js +2 -4
- package/dist/src/oracle/request.js +3 -5
- package/dist/src/oracle/tokenStats.js +5 -1
- package/dist/src/oracle.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +20 -29
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -166,7 +166,7 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
166
166
|
observedLength,
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
|
-
const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames);
|
|
169
|
+
const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames, deps?.attachmentTimeoutMs);
|
|
170
170
|
if (!clicked) {
|
|
171
171
|
await input.dispatchKeyEvent({
|
|
172
172
|
type: "keyDown",
|
|
@@ -283,18 +283,171 @@ async function waitForDomReady(Runtime, logger, timeoutMs = 10_000) {
|
|
|
283
283
|
logger?.(`Page did not reach ready/composer state within ${timeoutMs}ms; continuing cautiously.`);
|
|
284
284
|
}
|
|
285
285
|
function buildAttachmentReadyExpression(attachmentNames) {
|
|
286
|
-
const
|
|
286
|
+
const attachmentExpectations = attachmentNames.map((attachment) => {
|
|
287
|
+
const name = typeof attachment === "string" ? attachment : attachment.name;
|
|
288
|
+
const normalized = name.toLowerCase().replace(/\s+/g, " ").trim();
|
|
289
|
+
return {
|
|
290
|
+
name: normalized,
|
|
291
|
+
stem: normalized.replace(/\.[a-z0-9]{1,10}$/i, ""),
|
|
292
|
+
extension: normalized.match(/(\.[a-z0-9]{1,10})$/i)?.[1] ?? "",
|
|
293
|
+
generatedBundle: typeof attachment === "object" && attachment.generatedBundle === true,
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
const namesLiteral = JSON.stringify(attachmentExpectations);
|
|
287
297
|
return `(() => {
|
|
288
|
-
const
|
|
298
|
+
const expected = ${namesLiteral};
|
|
299
|
+
const sendSelectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
300
|
+
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
301
|
+
const hasNameBoundary = (text, name) => {
|
|
302
|
+
if (!name) return false;
|
|
303
|
+
let from = 0;
|
|
304
|
+
while (from < text.length) {
|
|
305
|
+
const index = text.indexOf(name, from);
|
|
306
|
+
if (index === -1) return false;
|
|
307
|
+
const previous = text[index - 1] || '';
|
|
308
|
+
const next = text[index + name.length] || '';
|
|
309
|
+
const previousOk = !previous || !/[a-z0-9._-]/.test(previous);
|
|
310
|
+
const nextOk = !next || !/[a-z0-9._-]/.test(next);
|
|
311
|
+
if (previousOk && nextOk) return true;
|
|
312
|
+
from = index + name.length;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
};
|
|
316
|
+
const hasStemFileBoundary = (text, stem) => {
|
|
317
|
+
if (!stem) return false;
|
|
318
|
+
let from = 0;
|
|
319
|
+
while (from < text.length) {
|
|
320
|
+
const index = text.indexOf(stem, from);
|
|
321
|
+
if (index === -1) return false;
|
|
322
|
+
const previous = text[index - 1] || '';
|
|
323
|
+
const next = text[index + stem.length] || '';
|
|
324
|
+
const previousOk = !previous || !/[a-z0-9._-]/.test(previous);
|
|
325
|
+
const nextOk = !next || !/[a-z0-9._-]/.test(next);
|
|
326
|
+
if (previousOk && nextOk) return true;
|
|
327
|
+
from = index + stem.length;
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
};
|
|
331
|
+
const hasBareStemBoundary = (text, stem) => {
|
|
332
|
+
if (!stem) return false;
|
|
333
|
+
let from = 0;
|
|
334
|
+
while (from < text.length) {
|
|
335
|
+
const index = text.indexOf(stem, from);
|
|
336
|
+
if (index === -1) return false;
|
|
337
|
+
const previous = text[index - 1] || '';
|
|
338
|
+
const next = text[index + stem.length] || '';
|
|
339
|
+
const previousOk = !previous || !/[a-z0-9._-]/.test(previous);
|
|
340
|
+
const nextOk = !next || !/[a-z0-9._(-]/.test(next);
|
|
341
|
+
if (previousOk && nextOk) return true;
|
|
342
|
+
from = index + stem.length;
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
};
|
|
346
|
+
const hasExtensionBoundary = (text, extension) => {
|
|
347
|
+
if (!extension) return false;
|
|
348
|
+
let from = 0;
|
|
349
|
+
while (from < text.length) {
|
|
350
|
+
const index = text.indexOf(extension, from);
|
|
351
|
+
if (index === -1) return false;
|
|
352
|
+
const next = text[index + extension.length] || '';
|
|
353
|
+
if (!next || !/[a-z0-9]/.test(next)) return true;
|
|
354
|
+
from = index + extension.length;
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
};
|
|
358
|
+
const matchesExpected = (value, item) => {
|
|
359
|
+
const text = normalize(value);
|
|
360
|
+
if (!text) return false;
|
|
361
|
+
if (hasNameBoundary(text, item.name)) return true;
|
|
362
|
+
if (item.generatedBundle && hasBareStemBoundary(text, item.stem)) return true;
|
|
363
|
+
if (
|
|
364
|
+
item.stem &&
|
|
365
|
+
item.stem.length >= 4 &&
|
|
366
|
+
item.extension &&
|
|
367
|
+
text.includes(item.stem + '(') &&
|
|
368
|
+
hasExtensionBoundary(text, item.extension)
|
|
369
|
+
) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
if (text.includes('…') || text.includes('...')) {
|
|
373
|
+
const marker = text.includes('…') ? '…' : '...';
|
|
374
|
+
const [prefixRaw, suffixRaw] = text.split(marker);
|
|
375
|
+
const prefix = normalize(prefixRaw);
|
|
376
|
+
const suffix = normalize(suffixRaw);
|
|
377
|
+
const prefixParts = prefix.split(' ').filter(Boolean);
|
|
378
|
+
const suffixParts = suffix.split(' ').filter(Boolean);
|
|
379
|
+
const prefixCandidates = prefixParts.map((_, index) => prefixParts.slice(index).join(' '));
|
|
380
|
+
const suffixCandidates = suffixParts.map((_, index) =>
|
|
381
|
+
suffixParts.slice(0, suffixParts.length - index).join(' '),
|
|
382
|
+
);
|
|
383
|
+
if (prefixCandidates.length === 0 || suffixCandidates.length === 0) return false;
|
|
384
|
+
const targets = [item.name, item.stem && item.stem.length >= 4 ? item.stem : ''].filter(Boolean);
|
|
385
|
+
return targets.some((target) => {
|
|
386
|
+
return prefixCandidates.some((prefixPart) =>
|
|
387
|
+
suffixCandidates.some((suffixPart) => {
|
|
388
|
+
const strongEnough =
|
|
389
|
+
suffixPart.length >= 2 &&
|
|
390
|
+
(prefixPart.length >= 3 || (prefixPart.length >= 2 && suffixPart.length >= 4));
|
|
391
|
+
return strongEnough && target.startsWith(prefixPart) && target.endsWith(suffixPart);
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return false;
|
|
397
|
+
};
|
|
398
|
+
// Restrict to attachment affordances; never scan generic div/span nodes (prompt text can contain the file name).
|
|
399
|
+
const attachmentSelectors = [
|
|
400
|
+
// Current ChatGPT file tiles expose the filename through a role-group aria label.
|
|
401
|
+
'[role="group"][aria-label]',
|
|
402
|
+
'[data-testid*="chip"]',
|
|
403
|
+
'[data-testid*="attachment"]',
|
|
404
|
+
'[data-testid*="upload"]',
|
|
405
|
+
'[data-testid*="file"]',
|
|
406
|
+
'[aria-label*="Remove file"]',
|
|
407
|
+
'button[aria-label*="Remove file"]',
|
|
408
|
+
'[aria-label*="remove file"]',
|
|
409
|
+
'button[aria-label*="remove file"]',
|
|
410
|
+
'[aria-label*="Remove attachment"]',
|
|
411
|
+
'button[aria-label*="Remove attachment"]',
|
|
412
|
+
'[aria-label*="remove attachment"]',
|
|
413
|
+
'button[aria-label*="remove attachment"]',
|
|
414
|
+
];
|
|
415
|
+
const sendButton = sendSelectors
|
|
416
|
+
.map((selector) => document.querySelector(selector))
|
|
417
|
+
.find(Boolean);
|
|
418
|
+
const isUsableComposerRoot = (node) => {
|
|
419
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
420
|
+
if (String(node.tagName || '').toLowerCase() === 'button') return false;
|
|
421
|
+
const testId = String(node.getAttribute?.('data-testid') || '').toLowerCase();
|
|
422
|
+
if (!testId.includes('composer')) return false;
|
|
423
|
+
return !(
|
|
424
|
+
testId.includes('footer') ||
|
|
425
|
+
testId.includes('action') ||
|
|
426
|
+
testId.includes('plus') ||
|
|
427
|
+
testId.includes('send')
|
|
428
|
+
);
|
|
429
|
+
};
|
|
430
|
+
const closestComposerRoot = (node) => {
|
|
431
|
+
let current = node instanceof HTMLElement ? node : null;
|
|
432
|
+
while (current) {
|
|
433
|
+
if (isUsableComposerRoot(current)) return current;
|
|
434
|
+
current = current.parentElement;
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
};
|
|
438
|
+
const firstComposerRoot = () =>
|
|
439
|
+
Array.from(document.querySelectorAll('[data-testid*="composer"]')).find(isUsableComposerRoot) || null;
|
|
289
440
|
const composer =
|
|
290
|
-
|
|
441
|
+
closestComposerRoot(sendButton) ||
|
|
442
|
+
sendButton?.closest?.('form') ||
|
|
443
|
+
firstComposerRoot() ||
|
|
291
444
|
document.querySelector('form') ||
|
|
292
445
|
document.body ||
|
|
293
446
|
document;
|
|
294
447
|
// Walk node + ancestors (up to grandparent) + descendants to gather every textual hint.
|
|
295
448
|
// ChatGPT's current chip DOM nests the filename inside truncated child spans, so checking
|
|
296
449
|
// only the node's own textContent/aria/title misses the match.
|
|
297
|
-
const
|
|
450
|
+
const collectOwnLabelHaystack = (node) => {
|
|
298
451
|
if (!node) return '';
|
|
299
452
|
const pieces = [];
|
|
300
453
|
const pushAttrs = (el) => {
|
|
@@ -311,31 +464,21 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
311
464
|
};
|
|
312
465
|
pushAttrs(node);
|
|
313
466
|
pushText(node);
|
|
467
|
+
return pieces.join(' ').toLowerCase();
|
|
468
|
+
};
|
|
469
|
+
const collectLabelHaystack = (node) => {
|
|
470
|
+
if (!node) return '';
|
|
471
|
+
const pieces = [collectOwnLabelHaystack(node)];
|
|
472
|
+
const push = (el) => {
|
|
473
|
+
const text = collectOwnLabelHaystack(el);
|
|
474
|
+
if (text) pieces.push(text);
|
|
475
|
+
};
|
|
314
476
|
const parent = node.parentElement;
|
|
315
|
-
|
|
316
|
-
pushText(parent);
|
|
477
|
+
push(parent);
|
|
317
478
|
const grandparent = parent?.parentElement;
|
|
318
|
-
|
|
319
|
-
pushText(grandparent);
|
|
479
|
+
push(grandparent);
|
|
320
480
|
return pieces.join(' ').toLowerCase();
|
|
321
481
|
};
|
|
322
|
-
const match = (node, name) => collectLabelHaystack(node).includes(name);
|
|
323
|
-
|
|
324
|
-
// Restrict to attachment affordances; never scan generic div/span nodes (prompt text can contain the file name).
|
|
325
|
-
const attachmentSelectors = [
|
|
326
|
-
'[data-testid*="chip"]',
|
|
327
|
-
'[data-testid*="attachment"]',
|
|
328
|
-
'[data-testid*="upload"]',
|
|
329
|
-
'[data-testid*="file"]',
|
|
330
|
-
'[aria-label*="Remove file"]',
|
|
331
|
-
'button[aria-label*="Remove file"]',
|
|
332
|
-
'[aria-label*="remove file"]',
|
|
333
|
-
'button[aria-label*="remove file"]',
|
|
334
|
-
'[aria-label*="Remove attachment"]',
|
|
335
|
-
'button[aria-label*="Remove attachment"]',
|
|
336
|
-
'[aria-label*="remove attachment"]',
|
|
337
|
-
'button[aria-label*="remove attachment"]',
|
|
338
|
-
];
|
|
339
482
|
const attachmentRoots = Array.from(new Set([composer])).filter(Boolean);
|
|
340
483
|
const collectChipNodes = () => {
|
|
341
484
|
const seen = new Set();
|
|
@@ -354,32 +497,77 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
354
497
|
return collected;
|
|
355
498
|
};
|
|
356
499
|
const chipNodes = collectChipNodes();
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
500
|
+
const chipLabels = chipNodes.map((node) => collectLabelHaystack(node));
|
|
501
|
+
const chipOwnLabels = chipNodes.map((node) => collectOwnLabelHaystack(node));
|
|
502
|
+
const hasEllipsisSuffix = (label) => {
|
|
503
|
+
const marker = label.includes('…') ? '…' : label.includes('...') ? '...' : '';
|
|
504
|
+
if (!marker) return false;
|
|
505
|
+
return normalize(label.split(marker)[1] || '').length > 0;
|
|
506
|
+
};
|
|
507
|
+
const chipOwnLabelsWithVisibleNames = chipOwnLabels.filter((label) =>
|
|
508
|
+
/\\.[a-z][a-z0-9]{0,9}(?:\\b|$)/i.test(label) ||
|
|
509
|
+
hasEllipsisSuffix(label),
|
|
510
|
+
);
|
|
511
|
+
const visibleExtensionLabelsMatchExpected = chipOwnLabelsWithVisibleNames.every((label) =>
|
|
512
|
+
expected.some((item) => matchesExpected(label, item)),
|
|
513
|
+
);
|
|
514
|
+
const visibleStemOnlyMismatch = chipOwnLabels.some((label) =>
|
|
515
|
+
expected.some(
|
|
516
|
+
(item) =>
|
|
517
|
+
!item.generatedBundle &&
|
|
518
|
+
item.stem &&
|
|
519
|
+
hasStemFileBoundary(label, item.stem) &&
|
|
520
|
+
!matchesExpected(label, item),
|
|
521
|
+
),
|
|
360
522
|
);
|
|
361
|
-
|
|
523
|
+
|
|
524
|
+
const chipsReady = (() => {
|
|
525
|
+
const used = new Set();
|
|
526
|
+
return expected.every((item) => {
|
|
527
|
+
const index = chipLabels.findIndex((label, candidateIndex) =>
|
|
528
|
+
!used.has(candidateIndex) && matchesExpected(label, item),
|
|
529
|
+
);
|
|
530
|
+
if (index === -1) return false;
|
|
531
|
+
used.add(index);
|
|
532
|
+
return true;
|
|
533
|
+
});
|
|
534
|
+
})();
|
|
535
|
+
const inputsReady = expected.every((item) =>
|
|
362
536
|
attachmentRoots.some((root) =>
|
|
363
537
|
Array.from(root.querySelectorAll('input[type="file"]')).some((el) =>
|
|
364
538
|
Array.from((el instanceof HTMLInputElement ? el.files : []) || []).some((file) =>
|
|
365
|
-
file?.name
|
|
539
|
+
matchesExpected(file?.name, item),
|
|
366
540
|
),
|
|
367
541
|
),
|
|
368
542
|
),
|
|
369
543
|
);
|
|
370
544
|
// Count-based fallback: if we cannot match names individually (ChatGPT may strip
|
|
371
545
|
// the filename out of attribute-readable text into a deeply nested span), but we
|
|
372
|
-
// do see at least as many distinct
|
|
373
|
-
//
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
546
|
+
// do see at least as many distinct "Remove" affordances as attachments we
|
|
547
|
+
// uploaded, trust the upload without double-counting nested chip/remove nodes.
|
|
548
|
+
const removeAffordances = [];
|
|
549
|
+
const removeSeen = new Set();
|
|
550
|
+
for (const root of attachmentRoots) {
|
|
551
|
+
for (const node of Array.from(root.querySelectorAll(
|
|
378
552
|
'[aria-label*="Remove" i], [aria-label*="remove" i], button[aria-label*="Remove" i], button[aria-label*="remove" i]',
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
553
|
+
))) {
|
|
554
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
555
|
+
if (node.closest('textarea,[contenteditable="true"]')) continue;
|
|
556
|
+
const aria = (node.getAttribute?.('aria-label') ?? '').toLowerCase();
|
|
557
|
+
const fileSpecific = aria.includes('remove file') || aria.includes('remove attachment');
|
|
558
|
+
const attachmentOwner = node.closest(
|
|
559
|
+
'[data-testid*="chip"], [data-testid*="attachment"], [data-testid*="upload"], [data-testid*="file"]',
|
|
560
|
+
);
|
|
561
|
+
if (!fileSpecific && !attachmentOwner) continue;
|
|
562
|
+
if (removeSeen.has(node)) continue;
|
|
563
|
+
removeSeen.add(node);
|
|
564
|
+
removeAffordances.push(node);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const countReady =
|
|
568
|
+
!visibleStemOnlyMismatch &&
|
|
569
|
+
visibleExtensionLabelsMatchExpected &&
|
|
570
|
+
removeAffordances.length >= expected.length;
|
|
383
571
|
|
|
384
572
|
return chipsReady || inputsReady || countReady;
|
|
385
573
|
})()`;
|
|
@@ -387,7 +575,7 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
387
575
|
export function buildAttachmentReadyExpressionForTest(attachmentNames) {
|
|
388
576
|
return buildAttachmentReadyExpression(attachmentNames);
|
|
389
577
|
}
|
|
390
|
-
async function attemptSendButton(Runtime, _logger, attachmentNames) {
|
|
578
|
+
async function attemptSendButton(Runtime, _logger, attachmentNames, attachmentTimeoutMs) {
|
|
391
579
|
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
392
580
|
const script = `(() => {
|
|
393
581
|
${buildClickDispatcher()}
|
|
@@ -424,7 +612,8 @@ async function attemptSendButton(Runtime, _logger, attachmentNames) {
|
|
|
424
612
|
// Give attachment-bearing submissions more headroom. ChatGPT's chip render can
|
|
425
613
|
// settle slowly for multi-file uploads, but plain text sends should keep the
|
|
426
614
|
// shorter historical deadline.
|
|
427
|
-
const
|
|
615
|
+
const timeoutMs = sendButtonTimeoutMs(attachmentNames, attachmentTimeoutMs);
|
|
616
|
+
const deadline = Date.now() + timeoutMs;
|
|
428
617
|
while (Date.now() < deadline) {
|
|
429
618
|
if (needAttachment) {
|
|
430
619
|
const ready = await Runtime.evaluate({
|
|
@@ -446,16 +635,22 @@ async function attemptSendButton(Runtime, _logger, attachmentNames) {
|
|
|
446
635
|
await delay(100);
|
|
447
636
|
}
|
|
448
637
|
if (Array.isArray(attachmentNames) && attachmentNames.length > 0) {
|
|
449
|
-
throw new BrowserAutomationError(
|
|
638
|
+
throw new BrowserAutomationError(`Attachments never reached a clickable send button after ${Math.ceil(timeoutMs / 1000)}s; tune --browser-attachment-timeout.`, {
|
|
450
639
|
stage: "submit-prompt",
|
|
451
640
|
code: "attachment-send-not-ready",
|
|
452
641
|
attachmentNames,
|
|
642
|
+
timeoutMs,
|
|
453
643
|
});
|
|
454
644
|
}
|
|
455
645
|
return false;
|
|
456
646
|
}
|
|
457
|
-
function sendButtonTimeoutMs(attachmentNames) {
|
|
458
|
-
|
|
647
|
+
function sendButtonTimeoutMs(attachmentNames, attachmentTimeoutMs) {
|
|
648
|
+
if (!Array.isArray(attachmentNames) || attachmentNames.length === 0) {
|
|
649
|
+
return 20_000;
|
|
650
|
+
}
|
|
651
|
+
return typeof attachmentTimeoutMs === "number" && Number.isFinite(attachmentTimeoutMs)
|
|
652
|
+
? Math.max(1_000, attachmentTimeoutMs)
|
|
653
|
+
: 45_000;
|
|
459
654
|
}
|
|
460
655
|
async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselineTurns) {
|
|
461
656
|
const deadline = Date.now() + timeoutMs;
|