@steipete/oracle 0.15.0 → 0.15.2
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/dist/bin/oracle-cli.js +14 -6
- package/dist/docs-site/bridge.html +17 -1
- package/dist/docs-site/browser-mode.html +2 -2
- package/dist/docs-site/configuration.html +12 -2
- package/dist/docs-site/openai-endpoints.html +12 -0
- package/dist/scripts/test-browser.js +13 -2
- package/dist/src/browser/actions/assistantResponse.js +81 -50
- package/dist/src/browser/actions/attachments.js +31 -5
- package/dist/src/browser/actions/deepResearch.js +218 -73
- package/dist/src/browser/actions/modelSelection.js +30 -7
- package/dist/src/browser/actions/promptComposer.js +75 -19
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/artifacts.js +191 -6
- package/dist/src/browser/chatgptFiles.js +529 -98
- package/dist/src/browser/chatgptImages.js +3 -4
- package/dist/src/browser/chromeLifecycle.js +1 -0
- package/dist/src/browser/constants.js +6 -0
- package/dist/src/browser/conversationTurns.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +64 -0
- package/dist/src/browser/cookies.js +72 -0
- package/dist/src/browser/index.js +103 -94
- package/dist/src/browser/projectSourcesRunner.js +3 -2
- package/dist/src/browser/reattach.js +27 -11
- package/dist/src/browser/reattachHelpers.js +14 -5
- package/dist/src/browser/sessionRunner.js +9 -3
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +28 -10
- package/dist/src/config.js +3 -0
- package/dist/src/oracle/client.js +2 -0
- package/dist/src/oracle/modelResolver.js +85 -0
- package/dist/src/oracle/multiModelRunner.js +4 -1
- package/dist/src/oracle/oscProgress.js +3 -2
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/remote/client.js +253 -22
- package/dist/src/remote/health.js +27 -0
- package/dist/src/remote/server.js +239 -4
- package/dist/src/remote/types.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 -20
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { DEEP_RESEARCH_PLUS_BUTTON, DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, DEEP_RESEARCH_PILL_LABEL, DEEP_RESEARCH_POLL_INTERVAL_MS, DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR,
|
|
1
|
+
import { DEEP_RESEARCH_PLUS_BUTTON, DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, DEEP_RESEARCH_PILL_LABEL, DEEP_RESEARCH_POLL_INTERVAL_MS, DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, } from "../constants.js";
|
|
2
|
+
import { buildConversationTurnListExpression } from "../conversationTurns.js";
|
|
2
3
|
import { delay } from "../utils.js";
|
|
3
4
|
import { isDeepResearchIncompleteText } from "../deepResearchResult.js";
|
|
4
5
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
5
6
|
import { captureAssistantMarkdown, readAssistantSnapshot } from "./assistantResponse.js";
|
|
6
7
|
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
7
8
|
/**
|
|
8
|
-
* Activates Deep Research mode through ChatGPT's
|
|
9
|
-
*
|
|
9
|
+
* Activates Deep Research mode through ChatGPT's composer tools menu and
|
|
10
|
+
* verifies the selected tool pill before prompt submission.
|
|
10
11
|
*/
|
|
11
|
-
export async function activateDeepResearch(Runtime,
|
|
12
|
+
export async function activateDeepResearch(Runtime, Input, logger) {
|
|
12
13
|
const expression = buildActivateDeepResearchExpression();
|
|
13
14
|
const outcome = await Runtime.evaluate({
|
|
14
15
|
expression,
|
|
@@ -32,14 +33,48 @@ export async function activateDeepResearch(Runtime, _Input, logger) {
|
|
|
32
33
|
throw new BrowserAutomationError(`"Deep research" option not found in composer dropdown.${hint} ` +
|
|
33
34
|
"This feature may require a ChatGPT Plus or Pro subscription.", { stage: "deep-research-activate", code: "dropdown-item-missing" });
|
|
34
35
|
}
|
|
35
|
-
case "pill-not-confirmed":
|
|
36
|
+
case "pill-not-confirmed": {
|
|
37
|
+
const point = result.clickPoint;
|
|
38
|
+
if (typeof point?.x === "number" && typeof point.y === "number") {
|
|
39
|
+
await clickTrustedPoint(Runtime, Input, point.x, point.y);
|
|
40
|
+
if (await waitForDeepResearchPill(Runtime)) {
|
|
41
|
+
logger("Deep Research mode activated");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
36
45
|
throw new BrowserAutomationError("Deep Research pill did not appear after selection. The UI may have changed.", { stage: "deep-research-activate", code: "pill-not-confirmed" });
|
|
46
|
+
}
|
|
37
47
|
default:
|
|
38
48
|
throw new BrowserAutomationError("Unexpected result from Deep Research activation.", {
|
|
39
49
|
stage: "deep-research-activate",
|
|
40
50
|
});
|
|
41
51
|
}
|
|
42
52
|
}
|
|
53
|
+
async function clickTrustedPoint(Runtime, Input, x, y) {
|
|
54
|
+
if (Input && typeof Input.dispatchMouseEvent === "function") {
|
|
55
|
+
await Input.dispatchMouseEvent({ type: "mouseMoved", x, y });
|
|
56
|
+
await Input.dispatchMouseEvent({ type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
57
|
+
await Input.dispatchMouseEvent({ type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await Runtime.evaluate({
|
|
61
|
+
expression: `(() => {
|
|
62
|
+
const el = document.elementFromPoint(${JSON.stringify(x)}, ${JSON.stringify(y)});
|
|
63
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
64
|
+
el.click();
|
|
65
|
+
return true;
|
|
66
|
+
})()`,
|
|
67
|
+
returnByValue: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function waitForDeepResearchPill(Runtime, timeoutMs = 5000) {
|
|
71
|
+
const { result } = await Runtime.evaluate({
|
|
72
|
+
expression: buildWaitForDeepResearchPillExpression(timeoutMs),
|
|
73
|
+
awaitPromise: true,
|
|
74
|
+
returnByValue: true,
|
|
75
|
+
});
|
|
76
|
+
return Boolean(result?.value);
|
|
77
|
+
}
|
|
43
78
|
/**
|
|
44
79
|
* After prompt submission, waits for the research plan to appear and
|
|
45
80
|
* auto-confirm (~60s countdown + 10s safety margin).
|
|
@@ -446,9 +481,9 @@ async function readDeepResearchTargetOwnerTurnIndex(rawClient, frameId, pageSess
|
|
|
446
481
|
.send("Runtime.callFunctionOn", {
|
|
447
482
|
objectId,
|
|
448
483
|
functionDeclaration: `function() {
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
return
|
|
484
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
485
|
+
const index = turns.findIndex((turn) => turn === this || turn.contains?.(this));
|
|
486
|
+
return index >= 0 ? index : null;
|
|
452
487
|
}`,
|
|
453
488
|
returnByValue: true,
|
|
454
489
|
}, pageSessionId)
|
|
@@ -669,7 +704,6 @@ function buildDeepResearchStatusExpression() {
|
|
|
669
704
|
function buildDeepResearchCompletionPollExpression(minTurnIndex) {
|
|
670
705
|
const finishedSelector = JSON.stringify(FINISHED_ACTIONS_SELECTOR);
|
|
671
706
|
const stopSelector = JSON.stringify(STOP_BUTTON_SELECTOR);
|
|
672
|
-
const turnSelector = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
673
707
|
return `(() => {
|
|
674
708
|
const MIN_TURN_INDEX = ${minTurnIndex};
|
|
675
709
|
const stopVisible = Boolean(document.querySelector(${stopSelector}));
|
|
@@ -685,7 +719,7 @@ function buildDeepResearchCompletionPollExpression(minTurnIndex) {
|
|
|
685
719
|
String(node.getAttribute('data-testid') || '').toLowerCase().includes('conversation-turn') &&
|
|
686
720
|
/chatgpt\\s+said/i.test(node.innerText || node.textContent || '');
|
|
687
721
|
};
|
|
688
|
-
const conversationTurns =
|
|
722
|
+
const conversationTurns = ${buildConversationTurnListExpression()};
|
|
689
723
|
const allAssistantTurns = Array.from(document.querySelectorAll('[data-message-author-role="assistant"], [data-turn="assistant"]'));
|
|
690
724
|
const scopedTurns = scopedToNewTurns
|
|
691
725
|
? conversationTurns.slice(MIN_TURN_INDEX).filter(isAssistantTurn)
|
|
@@ -737,29 +771,56 @@ export function buildDeepResearchStatusExpressionForTest() {
|
|
|
737
771
|
export function buildDeepResearchCompletionPollExpressionForTest(minTurnIndex = -1) {
|
|
738
772
|
return buildDeepResearchCompletionPollExpression(minTurnIndex);
|
|
739
773
|
}
|
|
740
|
-
function
|
|
741
|
-
const plusBtnSelector = JSON.stringify(DEEP_RESEARCH_PLUS_BUTTON);
|
|
742
|
-
const targetText = JSON.stringify(DEEP_RESEARCH_DROPDOWN_ITEM_TEXT);
|
|
774
|
+
function buildFindDeepResearchPillExpression(functionName = "findDeepResearchPill") {
|
|
743
775
|
const pillLabel = JSON.stringify(DEEP_RESEARCH_PILL_LABEL);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
776
|
+
return `const ${functionName} = () => {
|
|
777
|
+
const label = ${pillLabel}.toLowerCase();
|
|
778
|
+
const selectors = [
|
|
779
|
+
'.__composer-pill-composite',
|
|
780
|
+
'.__composer-pill',
|
|
781
|
+
'[class*="composer-pill"]',
|
|
782
|
+
].join(',');
|
|
783
|
+
const candidates = Array.from(document.querySelectorAll(selectors));
|
|
784
|
+
const composerRoots = Array.from(document.querySelectorAll('[data-testid="composer"], form, [class*="composer"]'));
|
|
785
|
+
for (const root of composerRoots) {
|
|
786
|
+
candidates.push(...Array.from(root.querySelectorAll('button, [role="button"], [class*="pill"], [class*="composer-pill"]')));
|
|
787
|
+
}
|
|
788
|
+
const seen = new Set();
|
|
789
|
+
for (const pill of candidates) {
|
|
790
|
+
if (!(pill instanceof Element) || seen.has(pill)) continue;
|
|
791
|
+
seen.add(pill);
|
|
792
|
+
const rect = pill.getBoundingClientRect?.();
|
|
793
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) continue;
|
|
794
|
+
const text = (pill.textContent || '').replace(/\\s+/g, ' ').trim().toLowerCase();
|
|
795
|
+
const aria = (
|
|
796
|
+
pill.getAttribute('aria-label') ||
|
|
754
797
|
pill.querySelector('button')?.getAttribute('aria-label') ||
|
|
755
|
-
''
|
|
756
|
-
|
|
757
|
-
|
|
798
|
+
''
|
|
799
|
+
).toLowerCase();
|
|
800
|
+
if (text.includes(label) || aria.includes(label)) {
|
|
758
801
|
return pill;
|
|
759
802
|
}
|
|
760
803
|
}
|
|
761
804
|
return null;
|
|
762
|
-
}
|
|
805
|
+
};`;
|
|
806
|
+
}
|
|
807
|
+
function buildWaitForDeepResearchPillExpression(timeoutMs) {
|
|
808
|
+
return `(async () => {
|
|
809
|
+
${buildFindDeepResearchPillExpression()}
|
|
810
|
+
const deadline = Date.now() + ${JSON.stringify(Math.max(timeoutMs, 0))};
|
|
811
|
+
while (Date.now() < deadline) {
|
|
812
|
+
if (findDeepResearchPill()) return true;
|
|
813
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
814
|
+
}
|
|
815
|
+
return Boolean(findDeepResearchPill());
|
|
816
|
+
})()`;
|
|
817
|
+
}
|
|
818
|
+
function buildActivateDeepResearchExpression() {
|
|
819
|
+
const plusBtnSelector = JSON.stringify(DEEP_RESEARCH_PLUS_BUTTON);
|
|
820
|
+
const targetText = JSON.stringify(DEEP_RESEARCH_DROPDOWN_ITEM_TEXT);
|
|
821
|
+
return `(async () => {
|
|
822
|
+
${buildClickDispatcher()}
|
|
823
|
+
${buildFindDeepResearchPillExpression()}
|
|
763
824
|
|
|
764
825
|
const waitForPill = () => new Promise((resolve) => {
|
|
765
826
|
let elapsed = 0;
|
|
@@ -774,24 +835,105 @@ function buildActivateDeepResearchExpression() {
|
|
|
774
835
|
setTimeout(tick, 200);
|
|
775
836
|
});
|
|
776
837
|
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
838
|
+
const menuItemSelector = [
|
|
839
|
+
'[data-radix-collection-item]',
|
|
840
|
+
'[role="option"]',
|
|
841
|
+
'[cmdk-item]',
|
|
842
|
+
'button',
|
|
843
|
+
'[role="menuitem"]',
|
|
844
|
+
'[role="menuitemradio"]',
|
|
845
|
+
'.__menu-item',
|
|
846
|
+
'[class*="__menu-item"]',
|
|
847
|
+
'[class*="menu-item"]',
|
|
848
|
+
].join(',');
|
|
849
|
+
const dropdownItemSelector = [
|
|
850
|
+
'[data-radix-collection-item]',
|
|
851
|
+
'[role="menuitem"]',
|
|
852
|
+
'[role="menuitemradio"]',
|
|
853
|
+
'[role="option"]',
|
|
854
|
+
'[cmdk-item]',
|
|
855
|
+
'button',
|
|
856
|
+
'.__menu-item',
|
|
857
|
+
'[class*="__menu-item"]',
|
|
858
|
+
'[class*="menu-item"]',
|
|
859
|
+
].join(',');
|
|
860
|
+
const popoverSelector = [
|
|
861
|
+
'.popover',
|
|
862
|
+
'[class*="popover"]',
|
|
863
|
+
'[data-radix-popper-content-wrapper]',
|
|
864
|
+
'[data-floating-ui-portal]',
|
|
865
|
+
].join(',');
|
|
866
|
+
const target = ${targetText}.toLowerCase();
|
|
867
|
+
const normalizeText = (value) => String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
|
|
868
|
+
const getText = (item) => normalizeText(item.textContent || item.getAttribute?.('aria-label') || '');
|
|
869
|
+
const isInPopover = (item) => Boolean(item.closest?.(popoverSelector));
|
|
870
|
+
const isVisible = (item) => {
|
|
871
|
+
const rect = item.getBoundingClientRect?.();
|
|
872
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
|
|
873
|
+
const style = window.getComputedStyle?.(item);
|
|
874
|
+
return !style || (style.visibility !== 'hidden' && style.display !== 'none');
|
|
782
875
|
};
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
876
|
+
const findPopoverSearchInput = () => Array.from(
|
|
877
|
+
document.querySelectorAll('input, textarea, [contenteditable="true"]')
|
|
878
|
+
).find(item => {
|
|
879
|
+
const type = (item.getAttribute?.('type') || '').toLowerCase();
|
|
880
|
+
const testId = (item.getAttribute?.('data-testid') || '').toLowerCase();
|
|
881
|
+
return isInPopover(item) &&
|
|
882
|
+
isVisible(item) &&
|
|
883
|
+
type !== 'file' &&
|
|
884
|
+
testId !== 'upload-photos-input';
|
|
885
|
+
}) || null;
|
|
886
|
+
const setSearchText = (input, text) => {
|
|
887
|
+
input.focus?.();
|
|
888
|
+
if ('value' in input) input.value = text;
|
|
889
|
+
else input.textContent = text;
|
|
890
|
+
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
891
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
789
892
|
};
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
893
|
+
const isDeepResearchText = (text) => (
|
|
894
|
+
text === target ||
|
|
895
|
+
text.startsWith(target + ' ') ||
|
|
896
|
+
text === 'get a detailed report' ||
|
|
897
|
+
text.startsWith('get a detailed report ') ||
|
|
898
|
+
(text.includes(target) && text.includes('detailed report')) ||
|
|
899
|
+
text.replace(/\\s+/g, '').startsWith('deepresearch')
|
|
900
|
+
);
|
|
901
|
+
const getClickableItem = (item) => item.closest?.(
|
|
902
|
+
'[data-radix-collection-item], [role="option"], [cmdk-item], button, [role="menuitem"], [role="menuitemradio"], .__menu-item, [class*="__menu-item"], [class*="menu-item"]'
|
|
903
|
+
) || item;
|
|
904
|
+
const findDeepResearchItem = (options = {}) => {
|
|
905
|
+
const matches = Array.from(document.querySelectorAll(menuItemSelector))
|
|
906
|
+
.filter(item => {
|
|
907
|
+
const text = getText(item);
|
|
908
|
+
return text &&
|
|
909
|
+
text.length <= 180 &&
|
|
910
|
+
isVisible(item) &&
|
|
911
|
+
(!options.requirePopover || isInPopover(item)) &&
|
|
912
|
+
isDeepResearchText(text);
|
|
913
|
+
})
|
|
914
|
+
.map(item => {
|
|
915
|
+
const text = getText(item);
|
|
916
|
+
const clickable = getClickableItem(item);
|
|
917
|
+
const exact = text === target ? 0 : 1;
|
|
918
|
+
const menuRow = /(^|\\s)__menu-item(\\s|$)/.test(clickable.className || '') ? 0 : 1;
|
|
919
|
+
return { item: clickable, score: exact + menuRow, textLength: text.length };
|
|
920
|
+
})
|
|
921
|
+
.sort((a, b) => a.score - b.score || a.textLength - b.textLength);
|
|
922
|
+
return matches[0]?.item || null;
|
|
923
|
+
};
|
|
924
|
+
const collectAvailableItems = (options = {}) => {
|
|
925
|
+
const seen = new Set();
|
|
926
|
+
return Array.from(document.querySelectorAll(dropdownItemSelector))
|
|
927
|
+
.filter(item => !options.requirePopover || isInPopover(item))
|
|
928
|
+
.filter(item => isVisible(item))
|
|
929
|
+
.map(item => (item.textContent || '').replace(/\\s+/g, ' ').trim())
|
|
930
|
+
.filter(text => text && text.length <= 180)
|
|
931
|
+
.filter(text => {
|
|
932
|
+
const key = text.toLowerCase();
|
|
933
|
+
if (seen.has(key)) return false;
|
|
934
|
+
seen.add(key);
|
|
935
|
+
return true;
|
|
936
|
+
});
|
|
795
937
|
};
|
|
796
938
|
|
|
797
939
|
// Step 0: Check if already active
|
|
@@ -799,20 +941,8 @@ function buildActivateDeepResearchExpression() {
|
|
|
799
941
|
return { status: 'already-active' };
|
|
800
942
|
}
|
|
801
943
|
|
|
802
|
-
// Step 1:
|
|
803
|
-
|
|
804
|
-
if (composer) {
|
|
805
|
-
setComposerText(composer, '/Deepresearch');
|
|
806
|
-
await new Promise(resolve => setTimeout(resolve, 600));
|
|
807
|
-
const slashItem = findDeepResearchItem();
|
|
808
|
-
if (slashItem) {
|
|
809
|
-
dispatchClickSequence(slashItem);
|
|
810
|
-
if (await waitForPill()) return { status: 'activated' };
|
|
811
|
-
}
|
|
812
|
-
clearComposer(composer);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// Step 2: Fall back to the composer tools menu.
|
|
944
|
+
// Step 1: Open the composer tools menu. Avoid slash commands because they
|
|
945
|
+
// mutate the main composer and can be submitted as normal prompt text.
|
|
816
946
|
const plusBtn = document.querySelector(${plusBtnSelector}) ||
|
|
817
947
|
Array.from(document.querySelectorAll('button')).find(
|
|
818
948
|
b => (b.getAttribute('aria-label') || '').toLowerCase().includes('add files')
|
|
@@ -820,14 +950,21 @@ function buildActivateDeepResearchExpression() {
|
|
|
820
950
|
if (!plusBtn) return { status: 'plus-button-missing' };
|
|
821
951
|
dispatchClickSequence(plusBtn);
|
|
822
952
|
|
|
823
|
-
// Step
|
|
953
|
+
// Step 2: Wait for dropdown
|
|
824
954
|
const waitForDropdown = () => new Promise((resolve) => {
|
|
825
955
|
let elapsed = 0;
|
|
826
956
|
const tick = () => {
|
|
827
|
-
const items =
|
|
828
|
-
if (
|
|
957
|
+
const items = collectAvailableItems({ requirePopover: true });
|
|
958
|
+
if (findDeepResearchItem({ requirePopover: true }) || items.some(text => {
|
|
959
|
+
const normalized = normalizeText(text);
|
|
960
|
+
return normalized.includes('add photos') ||
|
|
961
|
+
normalized.includes('create image') ||
|
|
962
|
+
normalized.includes('web search') ||
|
|
963
|
+
normalized.includes('deep research') ||
|
|
964
|
+
normalized.includes('get a detailed report');
|
|
965
|
+
})) { resolve(items); return; }
|
|
829
966
|
elapsed += 150;
|
|
830
|
-
if (elapsed > 3000) { resolve(null); return; }
|
|
967
|
+
if (elapsed > 3000) { resolve(items.length ? items : null); return; }
|
|
831
968
|
setTimeout(tick, 150);
|
|
832
969
|
};
|
|
833
970
|
setTimeout(tick, 150);
|
|
@@ -835,25 +972,33 @@ function buildActivateDeepResearchExpression() {
|
|
|
835
972
|
const items = await waitForDropdown();
|
|
836
973
|
if (!items) return { status: 'dropdown-item-missing', available: [] };
|
|
837
974
|
|
|
838
|
-
// Step
|
|
839
|
-
|
|
840
|
-
let match =
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
975
|
+
// Step 3: Find "Deep research" item. Some ChatGPT variants only reveal it
|
|
976
|
+
// after typing in the tools menu search field.
|
|
977
|
+
let match = findDeepResearchItem({ requirePopover: true });
|
|
978
|
+
let available = Array.isArray(items) ? items : collectAvailableItems({ requirePopover: true });
|
|
979
|
+
if (!match) {
|
|
980
|
+
const searchInput = findPopoverSearchInput();
|
|
981
|
+
if (searchInput) {
|
|
982
|
+
setSearchText(searchInput, ${targetText});
|
|
983
|
+
await new Promise(resolve => setTimeout(resolve, 600));
|
|
984
|
+
match = findDeepResearchItem({ requirePopover: true });
|
|
985
|
+
available = collectAvailableItems({ requirePopover: true });
|
|
847
986
|
}
|
|
848
987
|
}
|
|
849
988
|
if (!match) return { status: 'dropdown-item-missing', available };
|
|
850
989
|
|
|
851
|
-
// Step
|
|
990
|
+
// Step 4: Click it
|
|
991
|
+
match.scrollIntoView?.({ block: 'center', inline: 'center' });
|
|
992
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
993
|
+
const rect = match.getBoundingClientRect();
|
|
994
|
+
const clickPoint = rect && rect.width > 0 && rect.height > 0
|
|
995
|
+
? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
|
996
|
+
: undefined;
|
|
852
997
|
dispatchClickSequence(match);
|
|
853
998
|
|
|
854
|
-
// Step
|
|
999
|
+
// Step 5: Verify pill appeared
|
|
855
1000
|
const pillConfirmed = await waitForPill();
|
|
856
|
-
return pillConfirmed ? { status: 'activated' } : { status: 'pill-not-confirmed' };
|
|
1001
|
+
return pillConfirmed ? { status: 'activated' } : { status: 'pill-not-confirmed', clickPoint };
|
|
857
1002
|
})()`;
|
|
858
1003
|
}
|
|
859
1004
|
export function buildActivateDeepResearchExpressionForTest() {
|
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
import { COMPOSER_MODEL_SIGNAL_SELECTOR, MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from "../constants.js";
|
|
2
2
|
import { logDomFailure } from "../domDebug.js";
|
|
3
3
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
4
|
+
import { delay } from "../utils.js";
|
|
4
5
|
const LEGACY_PRO_VERSION_WORD_TOKENS = ["5 4", "5 2", "5 1", "5 0", "gpt 5 pro"];
|
|
5
6
|
const LEGACY_PRO_VERSION_COMPACT_TOKENS = ["gpt54", "gpt52", "gpt51", "gpt50"];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
// The model/effort picker is a composer pill that React mounts a beat after the page
|
|
8
|
+
// becomes interactive (~1-4s on a cold profile, e.g. cookie-sync's throwaway Chrome).
|
|
9
|
+
// Re-evaluate while it is still missing, up to a bounded deadline, so selection does not
|
|
10
|
+
// give up before the pill renders. Only "button-missing" waits; a genuine
|
|
11
|
+
// "option-not-found" surfaces immediately.
|
|
12
|
+
const MODEL_BUTTON_WAIT_MS = 8000;
|
|
13
|
+
const MODEL_BUTTON_POLL_MS = 250;
|
|
14
|
+
export async function ensureModelSelection(Runtime, desiredModel, logger, strategy = "select", options = {}) {
|
|
15
|
+
const buttonWaitMs = options.buttonWaitMs ?? MODEL_BUTTON_WAIT_MS;
|
|
16
|
+
const buttonPollMs = options.buttonPollMs ?? MODEL_BUTTON_POLL_MS;
|
|
17
|
+
const deadline = Date.now() + Math.max(0, buttonWaitMs);
|
|
18
|
+
let result;
|
|
19
|
+
let announcedWait = false;
|
|
20
|
+
for (;;) {
|
|
21
|
+
const outcome = await Runtime.evaluate({
|
|
22
|
+
expression: buildModelSelectionExpression(desiredModel, strategy),
|
|
23
|
+
awaitPromise: true,
|
|
24
|
+
returnByValue: true,
|
|
25
|
+
});
|
|
26
|
+
result = outcome.result?.value;
|
|
27
|
+
if (result?.status !== "button-missing" || Date.now() >= deadline) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
if (!announcedWait) {
|
|
31
|
+
announcedWait = true;
|
|
32
|
+
logger(`Model picker button not mounted yet; waiting up to ${Math.round(buttonWaitMs / 1000)}s for the composer pill to render.`);
|
|
33
|
+
}
|
|
34
|
+
await delay(buttonPollMs);
|
|
35
|
+
}
|
|
13
36
|
switch (result?.status) {
|
|
14
37
|
case "already-selected":
|
|
15
38
|
case "switched": {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEND_BUTTON_SELECTORS,
|
|
1
|
+
import { INPUT_SELECTORS, PROMPT_PRIMARY_SELECTOR, PROMPT_FALLBACK_SELECTOR, SEND_BUTTON_SELECTORS, STOP_BUTTON_SELECTOR, ASSISTANT_ROLE_SELECTOR, } from "../constants.js";
|
|
2
|
+
import { buildConversationTurnCountExpression, buildConversationTurnListExpression, } from "../conversationTurns.js";
|
|
2
3
|
import { delay } from "../utils.js";
|
|
3
4
|
import { logDomFailure } from "../domDebug.js";
|
|
4
5
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
@@ -166,7 +167,7 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
166
167
|
observedLength,
|
|
167
168
|
});
|
|
168
169
|
}
|
|
169
|
-
const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames, deps?.attachmentTimeoutMs);
|
|
170
|
+
const clicked = await attemptSendButton(runtime, input, logger, deps?.attachmentNames, deps?.attachmentTimeoutMs);
|
|
170
171
|
if (!clicked) {
|
|
171
172
|
await input.dispatchKeyEvent({
|
|
172
173
|
type: "keyDown",
|
|
@@ -575,7 +576,7 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
575
576
|
export function buildAttachmentReadyExpressionForTest(attachmentNames) {
|
|
576
577
|
return buildAttachmentReadyExpression(attachmentNames);
|
|
577
578
|
}
|
|
578
|
-
async function attemptSendButton(Runtime, _logger, attachmentNames, attachmentTimeoutMs) {
|
|
579
|
+
async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attachmentTimeoutMs) {
|
|
579
580
|
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
580
581
|
const script = `(() => {
|
|
581
582
|
${buildClickDispatcher()}
|
|
@@ -604,10 +605,15 @@ async function attemptSendButton(Runtime, _logger, attachmentNames, attachmentTi
|
|
|
604
605
|
candidates.push(...Array.from(document.querySelectorAll(selector)));
|
|
605
606
|
}
|
|
606
607
|
const button = candidates.find((node) => isVisible(node) && isEnabled(node)) || null;
|
|
607
|
-
if (!button) return 'missing';
|
|
608
|
-
|
|
608
|
+
if (!button) return { status: 'missing' };
|
|
609
|
+
button.scrollIntoView({ block: 'center', inline: 'center' });
|
|
610
|
+
const rect = button.getBoundingClientRect();
|
|
611
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
612
|
+
return { status: 'point', x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
613
|
+
}
|
|
614
|
+
// Last-resort fallback for unusual DOMs where the button is visible but has no useful rect.
|
|
609
615
|
dispatchClickSequence(button);
|
|
610
|
-
return 'clicked';
|
|
616
|
+
return { status: 'clicked' };
|
|
611
617
|
})()`;
|
|
612
618
|
// Give attachment-bearing submissions more headroom. ChatGPT's chip render can
|
|
613
619
|
// settle slowly for multi-file uploads, but plain text sends should keep the
|
|
@@ -626,10 +632,19 @@ async function attemptSendButton(Runtime, _logger, attachmentNames, attachmentTi
|
|
|
626
632
|
}
|
|
627
633
|
}
|
|
628
634
|
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
629
|
-
|
|
635
|
+
const value = result.value;
|
|
636
|
+
const status = typeof value === "string" ? value : value?.status;
|
|
637
|
+
if (status === "point" &&
|
|
638
|
+
typeof value === "object" &&
|
|
639
|
+
typeof value.x === "number" &&
|
|
640
|
+
typeof value.y === "number") {
|
|
641
|
+
await clickTrustedPoint(Runtime, Input, value.x, value.y);
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
if (status === "clicked") {
|
|
630
645
|
return true;
|
|
631
646
|
}
|
|
632
|
-
if (
|
|
647
|
+
if (status === "missing") {
|
|
633
648
|
break;
|
|
634
649
|
}
|
|
635
650
|
await delay(100);
|
|
@@ -644,6 +659,23 @@ async function attemptSendButton(Runtime, _logger, attachmentNames, attachmentTi
|
|
|
644
659
|
}
|
|
645
660
|
return false;
|
|
646
661
|
}
|
|
662
|
+
async function clickTrustedPoint(Runtime, Input, x, y) {
|
|
663
|
+
if (Input && typeof Input.dispatchMouseEvent === "function") {
|
|
664
|
+
await Input.dispatchMouseEvent({ type: "mouseMoved", x, y });
|
|
665
|
+
await Input.dispatchMouseEvent({ type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
666
|
+
await Input.dispatchMouseEvent({ type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
await Runtime.evaluate({
|
|
670
|
+
expression: `(() => {
|
|
671
|
+
const el = document.elementFromPoint(${JSON.stringify(x)}, ${JSON.stringify(y)});
|
|
672
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
673
|
+
el.click();
|
|
674
|
+
return true;
|
|
675
|
+
})()`,
|
|
676
|
+
returnByValue: true,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
647
679
|
function sendButtonTimeoutMs(attachmentNames, attachmentTimeoutMs) {
|
|
648
680
|
if (!Array.isArray(attachmentNames) || attachmentNames.length === 0) {
|
|
649
681
|
return 20_000;
|
|
@@ -660,14 +692,13 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
660
692
|
const inputSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
|
|
661
693
|
const stopSelectorLiteral = JSON.stringify(STOP_BUTTON_SELECTOR);
|
|
662
694
|
const assistantSelectorLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
663
|
-
const turnSelectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
664
695
|
let baseline = typeof baselineTurns === "number" && Number.isFinite(baselineTurns) && baselineTurns >= 0
|
|
665
696
|
? Math.floor(baselineTurns)
|
|
666
697
|
: null;
|
|
667
698
|
if (baseline === null) {
|
|
668
699
|
try {
|
|
669
700
|
const { result } = await Runtime.evaluate({
|
|
670
|
-
expression:
|
|
701
|
+
expression: buildConversationTurnCountExpression(),
|
|
671
702
|
returnByValue: true,
|
|
672
703
|
});
|
|
673
704
|
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
@@ -695,8 +726,7 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
695
726
|
};
|
|
696
727
|
const normalizedPrompt = normalize(${encodedPrompt});
|
|
697
728
|
const normalizedPromptPrefix = normalizedPrompt.slice(0, 120);
|
|
698
|
-
const
|
|
699
|
-
const articles = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
729
|
+
const articles = ${buildConversationTurnListExpression()};
|
|
700
730
|
const normalizedTurns = articles.map((node) => normalize(node?.innerText));
|
|
701
731
|
const readValue = (node) => {
|
|
702
732
|
if (!node) return '';
|
|
@@ -755,9 +785,13 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
755
785
|
turnsCount: normalizedTurns.length,
|
|
756
786
|
};
|
|
757
787
|
})()`;
|
|
788
|
+
let lastProbe;
|
|
758
789
|
while (Date.now() < deadline) {
|
|
759
790
|
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
760
791
|
const info = result.value;
|
|
792
|
+
if (info && typeof info === "object") {
|
|
793
|
+
lastProbe = info;
|
|
794
|
+
}
|
|
761
795
|
const turnsCount = result.value?.turnsCount;
|
|
762
796
|
const matchesPrompt = Boolean(info?.lastMatched || info?.userMatched || info?.prefixMatched);
|
|
763
797
|
const baselineUnknown = typeof info?.baseline === "number" ? info.baseline < 0 : baselineLiteral < 0;
|
|
@@ -772,13 +806,12 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
772
806
|
}
|
|
773
807
|
await delay(100);
|
|
774
808
|
}
|
|
809
|
+
const finalProbe = await Runtime.evaluate({ expression: script, returnByValue: true })
|
|
810
|
+
.then((res) => res?.result?.value)
|
|
811
|
+
.catch(() => undefined);
|
|
812
|
+
const probe = finalProbe && typeof finalProbe === "object" ? finalProbe : lastProbe;
|
|
775
813
|
if (logger) {
|
|
776
|
-
logger(`Prompt commit check failed; latest state: ${
|
|
777
|
-
expression: script,
|
|
778
|
-
returnByValue: true,
|
|
779
|
-
})
|
|
780
|
-
.then((res) => JSON.stringify(res?.result?.value))
|
|
781
|
-
.catch(() => "unavailable")}`);
|
|
814
|
+
logger(`Prompt commit check failed; latest state: ${probe ? JSON.stringify(probe) : "unavailable"}`);
|
|
782
815
|
await logDomFailure(Runtime, logger, "prompt-commit");
|
|
783
816
|
}
|
|
784
817
|
if (prompt.trim().length >= 50_000) {
|
|
@@ -789,7 +822,30 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
789
822
|
timeoutMs,
|
|
790
823
|
});
|
|
791
824
|
}
|
|
792
|
-
throw new
|
|
825
|
+
throw new BrowserAutomationError("Prompt did not appear in conversation before timeout (send may have failed)", {
|
|
826
|
+
stage: "submit-prompt",
|
|
827
|
+
code: "prompt-commit-timeout",
|
|
828
|
+
promptLength: prompt.trim().length,
|
|
829
|
+
timeoutMs,
|
|
830
|
+
commitProbe: probe ? summarizeCommitProbe(probe) : undefined,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
// Keep booleans/counts but replace free text with lengths so session metadata stays lean.
|
|
834
|
+
function summarizeCommitProbe(probe) {
|
|
835
|
+
return {
|
|
836
|
+
baseline: probe.baseline,
|
|
837
|
+
turnsCount: probe.turnsCount,
|
|
838
|
+
userMatched: probe.userMatched,
|
|
839
|
+
prefixMatched: probe.prefixMatched,
|
|
840
|
+
lastMatched: probe.lastMatched,
|
|
841
|
+
hasNewTurn: probe.hasNewTurn,
|
|
842
|
+
stopVisible: probe.stopVisible,
|
|
843
|
+
assistantVisible: probe.assistantVisible,
|
|
844
|
+
composerCleared: probe.composerCleared,
|
|
845
|
+
inConversation: probe.inConversation,
|
|
846
|
+
editorLength: typeof probe.editorValue === "string" ? probe.editorValue.length : undefined,
|
|
847
|
+
lastTurnLength: typeof probe.lastTurn === "string" ? probe.lastTurn.length : undefined,
|
|
848
|
+
};
|
|
793
849
|
}
|
|
794
850
|
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
795
851
|
export const __test__ = {
|