@steipete/oracle 0.14.1 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +16 -6
- package/dist/bin/oracle.js +569 -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 +416 -0
- package/dist/docs-site/browser-mode.html +594 -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 +462 -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 +399 -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 +40 -31
- package/dist/src/browser/actions/attachments.js +28 -1
- package/dist/src/browser/actions/deepResearch.js +344 -128
- package/dist/src/browser/actions/modelSelection.js +75 -9
- package/dist/src/browser/actions/promptComposer.js +71 -14
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/actions/thinkingTime.js +65 -20
- package/dist/src/browser/artifacts.js +193 -14
- package/dist/src/browser/chatgptFiles.js +674 -91
- package/dist/src/browser/chromeCookies.js +312 -0
- package/dist/src/browser/chromeLifecycle.js +35 -4
- package/dist/src/browser/constants.js +5 -0
- package/dist/src/browser/deepResearchResult.js +23 -0
- package/dist/src/browser/index.js +112 -41
- package/dist/src/browser/keytarShim.js +56 -0
- package/dist/src/browser/profileCopy.js +93 -0
- package/dist/src/browser/sessionRunner.js +9 -3
- package/dist/src/browser/windowsCookies.js +219 -0
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/browserConfig.js +17 -1
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +41 -17
- 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/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/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +45 -43
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/vendor/oracle-notifier/README.md +26 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
|
@@ -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": {
|
|
@@ -659,10 +682,27 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
659
682
|
normalizedTestId.includes('pro');
|
|
660
683
|
const candidateHasInstant =
|
|
661
684
|
normalizedText.includes('instant') || normalizedTestId.includes('instant');
|
|
685
|
+
const candidateSelectsConfiguredVersion =
|
|
686
|
+
Boolean(getConfigurationDialog()) &&
|
|
687
|
+
candidateSelectsDesiredVersion &&
|
|
688
|
+
(node?.getAttribute?.('role') === 'option' ||
|
|
689
|
+
node?.getAttribute?.('role') === 'menuitemradio');
|
|
690
|
+
const candidateOpensInstantSubmenu =
|
|
691
|
+
wantsInstant &&
|
|
692
|
+
candidateSelectsDesiredVersion &&
|
|
693
|
+
!candidateHasInstant &&
|
|
694
|
+
(normalizedTestId.includes('submenu') ||
|
|
695
|
+
node?.getAttribute?.('aria-haspopup') === 'menu' ||
|
|
696
|
+
node?.getAttribute?.('data-has-submenu') !== null);
|
|
662
697
|
if (wantsPro && candidateHasThinking) return 0;
|
|
663
698
|
if (wantsPro && candidateHasLegacyProVersion && !candidateSelectsDesiredVersion) return 0;
|
|
664
699
|
if (wantsPro && !candidateHasPro && !candidateSelectsDesiredVersion) return 0;
|
|
665
|
-
if (
|
|
700
|
+
if (
|
|
701
|
+
wantsInstant &&
|
|
702
|
+
!candidateHasInstant &&
|
|
703
|
+
!candidateOpensInstantSubmenu &&
|
|
704
|
+
!candidateSelectsConfiguredVersion
|
|
705
|
+
) return 0;
|
|
666
706
|
if (wantsThinking && candidateHasPro) return 0;
|
|
667
707
|
if (wantsThinking && !candidateHasThinking && !candidateSelectsDesiredVersion) return 0;
|
|
668
708
|
if (desiredVersion === '5-5' && normalizedText && !candidateGpt55VisibleAlias) {
|
|
@@ -701,10 +741,14 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
701
741
|
if (
|
|
702
742
|
desiredVersion &&
|
|
703
743
|
candidateTextVersion === desiredVersion &&
|
|
744
|
+
!candidateOpensInstantSubmenu &&
|
|
704
745
|
!(wantsThinking && desiredVersion === '5-5' && normalizedText === 'gpt 5 5')
|
|
705
746
|
) {
|
|
706
747
|
score += 1200;
|
|
707
748
|
}
|
|
749
|
+
if (candidateOpensInstantSubmenu) {
|
|
750
|
+
score += 300;
|
|
751
|
+
}
|
|
708
752
|
if (candidateOpensVersionSubmenu) {
|
|
709
753
|
score += 500;
|
|
710
754
|
}
|
|
@@ -809,6 +853,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
809
853
|
const currentButtonLabel = normalizeText(getButtonLabel());
|
|
810
854
|
return !labelHasProWord(currentButtonLabel) && !hasProComposerPill();
|
|
811
855
|
};
|
|
856
|
+
const openedSubmenuKeys = new Set();
|
|
857
|
+
const submenuKey = (normalizedText, testid) =>
|
|
858
|
+
normalizeText(testid ?? '') + '|' + normalizedText;
|
|
812
859
|
|
|
813
860
|
const findBestOption = () => {
|
|
814
861
|
// Walk through every menu item and keep whichever earns the highest score.
|
|
@@ -823,6 +870,10 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
823
870
|
const text = option.textContent ?? '';
|
|
824
871
|
const normalizedText = normalizeText(text);
|
|
825
872
|
const testid = option.getAttribute('data-testid') ?? '';
|
|
873
|
+
const optionSubmenuKey = submenuKey(normalizedText, testid);
|
|
874
|
+
if (isSubmenuOption(option, testid) && openedSubmenuKeys.has(optionSubmenuKey)) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
826
877
|
let score = scoreOption(normalizedText, testid, option);
|
|
827
878
|
if (score <= 0) {
|
|
828
879
|
continue;
|
|
@@ -832,7 +883,14 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
832
883
|
}
|
|
833
884
|
const label = getOptionLabel(option);
|
|
834
885
|
if (!bestMatch || score > bestMatch.score) {
|
|
835
|
-
bestMatch = {
|
|
886
|
+
bestMatch = {
|
|
887
|
+
node: option,
|
|
888
|
+
label,
|
|
889
|
+
score,
|
|
890
|
+
testid,
|
|
891
|
+
normalizedText,
|
|
892
|
+
submenuKey: optionSubmenuKey,
|
|
893
|
+
};
|
|
836
894
|
}
|
|
837
895
|
}
|
|
838
896
|
}
|
|
@@ -924,6 +982,13 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
924
982
|
const openDelay = () => new Promise((r) => setTimeout(r, INITIAL_WAIT_MS));
|
|
925
983
|
let initialized = false;
|
|
926
984
|
const attempt = async () => {
|
|
985
|
+
if (performance.now() - start > MAX_WAIT_MS) {
|
|
986
|
+
resolve({
|
|
987
|
+
status: 'option-not-found',
|
|
988
|
+
hint: { temporaryChat: detectTemporaryChat(), availableOptions: collectAvailableOptions() },
|
|
989
|
+
});
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
927
992
|
if (!initialized) {
|
|
928
993
|
initialized = true;
|
|
929
994
|
await openDelay();
|
|
@@ -946,6 +1011,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
946
1011
|
// Keep scanning once the submenu opens instead of treating the submenu click as a final switch.
|
|
947
1012
|
const isSubmenu = isSubmenuOption(match.node, match.testid);
|
|
948
1013
|
if (isSubmenu) {
|
|
1014
|
+
openedSubmenuKeys.add(match.submenuKey);
|
|
949
1015
|
openSubmenuOption(match.node);
|
|
950
1016
|
setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
|
|
951
1017
|
return;
|
|
@@ -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, deps?.attachmentTimeoutMs);
|
|
169
|
+
const clicked = await attemptSendButton(runtime, input, logger, deps?.attachmentNames, deps?.attachmentTimeoutMs);
|
|
170
170
|
if (!clicked) {
|
|
171
171
|
await input.dispatchKeyEvent({
|
|
172
172
|
type: "keyDown",
|
|
@@ -575,7 +575,7 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
575
575
|
export function buildAttachmentReadyExpressionForTest(attachmentNames) {
|
|
576
576
|
return buildAttachmentReadyExpression(attachmentNames);
|
|
577
577
|
}
|
|
578
|
-
async function attemptSendButton(Runtime, _logger, attachmentNames, attachmentTimeoutMs) {
|
|
578
|
+
async function attemptSendButton(Runtime, Input, _logger, attachmentNames, attachmentTimeoutMs) {
|
|
579
579
|
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
580
580
|
const script = `(() => {
|
|
581
581
|
${buildClickDispatcher()}
|
|
@@ -604,10 +604,15 @@ async function attemptSendButton(Runtime, _logger, attachmentNames, attachmentTi
|
|
|
604
604
|
candidates.push(...Array.from(document.querySelectorAll(selector)));
|
|
605
605
|
}
|
|
606
606
|
const button = candidates.find((node) => isVisible(node) && isEnabled(node)) || null;
|
|
607
|
-
if (!button) return 'missing';
|
|
608
|
-
|
|
607
|
+
if (!button) return { status: 'missing' };
|
|
608
|
+
button.scrollIntoView({ block: 'center', inline: 'center' });
|
|
609
|
+
const rect = button.getBoundingClientRect();
|
|
610
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
611
|
+
return { status: 'point', x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
|
|
612
|
+
}
|
|
613
|
+
// Last-resort fallback for unusual DOMs where the button is visible but has no useful rect.
|
|
609
614
|
dispatchClickSequence(button);
|
|
610
|
-
return 'clicked';
|
|
615
|
+
return { status: 'clicked' };
|
|
611
616
|
})()`;
|
|
612
617
|
// Give attachment-bearing submissions more headroom. ChatGPT's chip render can
|
|
613
618
|
// settle slowly for multi-file uploads, but plain text sends should keep the
|
|
@@ -626,10 +631,19 @@ async function attemptSendButton(Runtime, _logger, attachmentNames, attachmentTi
|
|
|
626
631
|
}
|
|
627
632
|
}
|
|
628
633
|
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
629
|
-
|
|
634
|
+
const value = result.value;
|
|
635
|
+
const status = typeof value === "string" ? value : value?.status;
|
|
636
|
+
if (status === "point" &&
|
|
637
|
+
typeof value === "object" &&
|
|
638
|
+
typeof value.x === "number" &&
|
|
639
|
+
typeof value.y === "number") {
|
|
640
|
+
await clickTrustedPoint(Runtime, Input, value.x, value.y);
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
if (status === "clicked") {
|
|
630
644
|
return true;
|
|
631
645
|
}
|
|
632
|
-
if (
|
|
646
|
+
if (status === "missing") {
|
|
633
647
|
break;
|
|
634
648
|
}
|
|
635
649
|
await delay(100);
|
|
@@ -644,6 +658,23 @@ async function attemptSendButton(Runtime, _logger, attachmentNames, attachmentTi
|
|
|
644
658
|
}
|
|
645
659
|
return false;
|
|
646
660
|
}
|
|
661
|
+
async function clickTrustedPoint(Runtime, Input, x, y) {
|
|
662
|
+
if (Input && typeof Input.dispatchMouseEvent === "function") {
|
|
663
|
+
await Input.dispatchMouseEvent({ type: "mouseMoved", x, y });
|
|
664
|
+
await Input.dispatchMouseEvent({ type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
665
|
+
await Input.dispatchMouseEvent({ type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
await Runtime.evaluate({
|
|
669
|
+
expression: `(() => {
|
|
670
|
+
const el = document.elementFromPoint(${JSON.stringify(x)}, ${JSON.stringify(y)});
|
|
671
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
672
|
+
el.click();
|
|
673
|
+
return true;
|
|
674
|
+
})()`,
|
|
675
|
+
returnByValue: true,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
647
678
|
function sendButtonTimeoutMs(attachmentNames, attachmentTimeoutMs) {
|
|
648
679
|
if (!Array.isArray(attachmentNames) || attachmentNames.length === 0) {
|
|
649
680
|
return 20_000;
|
|
@@ -755,9 +786,13 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
755
786
|
turnsCount: normalizedTurns.length,
|
|
756
787
|
};
|
|
757
788
|
})()`;
|
|
789
|
+
let lastProbe;
|
|
758
790
|
while (Date.now() < deadline) {
|
|
759
791
|
const { result } = await Runtime.evaluate({ expression: script, returnByValue: true });
|
|
760
792
|
const info = result.value;
|
|
793
|
+
if (info && typeof info === "object") {
|
|
794
|
+
lastProbe = info;
|
|
795
|
+
}
|
|
761
796
|
const turnsCount = result.value?.turnsCount;
|
|
762
797
|
const matchesPrompt = Boolean(info?.lastMatched || info?.userMatched || info?.prefixMatched);
|
|
763
798
|
const baselineUnknown = typeof info?.baseline === "number" ? info.baseline < 0 : baselineLiteral < 0;
|
|
@@ -772,13 +807,12 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
772
807
|
}
|
|
773
808
|
await delay(100);
|
|
774
809
|
}
|
|
810
|
+
const finalProbe = await Runtime.evaluate({ expression: script, returnByValue: true })
|
|
811
|
+
.then((res) => res?.result?.value)
|
|
812
|
+
.catch(() => undefined);
|
|
813
|
+
const probe = finalProbe && typeof finalProbe === "object" ? finalProbe : lastProbe;
|
|
775
814
|
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")}`);
|
|
815
|
+
logger(`Prompt commit check failed; latest state: ${probe ? JSON.stringify(probe) : "unavailable"}`);
|
|
782
816
|
await logDomFailure(Runtime, logger, "prompt-commit");
|
|
783
817
|
}
|
|
784
818
|
if (prompt.trim().length >= 50_000) {
|
|
@@ -789,7 +823,30 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
789
823
|
timeoutMs,
|
|
790
824
|
});
|
|
791
825
|
}
|
|
792
|
-
throw new
|
|
826
|
+
throw new BrowserAutomationError("Prompt did not appear in conversation before timeout (send may have failed)", {
|
|
827
|
+
stage: "submit-prompt",
|
|
828
|
+
code: "prompt-commit-timeout",
|
|
829
|
+
promptLength: prompt.trim().length,
|
|
830
|
+
timeoutMs,
|
|
831
|
+
commitProbe: probe ? summarizeCommitProbe(probe) : undefined,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
// Keep booleans/counts but replace free text with lengths so session metadata stays lean.
|
|
835
|
+
function summarizeCommitProbe(probe) {
|
|
836
|
+
return {
|
|
837
|
+
baseline: probe.baseline,
|
|
838
|
+
turnsCount: probe.turnsCount,
|
|
839
|
+
userMatched: probe.userMatched,
|
|
840
|
+
prefixMatched: probe.prefixMatched,
|
|
841
|
+
lastMatched: probe.lastMatched,
|
|
842
|
+
hasNewTurn: probe.hasNewTurn,
|
|
843
|
+
stopVisible: probe.stopVisible,
|
|
844
|
+
assistantVisible: probe.assistantVisible,
|
|
845
|
+
composerCleared: probe.composerCleared,
|
|
846
|
+
inConversation: probe.inConversation,
|
|
847
|
+
editorLength: typeof probe.editorValue === "string" ? probe.editorValue.length : undefined,
|
|
848
|
+
lastTurnLength: typeof probe.lastTurn === "string" ? probe.lastTurn.length : undefined,
|
|
849
|
+
};
|
|
793
850
|
}
|
|
794
851
|
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
795
852
|
export const __test__ = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatElapsed } from "../../oracle/format.js";
|
|
2
|
-
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR } from "../constants.js";
|
|
2
|
+
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, STOP_BUTTON_SELECTORS, } from "../constants.js";
|
|
3
3
|
const THINKING_STALE_HINT_MS = 10 * 60_000;
|
|
4
4
|
export function startThinkingStatusMonitor(Runtime, logger, options = {}) {
|
|
5
5
|
const intervalMs = resolveThinkingStatusInterval(options.intervalMs);
|
|
@@ -127,6 +127,7 @@ async function readThinkingStatus(Runtime) {
|
|
|
127
127
|
}
|
|
128
128
|
const SAFE_THINKING_STATUS_MESSAGES = new Set([
|
|
129
129
|
"active",
|
|
130
|
+
"response streaming",
|
|
130
131
|
"thinking sidecar active",
|
|
131
132
|
"thinking sidecar opened",
|
|
132
133
|
]);
|
|
@@ -157,13 +158,16 @@ function buildThinkingStatusExpression() {
|
|
|
157
158
|
'[aria-live="polite"]',
|
|
158
159
|
];
|
|
159
160
|
const keywords = ["pro thinking", "thinking", "reasoning"];
|
|
161
|
+
const stopSelector = STOP_BUTTON_SELECTORS.join(", ");
|
|
160
162
|
const selectorLiteral = JSON.stringify(selectors);
|
|
161
163
|
const keywordsLiteral = JSON.stringify(keywords);
|
|
164
|
+
const stopSelectorLiteral = JSON.stringify(stopSelector);
|
|
162
165
|
return `(async () => {
|
|
163
166
|
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
164
167
|
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
165
168
|
const selectors = ${selectorLiteral};
|
|
166
169
|
const keywords = ${keywordsLiteral};
|
|
170
|
+
const stopSelector = ${stopSelectorLiteral};
|
|
167
171
|
const normalize = (value) =>
|
|
168
172
|
String(value || '')
|
|
169
173
|
.normalize('NFD')
|
|
@@ -383,6 +387,20 @@ function buildThinkingStatusExpression() {
|
|
|
383
387
|
};
|
|
384
388
|
}
|
|
385
389
|
}
|
|
390
|
+
// Last-resort liveness fallback: selector drift can hide every thinking
|
|
391
|
+
// indicator while a response is still generating, and returning null here
|
|
392
|
+
// reads as "dead" downstream. The stop/interrupt control is a stable,
|
|
393
|
+
// language-independent signal that generation is active; it lives in the
|
|
394
|
+
// composer, so isComposerAdjacent must not filter it.
|
|
395
|
+
const stopVisible = Array.from(document.querySelectorAll(stopSelector)).some((node) =>
|
|
396
|
+
isVisible(node),
|
|
397
|
+
);
|
|
398
|
+
if (stopVisible) {
|
|
399
|
+
return {
|
|
400
|
+
message: 'response streaming',
|
|
401
|
+
source: 'inline',
|
|
402
|
+
};
|
|
403
|
+
}
|
|
386
404
|
return null;
|
|
387
405
|
})()`;
|
|
388
406
|
}
|
|
@@ -578,9 +578,62 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
578
578
|
}
|
|
579
579
|
return null;
|
|
580
580
|
};
|
|
581
|
-
|
|
582
|
-
|
|
581
|
+
let composerEffortPill = findComposerEffortPill();
|
|
582
|
+
let modelBtn = findModelButton();
|
|
583
|
+
const modelKindFromLegacyTrailing = (trailing) => {
|
|
584
|
+
const row = trailing.closest?.(
|
|
585
|
+
'[role="menuitem"], [role="menuitemradio"], [data-radix-collection-item]',
|
|
586
|
+
);
|
|
587
|
+
const idText = normalize(
|
|
588
|
+
(row?.getAttribute?.('data-testid') ?? '') + ' ' +
|
|
589
|
+
(trailing.getAttribute?.('data-testid') ?? '')
|
|
590
|
+
);
|
|
591
|
+
if (!idText.includes('model switcher')) return null;
|
|
592
|
+
const modelPart = normalize(idText.replace(/\\bthinking effort\\b.*$/, ''));
|
|
593
|
+
if (hasToken(modelPart, 'pro')) return 'pro';
|
|
594
|
+
if (hasToken(modelPart, 'thinking')) return 'thinking';
|
|
595
|
+
if (hasToken(modelPart, 'instant')) return 'instant';
|
|
596
|
+
return null;
|
|
597
|
+
};
|
|
598
|
+
const legacyEffortOwnerIsReady = () => {
|
|
599
|
+
if (
|
|
600
|
+
TARGET_MODEL_KIND === 'pro' &&
|
|
601
|
+
TARGET_LEVEL === 'extended' &&
|
|
602
|
+
isVisible(document.querySelector(INTELLIGENCE_MENU_SELECTOR))
|
|
603
|
+
) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
const expectedKind = TARGET_MODEL_KIND || modelKindFromNode(modelBtn);
|
|
607
|
+
return Boolean(
|
|
608
|
+
expectedKind &&
|
|
609
|
+
findTrailingButtons().some(
|
|
610
|
+
(button) => isVisible(button) && modelKindFromLegacyTrailing(button) === expectedKind,
|
|
611
|
+
),
|
|
612
|
+
);
|
|
613
|
+
};
|
|
614
|
+
let attemptedModelButton =
|
|
615
|
+
modelBtn?.getAttribute?.('aria-expanded') === 'true' ? modelBtn : null;
|
|
616
|
+
const effortOwnerDeadline = performance.now() + MAX_WAIT_MS;
|
|
617
|
+
while (!composerEffortPill && performance.now() < effortOwnerDeadline) {
|
|
618
|
+
if (
|
|
619
|
+
modelBtn &&
|
|
620
|
+
attemptedModelButton !== modelBtn &&
|
|
621
|
+
modelBtn.getAttribute?.('aria-expanded') !== 'true'
|
|
622
|
+
) {
|
|
623
|
+
dispatchClickSequence(modelBtn);
|
|
624
|
+
attemptedModelButton = modelBtn;
|
|
625
|
+
await sleep(INITIAL_WAIT_MS);
|
|
626
|
+
}
|
|
627
|
+
if (modelBtn && legacyEffortOwnerIsReady()) break;
|
|
628
|
+
await sleep(100);
|
|
629
|
+
composerEffortPill = findComposerEffortPill();
|
|
630
|
+
modelBtn = findModelButton();
|
|
631
|
+
if (modelBtn?.getAttribute?.('aria-expanded') === 'true') {
|
|
632
|
+
attemptedModelButton = modelBtn;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
583
635
|
if (composerEffortPill) {
|
|
636
|
+
if (attemptedModelButton && attemptedModelButton !== composerEffortPill) closeOpenMenus();
|
|
584
637
|
const composerModelKind = TARGET_MODEL_KIND || modelKindFromNode(composerEffortPill);
|
|
585
638
|
if (composerEffortPill.getAttribute?.('aria-expanded') !== 'true') {
|
|
586
639
|
dispatchClickSequence(composerEffortPill);
|
|
@@ -646,22 +699,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
646
699
|
(trailing.getAttribute?.('data-testid') ?? '')
|
|
647
700
|
);
|
|
648
701
|
};
|
|
649
|
-
const
|
|
650
|
-
const row = rowForTrailing(trailing) || findEffortRow(trailing);
|
|
651
|
-
return normalize(
|
|
652
|
-
(row?.getAttribute?.('data-testid') ?? '') + ' ' +
|
|
653
|
-
(trailing.getAttribute?.('data-testid') ?? '')
|
|
654
|
-
);
|
|
655
|
-
};
|
|
656
|
-
const modelKindFromTrailing = (trailing) => {
|
|
657
|
-
const idText = testIdTextForTrailing(trailing);
|
|
658
|
-
if (!idText.includes('model switcher')) return null;
|
|
659
|
-
const modelPart = normalize(idText.replace(/\\bthinking effort\\b.*$/, ''));
|
|
660
|
-
if (hasToken(modelPart, 'pro')) return 'pro';
|
|
661
|
-
if (hasToken(modelPart, 'thinking')) return 'thinking';
|
|
662
|
-
if (hasToken(modelPart, 'instant')) return 'instant';
|
|
663
|
-
return null;
|
|
664
|
-
};
|
|
702
|
+
const modelKindFromTrailing = modelKindFromLegacyTrailing;
|
|
665
703
|
const trailingMatchesTargetModelKind = (trailing) => {
|
|
666
704
|
if (!TARGET_MODEL_KIND) return false;
|
|
667
705
|
const idKind = modelKindFromTrailing(trailing);
|
|
@@ -698,12 +736,19 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
698
736
|
return null;
|
|
699
737
|
};
|
|
700
738
|
|
|
701
|
-
const
|
|
739
|
+
const modelButtonDeadline = performance.now() + MAX_WAIT_MS;
|
|
740
|
+
while (!modelBtn && performance.now() < modelButtonDeadline) {
|
|
741
|
+
await sleep(100);
|
|
742
|
+
modelBtn = findModelButton();
|
|
743
|
+
}
|
|
702
744
|
if (!modelBtn) {
|
|
703
745
|
return failure('chip-not-found');
|
|
704
746
|
}
|
|
705
747
|
// Open model menu (idempotent — leaves it open if already open).
|
|
706
|
-
if (
|
|
748
|
+
if (
|
|
749
|
+
modelBtn.getAttribute('aria-expanded') !== 'true' &&
|
|
750
|
+
!legacyEffortOwnerIsReady()
|
|
751
|
+
) {
|
|
707
752
|
dispatchClickSequence(modelBtn);
|
|
708
753
|
await sleep(INITIAL_WAIT_MS);
|
|
709
754
|
}
|