@steipete/oracle 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +61 -48
- package/dist/bin/oracle-cli.js +455 -402
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/assistantResponse.js +149 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +275 -117
- package/dist/src/browser/actions/navigation.js +161 -137
- package/dist/src/browser/actions/promptComposer.js +100 -64
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/chromeLifecycle.js +62 -60
- package/dist/src/browser/config.js +34 -15
- package/dist/src/browser/constants.js +17 -12
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +62 -62
- package/dist/src/browser/domDebug.js +1 -1
- package/dist/src/browser/index.js +390 -295
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +44 -39
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +67 -34
- package/dist/src/browser/reattachHelpers.js +31 -26
- package/dist/src/browser/sessionRunner.js +37 -25
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +16 -16
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +62 -48
- package/dist/src/cli/browserDefaults.js +27 -26
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +29 -25
- package/dist/src/cli/duplicatePromptGuard.js +3 -3
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +127 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +31 -21
- package/dist/src/cli/sessionDisplay.js +95 -81
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +103 -93
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/server.js +16 -12
- package/dist/src/mcp/tools/consult.js +51 -47
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +5 -5
- package/dist/src/mcp/utils.js +15 -7
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +156 -134
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +77 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +66 -62
- package/vendor/oracle-notifier/README.md +2 -0
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { CONVERSATION_TURN_SELECTOR, INPUT_SELECTORS, SEND_BUTTON_SELECTORS, UPLOAD_STATUS_SELECTORS } from
|
|
3
|
-
import { delay } from
|
|
4
|
-
import { logDomFailure } from
|
|
5
|
-
import { transferAttachmentViaDataTransfer } from
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { CONVERSATION_TURN_SELECTOR, INPUT_SELECTORS, SEND_BUTTON_SELECTORS, UPLOAD_STATUS_SELECTORS, } from "../constants.js";
|
|
3
|
+
import { delay } from "../utils.js";
|
|
4
|
+
import { logDomFailure } from "../domDebug.js";
|
|
5
|
+
import { transferAttachmentViaDataTransfer } from "./attachmentDataTransfer.js";
|
|
6
6
|
export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
7
7
|
const { runtime, dom, input } = deps;
|
|
8
8
|
if (!dom) {
|
|
9
|
-
throw new Error(
|
|
9
|
+
throw new Error("DOM domain unavailable while uploading attachments.");
|
|
10
10
|
}
|
|
11
|
-
const expectedCount = typeof options?.expectedCount ===
|
|
11
|
+
const expectedCount = typeof options?.expectedCount === "number" && Number.isFinite(options.expectedCount)
|
|
12
12
|
? Math.max(0, Math.floor(options.expectedCount))
|
|
13
13
|
: 0;
|
|
14
14
|
const readAttachmentSignals = async (name) => {
|
|
@@ -266,17 +266,17 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
266
266
|
return {
|
|
267
267
|
ui: Boolean(value?.ui),
|
|
268
268
|
input: Boolean(value?.input),
|
|
269
|
-
inputCount: typeof value?.inputCount ===
|
|
270
|
-
chipCount: typeof value?.chipCount ===
|
|
271
|
-
chipSignature: typeof value?.chipSignature ===
|
|
269
|
+
inputCount: typeof value?.inputCount === "number" ? value?.inputCount : 0,
|
|
270
|
+
chipCount: typeof value?.chipCount === "number" ? value?.chipCount : 0,
|
|
271
|
+
chipSignature: typeof value?.chipSignature === "string" ? value?.chipSignature : "",
|
|
272
272
|
uploading: Boolean(value?.uploading),
|
|
273
|
-
fileCount: typeof value?.fileCount ===
|
|
273
|
+
fileCount: typeof value?.fileCount === "number" ? value?.fileCount : 0,
|
|
274
274
|
};
|
|
275
275
|
};
|
|
276
276
|
// New ChatGPT UI hides the real file input behind a composer "+" menu; click it pre-emptively.
|
|
277
277
|
// Learned: synthetic `.click()` is sometimes ignored (isTrusted checks). Prefer a CDP mouse click when possible.
|
|
278
278
|
const clickPlusTrusted = async () => {
|
|
279
|
-
if (!input || typeof input.dispatchMouseEvent !==
|
|
279
|
+
if (!input || typeof input.dispatchMouseEvent !== "function")
|
|
280
280
|
return false;
|
|
281
281
|
const locate = await runtime
|
|
282
282
|
.evaluate({
|
|
@@ -304,13 +304,13 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
304
304
|
})
|
|
305
305
|
.then((res) => res?.result?.value)
|
|
306
306
|
.catch(() => undefined);
|
|
307
|
-
if (!locate?.ok || typeof locate.x !==
|
|
307
|
+
if (!locate?.ok || typeof locate.x !== "number" || typeof locate.y !== "number")
|
|
308
308
|
return false;
|
|
309
309
|
const x = locate.x;
|
|
310
310
|
const y = locate.y;
|
|
311
|
-
await input.dispatchMouseEvent({ type:
|
|
312
|
-
await input.dispatchMouseEvent({ type:
|
|
313
|
-
await input.dispatchMouseEvent({ type:
|
|
311
|
+
await input.dispatchMouseEvent({ type: "mouseMoved", x, y });
|
|
312
|
+
await input.dispatchMouseEvent({ type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
313
|
+
await input.dispatchMouseEvent({ type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
314
314
|
return true;
|
|
315
315
|
};
|
|
316
316
|
const clickedTrusted = await clickPlusTrusted().catch(() => false);
|
|
@@ -338,13 +338,13 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
338
338
|
})).catch(() => undefined);
|
|
339
339
|
}
|
|
340
340
|
await delay(350);
|
|
341
|
-
const normalizeForMatch = (value) => String(value ||
|
|
341
|
+
const normalizeForMatch = (value) => String(value || "")
|
|
342
342
|
.toLowerCase()
|
|
343
|
-
.replace(/\s+/g,
|
|
343
|
+
.replace(/\s+/g, " ")
|
|
344
344
|
.trim();
|
|
345
345
|
const expectedName = path.basename(attachment.path);
|
|
346
346
|
const expectedNameLower = normalizeForMatch(expectedName);
|
|
347
|
-
const expectedNameNoExt = expectedNameLower.replace(/\.[a-z0-9]{1,10}$/i,
|
|
347
|
+
const expectedNameNoExt = expectedNameLower.replace(/\.[a-z0-9]{1,10}$/i, "");
|
|
348
348
|
const matchesExpectedName = (value) => {
|
|
349
349
|
const normalized = normalizeForMatch(value);
|
|
350
350
|
if (!normalized)
|
|
@@ -367,16 +367,17 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
367
367
|
const isExpectedSatisfied = (signals) => {
|
|
368
368
|
if (expectedCount <= 0)
|
|
369
369
|
return false;
|
|
370
|
-
const fileCount = typeof signals.fileCount ===
|
|
371
|
-
const chipCount = typeof signals.chipCount ===
|
|
370
|
+
const fileCount = typeof signals.fileCount === "number" ? signals.fileCount : 0;
|
|
371
|
+
const chipCount = typeof signals.chipCount === "number" ? signals.chipCount : 0;
|
|
372
372
|
if (fileCount >= expectedCount)
|
|
373
373
|
return true;
|
|
374
374
|
return Boolean(signals.ui && chipCount >= expectedCount);
|
|
375
375
|
};
|
|
376
376
|
const initialInputSatisfied = expectedCount > 0 ? initialSignals.inputCount >= expectedCount : Boolean(initialSignals.input);
|
|
377
|
-
if (expectedCount > 0 &&
|
|
377
|
+
if (expectedCount > 0 &&
|
|
378
|
+
(initialSignals.fileCount >= expectedCount || initialSignals.inputCount >= expectedCount)) {
|
|
378
379
|
const satisfiedCount = Math.max(initialSignals.fileCount, initialSignals.inputCount);
|
|
379
|
-
logger(`Attachment already present: composer shows ${satisfiedCount} file${satisfiedCount === 1 ?
|
|
380
|
+
logger(`Attachment already present: composer shows ${satisfiedCount} file${satisfiedCount === 1 ? "" : "s"}`);
|
|
380
381
|
return true;
|
|
381
382
|
}
|
|
382
383
|
if (initialInputSatisfied || initialSignals.input) {
|
|
@@ -611,33 +612,38 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
611
612
|
});
|
|
612
613
|
const candidateValue = candidateSetup?.result?.value;
|
|
613
614
|
const candidateOrder = Array.isArray(candidateValue?.order) ? candidateValue.order : [];
|
|
614
|
-
const baselineChipCount = typeof candidateValue?.baselineChipCount ===
|
|
615
|
-
const baselineChips = Array.isArray(candidateValue?.baselineChips)
|
|
615
|
+
const baselineChipCount = typeof candidateValue?.baselineChipCount === "number" ? candidateValue.baselineChipCount : 0;
|
|
616
|
+
const baselineChips = Array.isArray(candidateValue?.baselineChips)
|
|
617
|
+
? candidateValue.baselineChips
|
|
618
|
+
: [];
|
|
616
619
|
const baselineUploading = Boolean(candidateValue?.baselineUploading);
|
|
617
|
-
const baselineFileCount = typeof candidateValue?.baselineFileCount ===
|
|
618
|
-
const baselineInputCount = typeof candidateValue?.baselineInputCount ===
|
|
620
|
+
const baselineFileCount = typeof candidateValue?.baselineFileCount === "number" ? candidateValue.baselineFileCount : 0;
|
|
621
|
+
const baselineInputCount = typeof candidateValue?.baselineInputCount === "number" ? candidateValue.baselineInputCount : 0;
|
|
619
622
|
const serializeChips = (chips) => chips
|
|
620
623
|
.map((chip) => [chip.text, chip.aria, chip.title, chip.testid]
|
|
621
|
-
.map((value) => String(value ||
|
|
622
|
-
.
|
|
623
|
-
.
|
|
624
|
+
.map((value) => String(value || "")
|
|
625
|
+
.toLowerCase()
|
|
626
|
+
.replace(/\s+/g, " ")
|
|
627
|
+
.trim())
|
|
628
|
+
.join("|"))
|
|
629
|
+
.join("||");
|
|
624
630
|
const baselineChipSignature = serializeChips(baselineChips);
|
|
625
631
|
if (!candidateValue?.ok || candidateOrder.length === 0) {
|
|
626
|
-
await logDomFailure(runtime, logger,
|
|
627
|
-
throw new Error(
|
|
632
|
+
await logDomFailure(runtime, logger, "file-input-missing");
|
|
633
|
+
throw new Error("Unable to locate ChatGPT file attachment input.");
|
|
628
634
|
}
|
|
629
635
|
const hasChipDelta = (signals) => {
|
|
630
|
-
const chipCount = typeof signals.chipCount ===
|
|
631
|
-
const chipSignature = typeof signals.chipSignature ===
|
|
636
|
+
const chipCount = typeof signals.chipCount === "number" ? signals.chipCount : 0;
|
|
637
|
+
const chipSignature = typeof signals.chipSignature === "string" ? signals.chipSignature : "";
|
|
632
638
|
if (chipCount > baselineChipCount)
|
|
633
639
|
return true;
|
|
634
640
|
if (baselineChipSignature && chipSignature && chipSignature !== baselineChipSignature)
|
|
635
641
|
return true;
|
|
636
642
|
return false;
|
|
637
643
|
};
|
|
638
|
-
const hasInputDelta = (signals) => (typeof signals.inputCount ===
|
|
644
|
+
const hasInputDelta = (signals) => (typeof signals.inputCount === "number" ? signals.inputCount : 0) > baselineInputCount;
|
|
639
645
|
const hasUploadDelta = (signals) => Boolean(signals.uploading && !baselineUploading);
|
|
640
|
-
const hasFileCountDelta = (signals) => (typeof signals.fileCount ===
|
|
646
|
+
const hasFileCountDelta = (signals) => (typeof signals.fileCount === "number" ? signals.fileCount : 0) > baselineFileCount;
|
|
641
647
|
const waitForAttachmentUiSignal = async (timeoutMs) => {
|
|
642
648
|
const deadline = Date.now() + timeoutMs;
|
|
643
649
|
let sawInputSignal = false;
|
|
@@ -652,7 +658,14 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
652
658
|
if (inputDelta) {
|
|
653
659
|
sawInputSignal = true;
|
|
654
660
|
}
|
|
655
|
-
latest = {
|
|
661
|
+
latest = {
|
|
662
|
+
signals,
|
|
663
|
+
chipDelta,
|
|
664
|
+
inputDelta: sawInputSignal,
|
|
665
|
+
uploadDelta,
|
|
666
|
+
fileCountDelta,
|
|
667
|
+
expectedSatisfied,
|
|
668
|
+
};
|
|
656
669
|
if (signals.ui || chipDelta || uploadDelta || fileCountDelta || expectedSatisfied) {
|
|
657
670
|
return latest;
|
|
658
671
|
}
|
|
@@ -673,9 +686,9 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
673
686
|
})()`;
|
|
674
687
|
const parseInputSnapshot = (value) => {
|
|
675
688
|
const snapshot = value;
|
|
676
|
-
const names = Array.isArray(snapshot?.names) ? snapshot?.names ?? [] : [];
|
|
677
|
-
const valueText = typeof snapshot?.value ===
|
|
678
|
-
const count = typeof snapshot?.count ===
|
|
689
|
+
const names = Array.isArray(snapshot?.names) ? (snapshot?.names ?? []) : [];
|
|
690
|
+
const valueText = typeof snapshot?.value === "string" ? snapshot.value : "";
|
|
691
|
+
const count = typeof snapshot?.count === "number" ? snapshot.count : names.length;
|
|
679
692
|
return {
|
|
680
693
|
names,
|
|
681
694
|
value: valueText,
|
|
@@ -804,7 +817,7 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
804
817
|
})()`;
|
|
805
818
|
let confirmedAttachment = false;
|
|
806
819
|
let lastInputNames = [];
|
|
807
|
-
let lastInputValue =
|
|
820
|
+
let lastInputValue = "";
|
|
808
821
|
let finalSnapshot = null;
|
|
809
822
|
const resolveInputNameCandidates = () => {
|
|
810
823
|
const snapshot = finalSnapshot;
|
|
@@ -851,7 +864,7 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
851
864
|
chipCount: Number(snapshot.chipCount ?? 0),
|
|
852
865
|
chips: Array.isArray(snapshot.chips) ? snapshot.chips : [],
|
|
853
866
|
inputNames: Array.isArray(snapshot.inputNames) ? snapshot.inputNames : [],
|
|
854
|
-
composerText: typeof snapshot.composerText ===
|
|
867
|
+
composerText: typeof snapshot.composerText === "string" ? snapshot.composerText : "",
|
|
855
868
|
uploading: Boolean(snapshot.uploading),
|
|
856
869
|
};
|
|
857
870
|
}
|
|
@@ -871,15 +884,20 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
871
884
|
Boolean(signalResult?.inputDelta) ||
|
|
872
885
|
inputHasFile;
|
|
873
886
|
const uiDirect = Boolean(signalResult?.signals?.ui) || expectedSatisfied;
|
|
874
|
-
const uiDelta = Boolean(signalResult?.chipDelta) ||
|
|
887
|
+
const uiDelta = Boolean(signalResult?.chipDelta) ||
|
|
888
|
+
Boolean(signalResult?.uploadDelta) ||
|
|
889
|
+
Boolean(signalResult?.fileCountDelta);
|
|
875
890
|
if (uiDirect || (uiDelta && inputEvidence)) {
|
|
876
|
-
return { status:
|
|
891
|
+
return { status: "ui" };
|
|
877
892
|
}
|
|
878
893
|
const postSignals = await readAttachmentSignals(expectedName);
|
|
879
894
|
if (postSignals.ui ||
|
|
880
895
|
isExpectedSatisfied(postSignals) ||
|
|
881
|
-
((hasChipDelta(postSignals) ||
|
|
882
|
-
|
|
896
|
+
((hasChipDelta(postSignals) ||
|
|
897
|
+
hasUploadDelta(postSignals) ||
|
|
898
|
+
hasFileCountDelta(postSignals)) &&
|
|
899
|
+
inputEvidence)) {
|
|
900
|
+
return { status: "ui" };
|
|
883
901
|
}
|
|
884
902
|
const inputSignal = immediateInputMatch ||
|
|
885
903
|
postInputSignals.touched ||
|
|
@@ -889,15 +907,15 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
889
907
|
postSignals.input ||
|
|
890
908
|
hasInputDelta(postSignals);
|
|
891
909
|
if (inputSignal) {
|
|
892
|
-
return { status:
|
|
910
|
+
return { status: "input" };
|
|
893
911
|
}
|
|
894
|
-
return { status:
|
|
912
|
+
return { status: "none" };
|
|
895
913
|
};
|
|
896
914
|
const runInputAttempt = async (mode) => {
|
|
897
915
|
let immediateInputSnapshot = await readInputSnapshot(idx);
|
|
898
916
|
let hasExpectedFile = snapshotMatchesExpected(immediateInputSnapshot);
|
|
899
917
|
if (!hasExpectedFile) {
|
|
900
|
-
if (mode ===
|
|
918
|
+
if (mode === "set") {
|
|
901
919
|
await dom.setFileInputFiles({ nodeId: resultNode.nodeId, files: [attachment.path] });
|
|
902
920
|
}
|
|
903
921
|
else {
|
|
@@ -941,35 +959,37 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
941
959
|
})
|
|
942
960
|
.catch(() => undefined);
|
|
943
961
|
};
|
|
944
|
-
let result = await runInputAttempt(
|
|
945
|
-
if (result.evaluation.status ===
|
|
962
|
+
let result = await runInputAttempt("set");
|
|
963
|
+
if (result.evaluation.status === "ui") {
|
|
946
964
|
confirmedAttachment = true;
|
|
947
965
|
break;
|
|
948
966
|
}
|
|
949
|
-
if (result.evaluation.status ===
|
|
967
|
+
if (result.evaluation.status === "input") {
|
|
950
968
|
await dispatchInputEvents();
|
|
951
969
|
await delay(150);
|
|
952
970
|
const forcedState = await gatherSignals(1_500);
|
|
953
971
|
const forcedEvaluation = await evaluateSignals(forcedState.signalResult, forcedState.postInputSignals, result.immediateInputMatch);
|
|
954
|
-
if (forcedEvaluation.status ===
|
|
972
|
+
if (forcedEvaluation.status === "ui") {
|
|
955
973
|
confirmedAttachment = true;
|
|
956
974
|
break;
|
|
957
975
|
}
|
|
958
|
-
if (forcedEvaluation.status ===
|
|
959
|
-
logger(
|
|
976
|
+
if (forcedEvaluation.status === "input") {
|
|
977
|
+
logger("Attachment input set; proceeding without UI confirmation.");
|
|
960
978
|
inputConfirmed = true;
|
|
961
979
|
break;
|
|
962
980
|
}
|
|
963
|
-
logger(
|
|
964
|
-
await dom
|
|
981
|
+
logger("Attachment input set; retrying with data transfer to trigger ChatGPT upload.");
|
|
982
|
+
await dom
|
|
983
|
+
.setFileInputFiles({ nodeId: resultNode.nodeId, files: [] })
|
|
984
|
+
.catch(() => undefined);
|
|
965
985
|
await delay(150);
|
|
966
|
-
result = await runInputAttempt(
|
|
967
|
-
if (result.evaluation.status ===
|
|
986
|
+
result = await runInputAttempt("transfer");
|
|
987
|
+
if (result.evaluation.status === "ui") {
|
|
968
988
|
confirmedAttachment = true;
|
|
969
989
|
break;
|
|
970
990
|
}
|
|
971
|
-
if (result.evaluation.status ===
|
|
972
|
-
logger(
|
|
991
|
+
if (result.evaluation.status === "input") {
|
|
992
|
+
logger("Attachment input set; proceeding without UI confirmation.");
|
|
973
993
|
inputConfirmed = true;
|
|
974
994
|
break;
|
|
975
995
|
}
|
|
@@ -984,23 +1004,25 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
984
1004
|
break;
|
|
985
1005
|
}
|
|
986
1006
|
if (lateSignals.input || hasInputDelta(lateSignals)) {
|
|
987
|
-
logger(
|
|
1007
|
+
logger("Attachment input set; proceeding without UI confirmation.");
|
|
988
1008
|
inputConfirmed = true;
|
|
989
1009
|
break;
|
|
990
1010
|
}
|
|
991
|
-
logger(
|
|
992
|
-
result = await runInputAttempt(
|
|
993
|
-
if (result.evaluation.status ===
|
|
1011
|
+
logger("Attachment not acknowledged after file input set; retrying with data transfer.");
|
|
1012
|
+
result = await runInputAttempt("transfer");
|
|
1013
|
+
if (result.evaluation.status === "ui") {
|
|
994
1014
|
confirmedAttachment = true;
|
|
995
1015
|
break;
|
|
996
1016
|
}
|
|
997
|
-
if (result.evaluation.status ===
|
|
998
|
-
logger(
|
|
1017
|
+
if (result.evaluation.status === "input") {
|
|
1018
|
+
logger("Attachment input set; proceeding without UI confirmation.");
|
|
999
1019
|
inputConfirmed = true;
|
|
1000
1020
|
break;
|
|
1001
1021
|
}
|
|
1002
1022
|
if (orderIndex < candidateOrder.length - 1) {
|
|
1003
|
-
await dom
|
|
1023
|
+
await dom
|
|
1024
|
+
.setFileInputFiles({ nodeId: resultNode.nodeId, files: [] })
|
|
1025
|
+
.catch(() => undefined);
|
|
1004
1026
|
await delay(150);
|
|
1005
1027
|
}
|
|
1006
1028
|
}
|
|
@@ -1010,7 +1032,9 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
1010
1032
|
const inputHasFile = inputNameCandidates.some((name) => matchesExpectedName(name)) ||
|
|
1011
1033
|
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
1012
1034
|
await waitForAttachmentVisible(runtime, expectedName, attachmentUiTimeoutMs, logger);
|
|
1013
|
-
logger(inputHasFile
|
|
1035
|
+
logger(inputHasFile
|
|
1036
|
+
? "Attachment queued (UI anchored, file input confirmed)"
|
|
1037
|
+
: "Attachment queued (UI anchored)");
|
|
1014
1038
|
return true;
|
|
1015
1039
|
}
|
|
1016
1040
|
const inputNameCandidates = resolveInputNameCandidates();
|
|
@@ -1018,15 +1042,17 @@ export async function uploadAttachmentFile(deps, attachment, logger, options) {
|
|
|
1018
1042
|
(lastInputValue && matchesExpectedName(lastInputValue));
|
|
1019
1043
|
if (await waitForAttachmentAnchored(runtime, expectedName, attachmentUiTimeoutMs)) {
|
|
1020
1044
|
await waitForAttachmentVisible(runtime, expectedName, attachmentUiTimeoutMs, logger);
|
|
1021
|
-
logger(inputHasFile
|
|
1045
|
+
logger(inputHasFile
|
|
1046
|
+
? "Attachment queued (UI anchored, file input confirmed)"
|
|
1047
|
+
: "Attachment queued (UI anchored)");
|
|
1022
1048
|
return true;
|
|
1023
1049
|
}
|
|
1024
1050
|
if (inputConfirmed || inputHasFile) {
|
|
1025
|
-
logger(
|
|
1051
|
+
logger("Attachment input accepted the file but UI did not acknowledge it; continuing with input confirmation only.");
|
|
1026
1052
|
return true;
|
|
1027
1053
|
}
|
|
1028
|
-
await logDomFailure(runtime, logger,
|
|
1029
|
-
throw new Error(
|
|
1054
|
+
await logDomFailure(runtime, logger, "file-upload-missing");
|
|
1055
|
+
throw new Error("Attachment did not register with the ChatGPT composer in time.");
|
|
1030
1056
|
}
|
|
1031
1057
|
export async function clearComposerAttachments(Runtime, timeoutMs, logger) {
|
|
1032
1058
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
@@ -1131,8 +1157,8 @@ export async function clearComposerAttachments(Runtime, timeoutMs, logger) {
|
|
|
1131
1157
|
if (value?.hadAttachments) {
|
|
1132
1158
|
sawAttachments = true;
|
|
1133
1159
|
}
|
|
1134
|
-
const chipCount = typeof value?.chipCount ===
|
|
1135
|
-
const inputCount = typeof value?.inputCount ===
|
|
1160
|
+
const chipCount = typeof value?.chipCount === "number" ? value.chipCount : 0;
|
|
1161
|
+
const inputCount = typeof value?.inputCount === "number" ? value.inputCount : 0;
|
|
1136
1162
|
lastState = { chipCount, inputCount };
|
|
1137
1163
|
if (chipCount === 0 && inputCount === 0) {
|
|
1138
1164
|
return;
|
|
@@ -1141,7 +1167,7 @@ export async function clearComposerAttachments(Runtime, timeoutMs, logger) {
|
|
|
1141
1167
|
}
|
|
1142
1168
|
if (sawAttachments) {
|
|
1143
1169
|
logger?.(`Attachment cleanup timed out; still saw ${lastState?.chipCount ?? 0} chips and ${lastState?.inputCount ?? 0} inputs.`);
|
|
1144
|
-
throw new Error(
|
|
1170
|
+
throw new Error("Existing attachments still present in composer; aborting to avoid duplicate uploads.");
|
|
1145
1171
|
}
|
|
1146
1172
|
}
|
|
1147
1173
|
export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNames = [], logger) {
|
|
@@ -1363,13 +1389,12 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1363
1389
|
const { result } = response;
|
|
1364
1390
|
const value = result?.value;
|
|
1365
1391
|
if (!value && logger?.verbose) {
|
|
1366
|
-
const exception = response
|
|
1367
|
-
?.exceptionDetails;
|
|
1392
|
+
const exception = response?.exceptionDetails;
|
|
1368
1393
|
if (exception) {
|
|
1369
1394
|
const details = [exception.text, exception.exception?.description]
|
|
1370
1395
|
.filter((part) => Boolean(part))
|
|
1371
|
-
.join(
|
|
1372
|
-
logger(`Attachment wait eval failed: ${details ||
|
|
1396
|
+
.join(" - ");
|
|
1397
|
+
logger(`Attachment wait eval failed: ${details || "unknown error"}`);
|
|
1373
1398
|
}
|
|
1374
1399
|
}
|
|
1375
1400
|
if (value) {
|
|
@@ -1388,24 +1413,24 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1388
1413
|
}
|
|
1389
1414
|
}
|
|
1390
1415
|
const attachedNames = (value.attachedNames ?? [])
|
|
1391
|
-
.map((name) => name.toLowerCase().replace(/\s+/g,
|
|
1416
|
+
.map((name) => name.toLowerCase().replace(/\s+/g, " ").trim())
|
|
1392
1417
|
.filter(Boolean);
|
|
1393
1418
|
const inputNames = (value.inputNames ?? [])
|
|
1394
|
-
.map((name) => name.toLowerCase().replace(/\s+/g,
|
|
1419
|
+
.map((name) => name.toLowerCase().replace(/\s+/g, " ").trim())
|
|
1395
1420
|
.filter(Boolean);
|
|
1396
|
-
const fileCount = typeof value.fileCount ===
|
|
1421
|
+
const fileCount = typeof value.fileCount === "number" ? value.fileCount : 0;
|
|
1397
1422
|
const fileCountSatisfied = expectedNormalized.length > 0 && fileCount >= expectedNormalized.length;
|
|
1398
1423
|
const matchesExpected = (expected) => {
|
|
1399
|
-
const baseName = expected.split(
|
|
1400
|
-
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g,
|
|
1401
|
-
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i,
|
|
1424
|
+
const baseName = expected.split("/").pop()?.split("\\").pop() ?? expected;
|
|
1425
|
+
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1426
|
+
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, "");
|
|
1402
1427
|
return attachedNames.some((raw) => {
|
|
1403
1428
|
if (raw.includes(normalizedExpected))
|
|
1404
1429
|
return true;
|
|
1405
1430
|
if (expectedNoExt.length >= 6 && raw.includes(expectedNoExt))
|
|
1406
1431
|
return true;
|
|
1407
|
-
if (raw.includes(
|
|
1408
|
-
const marker = raw.includes(
|
|
1432
|
+
if (raw.includes("…") || raw.includes("...")) {
|
|
1433
|
+
const marker = raw.includes("…") ? "…" : "...";
|
|
1409
1434
|
const [prefixRaw, suffixRaw] = raw.split(marker);
|
|
1410
1435
|
const prefix = prefixRaw.trim();
|
|
1411
1436
|
const suffix = suffixRaw.trim();
|
|
@@ -1424,12 +1449,12 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1424
1449
|
attachmentMatchSince = Date.now();
|
|
1425
1450
|
}
|
|
1426
1451
|
const stable = Date.now() - attachmentMatchSince > stableThresholdMs;
|
|
1427
|
-
if (stable && value.state ===
|
|
1452
|
+
if (stable && value.state === "ready") {
|
|
1428
1453
|
return;
|
|
1429
1454
|
}
|
|
1430
1455
|
// Don't treat disabled button as complete - wait for it to become 'ready'.
|
|
1431
1456
|
// The spinner detection is unreliable, so a disabled button likely means upload is in progress.
|
|
1432
|
-
if (value.state ===
|
|
1457
|
+
if (value.state === "missing" && (value.filesAttached || fileCountSatisfied)) {
|
|
1433
1458
|
return;
|
|
1434
1459
|
}
|
|
1435
1460
|
// If files are attached but button isn't ready yet, give it more time but don't fail immediately.
|
|
@@ -1444,13 +1469,14 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1444
1469
|
// Fallback: if the file input has the expected names, allow progress once that condition is stable.
|
|
1445
1470
|
// Some ChatGPT surfaces only render the filename after sending the message.
|
|
1446
1471
|
const inputMissing = expectedNormalized.filter((expected) => {
|
|
1447
|
-
const baseName = expected.split(
|
|
1448
|
-
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g,
|
|
1449
|
-
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i,
|
|
1450
|
-
return !inputNames.some((raw) => raw.includes(normalizedExpected) ||
|
|
1472
|
+
const baseName = expected.split("/").pop()?.split("\\").pop() ?? expected;
|
|
1473
|
+
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1474
|
+
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, "");
|
|
1475
|
+
return !inputNames.some((raw) => raw.includes(normalizedExpected) ||
|
|
1476
|
+
(expectedNoExt.length >= 6 && raw.includes(expectedNoExt)));
|
|
1451
1477
|
});
|
|
1452
1478
|
// Don't include 'disabled' - a disabled button likely means upload is still in progress.
|
|
1453
|
-
const inputStateOk = value.state ===
|
|
1479
|
+
const inputStateOk = value.state === "ready" || value.state === "missing";
|
|
1454
1480
|
const inputSeenNow = inputMissing.length === 0 || fileCountSatisfied;
|
|
1455
1481
|
const inputEvidenceOk = Boolean(value.filesAttached) || Boolean(value.uploading) || fileCountSatisfied;
|
|
1456
1482
|
const stableThresholdMs = value.uploading ? 3000 : 1500;
|
|
@@ -1460,7 +1486,10 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1460
1486
|
}
|
|
1461
1487
|
sawInputMatch = true;
|
|
1462
1488
|
}
|
|
1463
|
-
if (inputMatchSince !== null &&
|
|
1489
|
+
if (inputMatchSince !== null &&
|
|
1490
|
+
inputStateOk &&
|
|
1491
|
+
inputEvidenceOk &&
|
|
1492
|
+
Date.now() - inputMatchSince > stableThresholdMs) {
|
|
1464
1493
|
return;
|
|
1465
1494
|
}
|
|
1466
1495
|
if (!inputSeenNow && !sawInputMatch) {
|
|
@@ -1469,28 +1498,113 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
1469
1498
|
}
|
|
1470
1499
|
await delay(250);
|
|
1471
1500
|
}
|
|
1472
|
-
logger?.(
|
|
1473
|
-
await logDomFailure(Runtime, logger ?? (() => { }),
|
|
1474
|
-
throw new Error(
|
|
1501
|
+
logger?.("Attachment upload timed out while waiting for ChatGPT composer to become ready.");
|
|
1502
|
+
await logDomFailure(Runtime, logger ?? (() => { }), "file-upload-timeout");
|
|
1503
|
+
throw new Error("Attachments did not finish uploading before timeout.");
|
|
1475
1504
|
}
|
|
1476
|
-
export async function waitForUserTurnAttachments(Runtime, expectedNames, timeoutMs, logger) {
|
|
1505
|
+
export async function waitForUserTurnAttachments(Runtime, expectedNames, timeoutMs, logger, options) {
|
|
1477
1506
|
if (!expectedNames || expectedNames.length === 0) {
|
|
1478
1507
|
return true;
|
|
1479
1508
|
}
|
|
1480
1509
|
const expectedNormalized = expectedNames.map((name) => name.toLowerCase());
|
|
1510
|
+
const minTurnIndex = typeof options?.minTurnIndex === "number" && Number.isFinite(options.minTurnIndex)
|
|
1511
|
+
? Math.max(0, Math.floor(options.minTurnIndex))
|
|
1512
|
+
: null;
|
|
1513
|
+
const expectedPromptPrefix = options?.expectedPrompt
|
|
1514
|
+
? options.expectedPrompt.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 80)
|
|
1515
|
+
: "";
|
|
1516
|
+
const expectedConversationId = typeof options?.expectedConversationId === "string" &&
|
|
1517
|
+
options.expectedConversationId.trim().length > 0
|
|
1518
|
+
? options.expectedConversationId.trim()
|
|
1519
|
+
: null;
|
|
1520
|
+
const expression = buildUserTurnAttachmentExpression({
|
|
1521
|
+
minTurnIndex,
|
|
1522
|
+
expectedPromptPrefix,
|
|
1523
|
+
expectedConversationId,
|
|
1524
|
+
});
|
|
1525
|
+
const deadline = Date.now() + timeoutMs;
|
|
1526
|
+
let sawAttachmentUi = false;
|
|
1527
|
+
while (Date.now() < deadline) {
|
|
1528
|
+
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
1529
|
+
const value = result?.value;
|
|
1530
|
+
if (!value?.ok) {
|
|
1531
|
+
if (value?.conversationMismatch && logger?.verbose) {
|
|
1532
|
+
logger("User-turn attachment verification ignored mismatched conversation.");
|
|
1533
|
+
}
|
|
1534
|
+
await delay(200);
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (value.hasAttachmentUi) {
|
|
1538
|
+
sawAttachmentUi = true;
|
|
1539
|
+
}
|
|
1540
|
+
const haystack = [value.text ?? "", ...(value.attrs ?? [])].join("\n");
|
|
1541
|
+
const fileCount = typeof value.fileCount === "number" ? value.fileCount : 0;
|
|
1542
|
+
const attachmentUiCount = typeof value.attachmentUiCount === "number" ? value.attachmentUiCount : 0;
|
|
1543
|
+
const promptMatches = expectedPromptPrefix ? value.promptMatches !== false : true;
|
|
1544
|
+
const fileCountSatisfied = fileCount >= expectedNormalized.length && expectedNormalized.length > 0;
|
|
1545
|
+
const attachmentUiSatisfied = attachmentUiCount >= expectedNormalized.length && expectedNormalized.length > 0;
|
|
1546
|
+
const missing = expectedNormalized.filter((expected) => {
|
|
1547
|
+
const baseName = expected.split("/").pop()?.split("\\").pop() ?? expected;
|
|
1548
|
+
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1549
|
+
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, "");
|
|
1550
|
+
if (haystack.includes(normalizedExpected))
|
|
1551
|
+
return false;
|
|
1552
|
+
if (expectedNoExt.length >= 6 && haystack.includes(expectedNoExt))
|
|
1553
|
+
return false;
|
|
1554
|
+
return true;
|
|
1555
|
+
});
|
|
1556
|
+
if (promptMatches && (missing.length === 0 || fileCountSatisfied || attachmentUiSatisfied)) {
|
|
1557
|
+
return true;
|
|
1558
|
+
}
|
|
1559
|
+
await delay(250);
|
|
1560
|
+
}
|
|
1561
|
+
if (!sawAttachmentUi) {
|
|
1562
|
+
logger?.("Sent user message did not expose attachment UI; skipping attachment verification.");
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
logger?.("Sent user message did not show expected attachment names in time.");
|
|
1566
|
+
await logDomFailure(Runtime, logger ?? (() => { }), "attachment-missing-user-turn");
|
|
1567
|
+
throw new Error("Attachment was not present on the sent user message.");
|
|
1568
|
+
}
|
|
1569
|
+
function buildUserTurnAttachmentExpression(options) {
|
|
1481
1570
|
const conversationSelectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
1482
|
-
const
|
|
1571
|
+
const minTurnLiteral = options.minTurnIndex === null ? "null" : String(options.minTurnIndex);
|
|
1572
|
+
const expectedPromptLiteral = JSON.stringify(options.expectedPromptPrefix);
|
|
1573
|
+
const expectedConversationLiteral = options.expectedConversationId
|
|
1574
|
+
? JSON.stringify(options.expectedConversationId)
|
|
1575
|
+
: "null";
|
|
1576
|
+
return `(() => {
|
|
1483
1577
|
const CONVERSATION_SELECTOR = ${conversationSelectorLiteral};
|
|
1578
|
+
const MIN_TURN_INDEX = ${minTurnLiteral};
|
|
1579
|
+
const EXPECTED_PROMPT_PREFIX = ${expectedPromptLiteral};
|
|
1580
|
+
const EXPECTED_CONVERSATION_ID = ${expectedConversationLiteral};
|
|
1581
|
+
const currentHref = typeof location === 'object' && location.href ? location.href : '';
|
|
1582
|
+
const currentConversationId = currentHref.match(/\\/c\\/([a-zA-Z0-9-]+)/)?.[1] ?? null;
|
|
1583
|
+
if (
|
|
1584
|
+
EXPECTED_CONVERSATION_ID &&
|
|
1585
|
+
currentConversationId &&
|
|
1586
|
+
currentConversationId !== EXPECTED_CONVERSATION_ID
|
|
1587
|
+
) {
|
|
1588
|
+
return { ok: false, conversationMismatch: true };
|
|
1589
|
+
}
|
|
1484
1590
|
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
1485
|
-
const userTurns = turns.filter((node) => {
|
|
1591
|
+
const userTurns = turns.map((node, index) => ({ node, index })).filter(({ node }) => {
|
|
1486
1592
|
const attr = (node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
1487
1593
|
if (attr === 'user') return true;
|
|
1488
1594
|
return Boolean(node.querySelector('[data-message-author-role="user"]'));
|
|
1489
1595
|
});
|
|
1490
|
-
const
|
|
1596
|
+
const eligibleTurns =
|
|
1597
|
+
MIN_TURN_INDEX === null ? userTurns : userTurns.filter(({ index }) => index >= MIN_TURN_INDEX);
|
|
1598
|
+
const lastUser = eligibleTurns[eligibleTurns.length - 1];
|
|
1491
1599
|
if (!lastUser) return { ok: false };
|
|
1492
|
-
const text = (lastUser.innerText || '').toLowerCase();
|
|
1493
|
-
const
|
|
1600
|
+
const text = (lastUser.node.innerText || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
1601
|
+
const textPrefix = text.slice(0, Math.min(text.length, EXPECTED_PROMPT_PREFIX.length));
|
|
1602
|
+
const promptMatches =
|
|
1603
|
+
!EXPECTED_PROMPT_PREFIX ||
|
|
1604
|
+
(text.length > 0 &&
|
|
1605
|
+
(text.includes(EXPECTED_PROMPT_PREFIX) ||
|
|
1606
|
+
(textPrefix.length > 0 && EXPECTED_PROMPT_PREFIX.includes(textPrefix))));
|
|
1607
|
+
const attrs = Array.from(lastUser.node.querySelectorAll('[aria-label],[title]')).map((el) => {
|
|
1494
1608
|
const aria = el.getAttribute('aria-label') || '';
|
|
1495
1609
|
const title = el.getAttribute('title') || '';
|
|
1496
1610
|
return (aria + ' ' + title).trim().toLowerCase();
|
|
@@ -1504,11 +1618,11 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1504
1618
|
'[title*="file"]',
|
|
1505
1619
|
'[title*="attachment"]',
|
|
1506
1620
|
];
|
|
1507
|
-
const attachmentUiCount = lastUser.querySelectorAll(attachmentSelectors.join(',')).length;
|
|
1621
|
+
const attachmentUiCount = lastUser.node.querySelectorAll(attachmentSelectors.join(',')).length;
|
|
1508
1622
|
const hasAttachmentUi =
|
|
1509
1623
|
attachmentUiCount > 0 || attrs.some((attr) => attr.includes('file') || attr.includes('attachment'));
|
|
1510
1624
|
const countRegex = /(?:^|\\b)(\\d+)\\s+(?:files?|attachments?)\\b/;
|
|
1511
|
-
const fileCountNodes = Array.from(lastUser.querySelectorAll('button,span,div,[aria-label],[title]'));
|
|
1625
|
+
const fileCountNodes = Array.from(lastUser.node.querySelectorAll('button,span,div,[aria-label],[title]'));
|
|
1512
1626
|
let fileCount = 0;
|
|
1513
1627
|
for (const node of fileCountNodes) {
|
|
1514
1628
|
if (!(node instanceof HTMLElement)) continue;
|
|
@@ -1541,47 +1655,29 @@ export async function waitForUserTurnAttachments(Runtime, expectedNames, timeout
|
|
|
1541
1655
|
}
|
|
1542
1656
|
}
|
|
1543
1657
|
}
|
|
1544
|
-
return {
|
|
1658
|
+
return {
|
|
1659
|
+
ok: true,
|
|
1660
|
+
text,
|
|
1661
|
+
attrs,
|
|
1662
|
+
fileCount,
|
|
1663
|
+
hasAttachmentUi,
|
|
1664
|
+
attachmentUiCount,
|
|
1665
|
+
promptMatches,
|
|
1666
|
+
turnIndex: lastUser.index,
|
|
1667
|
+
};
|
|
1545
1668
|
})()`;
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
const haystack = [value.text ?? '', ...(value.attrs ?? [])].join('\n');
|
|
1559
|
-
const fileCount = typeof value.fileCount === 'number' ? value.fileCount : 0;
|
|
1560
|
-
const attachmentUiCount = typeof value.attachmentUiCount === 'number' ? value.attachmentUiCount : 0;
|
|
1561
|
-
const fileCountSatisfied = fileCount >= expectedNormalized.length && expectedNormalized.length > 0;
|
|
1562
|
-
const attachmentUiSatisfied = attachmentUiCount >= expectedNormalized.length && expectedNormalized.length > 0;
|
|
1563
|
-
const missing = expectedNormalized.filter((expected) => {
|
|
1564
|
-
const baseName = expected.split('/').pop()?.split('\\').pop() ?? expected;
|
|
1565
|
-
const normalizedExpected = baseName.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
1566
|
-
const expectedNoExt = normalizedExpected.replace(/\.[a-z0-9]{1,10}$/i, '');
|
|
1567
|
-
if (haystack.includes(normalizedExpected))
|
|
1568
|
-
return false;
|
|
1569
|
-
if (expectedNoExt.length >= 6 && haystack.includes(expectedNoExt))
|
|
1570
|
-
return false;
|
|
1571
|
-
return true;
|
|
1572
|
-
});
|
|
1573
|
-
if (missing.length === 0 || fileCountSatisfied || attachmentUiSatisfied) {
|
|
1574
|
-
return true;
|
|
1575
|
-
}
|
|
1576
|
-
await delay(250);
|
|
1577
|
-
}
|
|
1578
|
-
if (!sawAttachmentUi) {
|
|
1579
|
-
logger?.('Sent user message did not expose attachment UI; skipping attachment verification.');
|
|
1580
|
-
return false;
|
|
1581
|
-
}
|
|
1582
|
-
logger?.('Sent user message did not show expected attachment names in time.');
|
|
1583
|
-
await logDomFailure(Runtime, logger ?? (() => { }), 'attachment-missing-user-turn');
|
|
1584
|
-
throw new Error('Attachment was not present on the sent user message.');
|
|
1669
|
+
}
|
|
1670
|
+
export function buildUserTurnAttachmentExpressionForTest(options) {
|
|
1671
|
+
return buildUserTurnAttachmentExpression({
|
|
1672
|
+
minTurnIndex: typeof options?.minTurnIndex === "number" && Number.isFinite(options.minTurnIndex)
|
|
1673
|
+
? Math.max(0, Math.floor(options.minTurnIndex))
|
|
1674
|
+
: null,
|
|
1675
|
+
expectedPromptPrefix: options?.expectedPromptPrefix ?? "",
|
|
1676
|
+
expectedConversationId: typeof options?.expectedConversationId === "string" &&
|
|
1677
|
+
options.expectedConversationId.trim().length > 0
|
|
1678
|
+
? options.expectedConversationId.trim()
|
|
1679
|
+
: null,
|
|
1680
|
+
});
|
|
1585
1681
|
}
|
|
1586
1682
|
export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs, logger) {
|
|
1587
1683
|
// Attachments can take a few seconds to render in the composer (headless/remote Chrome is slower),
|
|
@@ -1763,9 +1859,9 @@ export async function waitForAttachmentVisible(Runtime, expectedName, timeoutMs,
|
|
|
1763
1859
|
}
|
|
1764
1860
|
await delay(200);
|
|
1765
1861
|
}
|
|
1766
|
-
logger?.(
|
|
1767
|
-
await logDomFailure(Runtime, logger ?? (() => { }),
|
|
1768
|
-
throw new Error(
|
|
1862
|
+
logger?.("Attachment not visible in composer; giving up.");
|
|
1863
|
+
await logDomFailure(Runtime, logger ?? (() => { }), "attachment-visible");
|
|
1864
|
+
throw new Error("Attachment did not appear in ChatGPT composer.");
|
|
1769
1865
|
}
|
|
1770
1866
|
async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
1771
1867
|
const deadline = Date.now() + timeoutMs;
|