codex-chatgpt-control 0.3.0-alpha.1 → 0.5.1-alpha.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/CHANGELOG.md +17 -0
- package/README.md +42 -2
- package/contracts/v1/fixtures/backend-capabilities.json +10 -0
- package/contracts/v1/fixtures/command-descriptors.json +265 -8
- package/contracts/v1/fixtures/describe-runner-run.json +5 -2
- package/contracts/v1/fixtures/doctor-scenario-preflight.json +34 -2
- package/contracts/v1/fixtures/help-root.json +1 -1
- package/contracts/v1/fixtures/output-json-parse-success.json +9 -0
- package/contracts/v1/fixtures/run-timeout-partial.json +15 -4
- package/contracts/v1/fixtures/stream-in-progress.ndjson +3 -0
- package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
- package/contracts/v1/fixtures/surface-chat-legacy.json +35 -0
- package/contracts/v1/fixtures/surface-chat-simplified.json +37 -0
- package/contracts/v1/fixtures/surface-sidebar-false-positive.json +29 -0
- package/contracts/v1/fixtures/surface-work-advanced.json +45 -0
- package/contracts/v1/fixtures/surface-work-basic.json +38 -0
- package/contracts/v1/fixtures/workflow-ask-success.json +8 -0
- package/contracts/v1/manifest.json +32 -1
- package/contracts/v1/parity-suite.json +124 -0
- package/contracts/v1/schemas/backend-request.schema.json +15 -0
- package/contracts/v1/schemas/manifest.schema.json +6 -3
- package/contracts/v1/schemas/surface-profile.schema.json +166 -0
- package/dist/codex-chatgpt-control-backend.mjs +3475 -367
- package/dist/codex-chatgpt-control.bundle.mjs +5249 -2102
- package/dist/src/backend/client.d.ts +26 -1
- package/dist/src/backend/client.js +23 -1
- package/dist/src/backend/protocol.d.ts +1 -1
- package/dist/src/backend/protocol.js +10 -0
- package/dist/src/backend/session.js +20 -0
- package/dist/src/browser/attach.d.ts +1 -0
- package/dist/src/browser/attach.js +3 -3
- package/dist/src/browser/downloads.d.ts +1 -1
- package/dist/src/browser/downloads.js +13 -4
- package/dist/src/browser/page-state.js +5 -2
- package/dist/src/client.d.ts +31 -1
- package/dist/src/client.js +69 -14
- package/dist/src/commands/artifacts.js +1 -7
- package/dist/src/commands/configuration.d.ts +15 -0
- package/dist/src/commands/configuration.js +897 -0
- package/dist/src/commands/doctor.js +38 -1
- package/dist/src/commands/experience.d.ts +13 -0
- package/dist/src/commands/experience.js +421 -0
- package/dist/src/commands/files.js +160 -10
- package/dist/src/commands/messages.d.ts +2 -1
- package/dist/src/commands/messages.js +138 -16
- package/dist/src/commands/modes.js +28 -14
- package/dist/src/commands/registry.js +74 -7
- package/dist/src/commands/response-actions.js +1 -7
- package/dist/src/commands/sequence.js +24 -1
- package/dist/src/commands/session.d.ts +2 -0
- package/dist/src/commands/session.js +50 -1
- package/dist/src/commands/threads.js +1 -7
- package/dist/src/commands/work.d.ts +6 -0
- package/dist/src/commands/work.js +338 -0
- package/dist/src/dom/locale/am.d.ts +15 -1
- package/dist/src/dom/locale/am.js +15 -1
- package/dist/src/dom/locale/ar.d.ts +15 -1
- package/dist/src/dom/locale/ar.js +18 -4
- package/dist/src/dom/locale/bg.d.ts +15 -1
- package/dist/src/dom/locale/bg.js +18 -4
- package/dist/src/dom/locale/bn.d.ts +15 -1
- package/dist/src/dom/locale/bn.js +15 -1
- package/dist/src/dom/locale/bs.d.ts +14 -1
- package/dist/src/dom/locale/bs.js +18 -5
- package/dist/src/dom/locale/ca.d.ts +14 -1
- package/dist/src/dom/locale/ca.js +19 -6
- package/dist/src/dom/locale/cs.d.ts +14 -1
- package/dist/src/dom/locale/cs.js +14 -1
- package/dist/src/dom/locale/da.d.ts +14 -1
- package/dist/src/dom/locale/da.js +15 -2
- package/dist/src/dom/locale/de.d.ts +16 -3
- package/dist/src/dom/locale/de.js +18 -5
- package/dist/src/dom/locale/el.d.ts +15 -1
- package/dist/src/dom/locale/el.js +18 -4
- package/dist/src/dom/locale/en.d.ts +27 -2
- package/dist/src/dom/locale/en.js +28 -2
- package/dist/src/dom/locale/es-419.d.ts +15 -1
- package/dist/src/dom/locale/es-419.js +15 -1
- package/dist/src/dom/locale/es-ES.d.ts +15 -1
- package/dist/src/dom/locale/es-ES.js +15 -1
- package/dist/src/dom/locale/et.d.ts +14 -1
- package/dist/src/dom/locale/et.js +14 -1
- package/dist/src/dom/locale/fa.d.ts +14 -1
- package/dist/src/dom/locale/fa.js +16 -3
- package/dist/src/dom/locale/fi.d.ts +15 -1
- package/dist/src/dom/locale/fi.js +15 -1
- package/dist/src/dom/locale/fr-CA.d.ts +13 -1
- package/dist/src/dom/locale/fr-CA.js +13 -1
- package/dist/src/dom/locale/fr-FR.d.ts +16 -3
- package/dist/src/dom/locale/fr-FR.js +20 -7
- package/dist/src/dom/locale/gu.d.ts +15 -1
- package/dist/src/dom/locale/gu.js +19 -5
- package/dist/src/dom/locale/hi.d.ts +15 -1
- package/dist/src/dom/locale/hi.js +17 -3
- package/dist/src/dom/locale/hr.d.ts +14 -1
- package/dist/src/dom/locale/hr.js +17 -4
- package/dist/src/dom/locale/hu.d.ts +15 -1
- package/dist/src/dom/locale/hu.js +18 -4
- package/dist/src/dom/locale/hy.d.ts +15 -1
- package/dist/src/dom/locale/hy.js +18 -4
- package/dist/src/dom/locale/id.d.ts +14 -1
- package/dist/src/dom/locale/id.js +16 -3
- package/dist/src/dom/locale/index.d.ts +17 -2
- package/dist/src/dom/locale/index.js +65 -0
- package/dist/src/dom/locale/is.d.ts +14 -1
- package/dist/src/dom/locale/is.js +14 -1
- package/dist/src/dom/locale/it.d.ts +13 -1
- package/dist/src/dom/locale/it.js +17 -5
- package/dist/src/dom/locale/ja.d.ts +15 -1
- package/dist/src/dom/locale/ja.js +19 -5
- package/dist/src/dom/locale/ka.d.ts +15 -1
- package/dist/src/dom/locale/ka.js +15 -1
- package/dist/src/dom/locale/kk.d.ts +15 -1
- package/dist/src/dom/locale/kk.js +17 -3
- package/dist/src/dom/locale/kn.d.ts +15 -1
- package/dist/src/dom/locale/kn.js +19 -5
- package/dist/src/dom/locale/ko.d.ts +13 -1
- package/dist/src/dom/locale/ko.js +13 -1
- package/dist/src/dom/locale/lt.d.ts +15 -1
- package/dist/src/dom/locale/lt.js +17 -3
- package/dist/src/dom/locale/lv.d.ts +15 -1
- package/dist/src/dom/locale/lv.js +15 -1
- package/dist/src/dom/locale/mk.d.ts +15 -1
- package/dist/src/dom/locale/mk.js +19 -5
- package/dist/src/dom/locale/ml.d.ts +15 -1
- package/dist/src/dom/locale/ml.js +17 -3
- package/dist/src/dom/locale/mn.d.ts +15 -1
- package/dist/src/dom/locale/mn.js +15 -1
- package/dist/src/dom/locale/mr.d.ts +15 -1
- package/dist/src/dom/locale/mr.js +17 -3
- package/dist/src/dom/locale/ms.d.ts +13 -1
- package/dist/src/dom/locale/ms.js +13 -1
- package/dist/src/dom/locale/my.d.ts +15 -1
- package/dist/src/dom/locale/my.js +17 -3
- package/dist/src/dom/locale/nb.d.ts +14 -1
- package/dist/src/dom/locale/nb.js +14 -1
- package/dist/src/dom/locale/nl.d.ts +14 -1
- package/dist/src/dom/locale/nl.js +16 -3
- package/dist/src/dom/locale/pa.d.ts +15 -1
- package/dist/src/dom/locale/pa.js +19 -5
- package/dist/src/dom/locale/pl.d.ts +14 -1
- package/dist/src/dom/locale/pl.js +16 -3
- package/dist/src/dom/locale/pt-BR.d.ts +15 -1
- package/dist/src/dom/locale/pt-BR.js +17 -3
- package/dist/src/dom/locale/pt-PT.d.ts +15 -1
- package/dist/src/dom/locale/pt-PT.js +19 -5
- package/dist/src/dom/locale/ro.d.ts +13 -1
- package/dist/src/dom/locale/ro.js +16 -4
- package/dist/src/dom/locale/ru.d.ts +17 -1
- package/dist/src/dom/locale/ru.js +18 -2
- package/dist/src/dom/locale/sk.d.ts +14 -1
- package/dist/src/dom/locale/sk.js +16 -3
- package/dist/src/dom/locale/sl.d.ts +14 -1
- package/dist/src/dom/locale/sl.js +17 -4
- package/dist/src/dom/locale/so.d.ts +15 -1
- package/dist/src/dom/locale/so.js +17 -3
- package/dist/src/dom/locale/sq.d.ts +13 -1
- package/dist/src/dom/locale/sq.js +17 -5
- package/dist/src/dom/locale/sr.d.ts +17 -1
- package/dist/src/dom/locale/sr.js +18 -2
- package/dist/src/dom/locale/sv.d.ts +14 -1
- package/dist/src/dom/locale/sv.js +14 -1
- package/dist/src/dom/locale/sw.d.ts +15 -1
- package/dist/src/dom/locale/sw.js +15 -1
- package/dist/src/dom/locale/ta.d.ts +15 -1
- package/dist/src/dom/locale/ta.js +19 -5
- package/dist/src/dom/locale/te.d.ts +15 -1
- package/dist/src/dom/locale/te.js +19 -5
- package/dist/src/dom/locale/th.d.ts +7 -1
- package/dist/src/dom/locale/th.js +7 -1
- package/dist/src/dom/locale/tl.d.ts +11 -5
- package/dist/src/dom/locale/tl.js +11 -5
- package/dist/src/dom/locale/tr.d.ts +14 -1
- package/dist/src/dom/locale/tr.js +16 -3
- package/dist/src/dom/locale/types.d.ts +18 -1
- package/dist/src/dom/locale/uk.d.ts +15 -1
- package/dist/src/dom/locale/uk.js +17 -3
- package/dist/src/dom/locale/ur.d.ts +15 -1
- package/dist/src/dom/locale/ur.js +18 -4
- package/dist/src/dom/locale/vi.d.ts +15 -1
- package/dist/src/dom/locale/vi.js +18 -4
- package/dist/src/dom/locale/zh-HK.d.ts +15 -1
- package/dist/src/dom/locale/zh-HK.js +17 -3
- package/dist/src/dom/locale/zh-Hans.d.ts +15 -1
- package/dist/src/dom/locale/zh-Hans.js +19 -5
- package/dist/src/dom/locale/zh-TW.d.ts +15 -1
- package/dist/src/dom/locale/zh-TW.js +17 -3
- package/dist/src/dom/menus.js +26 -2
- package/dist/src/dom/selectors.js +6 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/platform/local-paths.d.ts +1 -0
- package/dist/src/platform/local-paths.js +7 -3
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +16 -1
- package/dist/src/runner/result.js +129 -4
- package/dist/src/runner/stream.d.ts +1 -1
- package/dist/src/runner/stream.js +6 -0
- package/dist/src/runner/types.d.ts +35 -1
- package/dist/src/safety/risk.d.ts +10 -0
- package/dist/src/safety/risk.js +10 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +15 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.js +225 -7
- package/dist/src/scripts/capture-intelligence-locales.d.ts +8 -0
- package/dist/src/scripts/capture-intelligence-locales.js +663 -6
- package/dist/src/scripts/capture-surface-profile.d.ts +25 -0
- package/dist/src/scripts/capture-surface-profile.js +354 -0
- package/dist/src/scripts/live-smoke/scenarios.d.ts +2 -0
- package/dist/src/scripts/live-smoke/scenarios.js +347 -5
- package/dist/src/scripts/live-smoke.js +2 -0
- package/dist/src/scripts/locale-capture/surface-graph.d.ts +26 -0
- package/dist/src/scripts/locale-capture/surface-graph.js +43 -0
- package/dist/src/scripts/release-canary-module.d.ts +20 -0
- package/dist/src/scripts/release-canary-module.js +86 -0
- package/dist/src/types.d.ts +248 -2
- package/package.json +9 -3
- package/references/2026-07-16-chat-work-surfaces.md +100 -0
- package/references/agents-runner.md +13 -2
- package/references/backend-protocol.md +26 -9
- package/references/language-coverage.md +51 -3
- package/references/localization.md +52 -6
- package/references/python-parity.md +12 -2
- package/references/responses-adapter.md +12 -1
- package/references/troubleshooting.md +38 -4
|
@@ -7,10 +7,10 @@ import { waitForDownloadFromClick } from "../browser/downloads.js";
|
|
|
7
7
|
import { resultError, resultOk } from "../errors.js";
|
|
8
8
|
import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.js";
|
|
9
9
|
import { localeLabels } from "../dom/locale-labels.js";
|
|
10
|
-
import { basenameForHostPath, isHostAbsolutePath, resolveForHostPath } from "../platform/local-paths.js";
|
|
10
|
+
import { basenameForHostPath, currentHostPathPlatform, isHostAbsolutePath, resolveForHostPath } from "../platform/local-paths.js";
|
|
11
11
|
import { contextFromPage } from "./context.js";
|
|
12
|
-
import {
|
|
13
|
-
import { localGuardTimeout } from "./timeouts.js";
|
|
12
|
+
import { ensurePage } from "./session.js";
|
|
13
|
+
import { localGuardTimeout, withTimeout } from "./timeouts.js";
|
|
14
14
|
const CODEX_UPLOAD_PERMISSION_FIX = "Codex Settings > Computer Use > Chrome > Permissions > Uploads: set to Always allow, or add chatgpt.com to the allowed upload domains.";
|
|
15
15
|
const CHROME_FILE_URL_PERMISSION_FIX = "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs.";
|
|
16
16
|
const DEFAULT_MAX_BYTES_PER_FILE = 512 * 1024 * 1024;
|
|
@@ -251,7 +251,7 @@ async function fileMetadata(absolute, bytes, includeHash = false) {
|
|
|
251
251
|
return metadata;
|
|
252
252
|
}
|
|
253
253
|
function extensionForHostPath(value) {
|
|
254
|
-
return
|
|
254
|
+
return currentHostPathPlatform() === "win32"
|
|
255
255
|
? path.win32.extname(value).toLowerCase()
|
|
256
256
|
: path.posix.extname(value).toLowerCase();
|
|
257
257
|
}
|
|
@@ -526,6 +526,24 @@ export async function downloadLatestFile(env, args) {
|
|
|
526
526
|
}
|
|
527
527
|
const page = env.page;
|
|
528
528
|
try {
|
|
529
|
+
const generatedFileDownload = await tryGeneratedFilePreviewDownload(page, args);
|
|
530
|
+
if (generatedFileDownload !== undefined) {
|
|
531
|
+
return generatedFileDownload;
|
|
532
|
+
}
|
|
533
|
+
if (args.filenamePattern !== undefined) {
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
status: "unsupported",
|
|
537
|
+
warnings: [],
|
|
538
|
+
blocker: {
|
|
539
|
+
kind: "download_unavailable",
|
|
540
|
+
code: "download_filename_not_found",
|
|
541
|
+
message: `No visible ChatGPT file affordance matched filenamePattern ${JSON.stringify(args.filenamePattern)}.`,
|
|
542
|
+
resumable: true
|
|
543
|
+
},
|
|
544
|
+
context: await contextFromPage(page)
|
|
545
|
+
};
|
|
546
|
+
}
|
|
529
547
|
const controls = requiredLocator(page, cssSelectors.downloadControls);
|
|
530
548
|
let count;
|
|
531
549
|
try {
|
|
@@ -571,6 +589,144 @@ export async function downloadLatestFile(env, args) {
|
|
|
571
589
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
572
590
|
}
|
|
573
591
|
}
|
|
592
|
+
async function tryGeneratedFilePreviewDownload(page, args) {
|
|
593
|
+
const timeoutMs = args.timeoutMs ?? 120000;
|
|
594
|
+
const candidates = await inspectGeneratedFileAffordances(page, localGuardTimeout(timeoutMs, 5000));
|
|
595
|
+
const selected = selectGeneratedFileAffordance(candidates, args);
|
|
596
|
+
if (selected === undefined)
|
|
597
|
+
return undefined;
|
|
598
|
+
try {
|
|
599
|
+
const assistantMessages = requiredLocator(page, cssSelectors.assistantMessages);
|
|
600
|
+
const assistantCount = await locatorCountWithTimeout(assistantMessages, localGuardTimeout(timeoutMs, 5000), "generated_file_assistant_count_timeout");
|
|
601
|
+
if (selected.assistantIndex < 0 || selected.assistantIndex >= assistantCount) {
|
|
602
|
+
throw new Error("The selected generated-file assistant turn is no longer present.");
|
|
603
|
+
}
|
|
604
|
+
const assistant = assistantMessages.nth?.(selected.assistantIndex) ?? assistantMessages;
|
|
605
|
+
const role = selected.tag === "button" ? "button" : "link";
|
|
606
|
+
const affordance = assistant.getByRole?.(role, { name: selected.filename, exact: true })
|
|
607
|
+
?? assistant.locator?.(`${selected.tag}[aria-label="${escapeCssAttribute(selected.filename)}"]`);
|
|
608
|
+
const affordanceCount = await locatorCountWithTimeout(affordance, localGuardTimeout(timeoutMs, 5000), "generated_file_affordance_count_timeout");
|
|
609
|
+
if (affordance === undefined || affordanceCount !== 1 || typeof affordance.click !== "function") {
|
|
610
|
+
throw new Error(`Expected one clickable generated-file affordance for ${selected.filename}, found ${affordanceCount}.`);
|
|
611
|
+
}
|
|
612
|
+
if (selected.tag === "a") {
|
|
613
|
+
const downloaded = await waitForDownloadFromClick(page, () => affordance.click({ timeoutMs: localGuardTimeout(timeoutMs, 10000) }), args.destDir, timeoutMs, selected.filename);
|
|
614
|
+
return resultOk(downloaded, await contextFromPage(page));
|
|
615
|
+
}
|
|
616
|
+
await affordance.click({ timeoutMs: localGuardTimeout(timeoutMs, 10000) });
|
|
617
|
+
const preview = requiredLocator(page, `section[aria-label="${escapeCssAttribute(selected.filename)}"]`);
|
|
618
|
+
const download = await waitForPreviewDownloadControl(page, preview, timeoutMs);
|
|
619
|
+
if (download === undefined) {
|
|
620
|
+
throw new Error(`The artifact preview for ${selected.filename} did not expose a visible Download control.`);
|
|
621
|
+
}
|
|
622
|
+
const downloaded = await waitForDownloadFromClick(page, async () => download.click?.({ timeoutMs: localGuardTimeout(timeoutMs, 10000) }), args.destDir, timeoutMs, selected.filename);
|
|
623
|
+
return resultOk(downloaded, await contextFromPage(page));
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async function inspectGeneratedFileAffordances(page, timeoutMs) {
|
|
630
|
+
if (typeof page.evaluate === "function") {
|
|
631
|
+
const fromDom = await withTimeout(page.evaluate(() => {
|
|
632
|
+
const visible = (element) => {
|
|
633
|
+
let current = element;
|
|
634
|
+
while (current !== null) {
|
|
635
|
+
const html = current;
|
|
636
|
+
const style = window.getComputedStyle(html);
|
|
637
|
+
const rect = html.getBoundingClientRect();
|
|
638
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity || "1") <= 0)
|
|
639
|
+
return false;
|
|
640
|
+
if (current === element && (rect.width <= 0 || rect.height <= 0))
|
|
641
|
+
return false;
|
|
642
|
+
current = current.parentElement;
|
|
643
|
+
}
|
|
644
|
+
return true;
|
|
645
|
+
};
|
|
646
|
+
const fileLike = (value) => /^[^\\/\r\n]{1,255}\.[a-z0-9][a-z0-9._-]{0,15}$/i.test(value);
|
|
647
|
+
const assistants = Array.from(document.querySelectorAll("[data-message-author-role='assistant']"));
|
|
648
|
+
return assistants.flatMap((assistant, assistantIndex) => Array.from(assistant.querySelectorAll("button[aria-label], a[download], a[href*='/backend-api/files/']"))
|
|
649
|
+
.filter(visible)
|
|
650
|
+
.map(element => ({
|
|
651
|
+
assistantIndex,
|
|
652
|
+
filename: (element.getAttribute("aria-label") ?? element.textContent ?? "").trim(),
|
|
653
|
+
tag: element.tagName.toLocaleLowerCase(),
|
|
654
|
+
text: (element.textContent ?? "").trim()
|
|
655
|
+
}))
|
|
656
|
+
.filter(item => (item.tag === "button" || item.tag === "a") && fileLike(item.filename) && item.filename === item.text)
|
|
657
|
+
.map(({ assistantIndex, filename, tag }) => ({ assistantIndex, filename, tag })));
|
|
658
|
+
}), timeoutMs, "Timed out while inspecting generated-file buttons.").catch(() => undefined);
|
|
659
|
+
if (Array.isArray(fromDom))
|
|
660
|
+
return fromDom;
|
|
661
|
+
}
|
|
662
|
+
if (typeof page.content !== "function")
|
|
663
|
+
return [];
|
|
664
|
+
const html = await withTimeout(page.content(), timeoutMs, "Timed out while reading generated-file button markup.").catch(() => "");
|
|
665
|
+
const candidates = [];
|
|
666
|
+
const buttonPattern = /<(button|a)\b[^>]*\baria-label=(['"])(.*?)\2[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
667
|
+
let match;
|
|
668
|
+
while ((match = buttonPattern.exec(html)) !== null) {
|
|
669
|
+
const filename = decodeBasicHtml(match[3] ?? "").trim();
|
|
670
|
+
const text = decodeBasicHtml((match[4] ?? "").replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
|
|
671
|
+
if (/^[^\\/\r\n]{1,255}\.[a-z0-9][a-z0-9._-]{0,15}$/i.test(filename) && filename === text) {
|
|
672
|
+
candidates.push({ assistantIndex: 0, filename, tag: (match[1] ?? "button").toLocaleLowerCase() });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return candidates;
|
|
676
|
+
}
|
|
677
|
+
function selectGeneratedFileAffordance(candidates, args) {
|
|
678
|
+
let scoped = candidates;
|
|
679
|
+
const from = args.from;
|
|
680
|
+
if (typeof from === "object" && from !== null) {
|
|
681
|
+
scoped = scoped.filter(candidate => candidate.assistantIndex === from.assistantIndex);
|
|
682
|
+
}
|
|
683
|
+
else if (from !== "visible_conversation") {
|
|
684
|
+
const latestAssistant = Math.max(-1, ...scoped.map(candidate => candidate.assistantIndex));
|
|
685
|
+
scoped = scoped.filter(candidate => candidate.assistantIndex === latestAssistant);
|
|
686
|
+
}
|
|
687
|
+
if (args.filenamePattern !== undefined) {
|
|
688
|
+
scoped = scoped.filter(candidate => filenameMatches(candidate.filename, args.filenamePattern));
|
|
689
|
+
}
|
|
690
|
+
return scoped.at(-1);
|
|
691
|
+
}
|
|
692
|
+
function filenameMatches(filename, pattern) {
|
|
693
|
+
try {
|
|
694
|
+
return new RegExp(pattern, "i").test(filename);
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
return filename.toLocaleLowerCase().includes(pattern.toLocaleLowerCase());
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async function waitForPreviewDownloadControl(page, preview, timeoutMs) {
|
|
701
|
+
const deadline = Date.now() + Math.min(timeoutMs, 15000);
|
|
702
|
+
while (Date.now() < deadline) {
|
|
703
|
+
for (const label of localeLabels.download) {
|
|
704
|
+
const control = preview.getByRole?.("button", { name: label, exact: true })
|
|
705
|
+
?? preview.locator?.(`button[aria-label="${escapeCssAttribute(label)}"]`);
|
|
706
|
+
if (await locatorCountWithTimeout(control, localGuardTimeout(timeoutMs, 2000), "artifact_preview_download_count_timeout") === 1) {
|
|
707
|
+
return control;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (typeof page.waitForTimeout === "function") {
|
|
711
|
+
await page.waitForTimeout(100);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return undefined;
|
|
718
|
+
}
|
|
719
|
+
function escapeCssAttribute(value) {
|
|
720
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\r\n]/g, " ");
|
|
721
|
+
}
|
|
722
|
+
function decodeBasicHtml(value) {
|
|
723
|
+
return value
|
|
724
|
+
.replace(/"/gi, '"')
|
|
725
|
+
.replace(/'|'/gi, "'")
|
|
726
|
+
.replace(/</gi, "<")
|
|
727
|
+
.replace(/>/gi, ">")
|
|
728
|
+
.replace(/&/gi, "&");
|
|
729
|
+
}
|
|
574
730
|
async function setHiddenFileInput(page, files) {
|
|
575
731
|
if (page === undefined) {
|
|
576
732
|
throw new Error("No active page is available for file upload.");
|
|
@@ -610,12 +766,6 @@ async function readBrowserInputDiagnostic(page) {
|
|
|
610
766
|
};
|
|
611
767
|
});
|
|
612
768
|
}
|
|
613
|
-
async function ensurePage(env) {
|
|
614
|
-
if (env.page !== undefined) {
|
|
615
|
-
return resultOk({}, await contextFromPage(env.page));
|
|
616
|
-
}
|
|
617
|
-
return bootstrap(env, { preferExistingTab: true });
|
|
618
|
-
}
|
|
619
769
|
async function setFilesViaDomDataTransfer(page, files) {
|
|
620
770
|
const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
621
771
|
const maxInlineBytes = 25 * 1024 * 1024;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AssistantGenerationState } from "../dom/generation-state.js";
|
|
2
|
-
import type { AskArgs, AskReadData, CommandResult, ComposeArgs, ComposeData, ReadLatestArgs, ReadLatestData, RuntimeEnv, SubmitArgs, SubmitData, WaitAndReadArgs, WaitArgs, WaitData } from "../types.js";
|
|
2
|
+
import type { AskArgs, AskReadData, CommandResult, ComposeArgs, ComposeData, MessageStatusArgs, MessageStatusData, ReadLatestArgs, ReadLatestData, RuntimeEnv, SubmitArgs, SubmitData, WaitAndReadArgs, WaitArgs, WaitData } from "../types.js";
|
|
3
3
|
export type CompletionSnapshot = {
|
|
4
4
|
textStableForMs: number;
|
|
5
5
|
stableMs: number;
|
|
@@ -12,6 +12,7 @@ export declare function composeMessage(env: RuntimeEnv, args: ComposeArgs): Prom
|
|
|
12
12
|
export declare function submitMessage(env: RuntimeEnv, args?: SubmitArgs): Promise<CommandResult<SubmitData>>;
|
|
13
13
|
export declare function waitForMessage(env: RuntimeEnv, args?: WaitArgs): Promise<CommandResult<WaitData>>;
|
|
14
14
|
export declare function readLatest(env: RuntimeEnv, args?: ReadLatestArgs): Promise<CommandResult<ReadLatestData>>;
|
|
15
|
+
export declare function messageStatus(env: RuntimeEnv, args?: MessageStatusArgs): Promise<CommandResult<MessageStatusData>>;
|
|
15
16
|
export declare function askMessage(env: RuntimeEnv, args: AskArgs): Promise<CommandResult<AskReadData>>;
|
|
16
17
|
export declare function waitAndRead(env: RuntimeEnv, args?: WaitAndReadArgs): Promise<CommandResult<AskReadData>>;
|
|
17
18
|
export declare function submittedUserTurnMatches(actual: string | undefined, wanted: string | undefined): boolean;
|
|
@@ -10,7 +10,7 @@ import { contextFromPage } from "./context.js";
|
|
|
10
10
|
import { createDeadline } from "./deadline.js";
|
|
11
11
|
import { withCommandOutputText } from "./output.js";
|
|
12
12
|
import { createSingleFlightProbe } from "./probes.js";
|
|
13
|
-
import {
|
|
13
|
+
import { ensurePage } from "./session.js";
|
|
14
14
|
export function isResponseComplete(snapshot) {
|
|
15
15
|
return snapshot.latestText.trim().length > 0
|
|
16
16
|
&& !isTransientAssistantText(snapshot.latestText)
|
|
@@ -98,8 +98,14 @@ export async function submitMessage(env, args = {}) {
|
|
|
98
98
|
}
|
|
99
99
|
if (userTurn === undefined) {
|
|
100
100
|
const latestUser = await readLatestMessage(page, "user", "normalized_text");
|
|
101
|
+
const generation = await readAssistantGenerationState(page).catch(() => EMPTY_GENERATION_STATE);
|
|
102
|
+
const turnCount = await countPageMessages(page).catch(() => undefined);
|
|
101
103
|
if (submittedUserTurnMatches(latestUser?.text, args.text)) {
|
|
102
|
-
return resultOk(submitData(latestUser?.text,
|
|
104
|
+
return resultOk(submitData(latestUser?.text, turnCount, generation.active ? "submitted_generating" : "submitted", generation), await contextFromPage(page));
|
|
105
|
+
}
|
|
106
|
+
const turnAdvanced = previousTurnCount !== undefined && turnCount !== undefined && turnCount > previousTurnCount;
|
|
107
|
+
if (turnAdvanced || generation.active) {
|
|
108
|
+
return resultOk(submitData(undefined, turnCount, generation.active ? "submitted_generating" : "submitted_unconfirmed", generation), await contextFromPage(page), ["Submitted prompt could not be matched to a rendered user turn yet, but ChatGPT page state indicates progress."]);
|
|
103
109
|
}
|
|
104
110
|
return {
|
|
105
111
|
ok: false,
|
|
@@ -113,7 +119,7 @@ export async function submitMessage(env, args = {}) {
|
|
|
113
119
|
context: await contextFromPage(page)
|
|
114
120
|
};
|
|
115
121
|
}
|
|
116
|
-
return resultOk(submitData(userTurn, await countPageMessages(page).catch(() => undefined)), await contextFromPage(page));
|
|
122
|
+
return resultOk(submitData(userTurn, await countPageMessages(page).catch(() => undefined), "submitted", await readAssistantGenerationState(page).catch(() => EMPTY_GENERATION_STATE)), await contextFromPage(page));
|
|
117
123
|
}
|
|
118
124
|
catch (error) {
|
|
119
125
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
@@ -287,6 +293,7 @@ export async function waitForMessage(env, args = {}) {
|
|
|
287
293
|
let lastChangedAt = Date.now();
|
|
288
294
|
let lastObservedTextLength = 0;
|
|
289
295
|
let latestAssistantCount = await countPageMessages(page, "assistant").catch(() => 0);
|
|
296
|
+
let lastGeneration = EMPTY_GENERATION_STATE;
|
|
290
297
|
while (Date.now() - started < timeoutMs) {
|
|
291
298
|
const probeResult = await waitSnapshotProbe(page, deadline, { timeoutMs: probeTimeoutMs });
|
|
292
299
|
addWarnings(waitWarnings, probeResult.warnings);
|
|
@@ -317,6 +324,7 @@ export async function waitForMessage(env, args = {}) {
|
|
|
317
324
|
}
|
|
318
325
|
pollIndex += 1;
|
|
319
326
|
const hasResponseActions = await resolveResponseActions(page, snapshot);
|
|
327
|
+
lastGeneration = snapshot.generation;
|
|
320
328
|
if (targetReached && snapshot.generation.stopped && snapshot.text.length > 0) {
|
|
321
329
|
const stoppedText = normalizeWhitespace(await fetchLatestAssistantText(page) ?? "");
|
|
322
330
|
// A failed re-read must not fabricate an empty capture: real text was observed in
|
|
@@ -324,6 +332,9 @@ export async function waitForMessage(env, args = {}) {
|
|
|
324
332
|
const data = stoppedText.length > 0
|
|
325
333
|
? waitDataFromText(args, false, stoppedText, latestAssistantCount, Date.now() - started)
|
|
326
334
|
: waitDataWithoutText(latestAssistantCount, Date.now() - started);
|
|
335
|
+
data.completionState = "stopped";
|
|
336
|
+
data.generationActive = snapshot.generation.active;
|
|
337
|
+
data.generationSignals = snapshot.generation.signals;
|
|
327
338
|
if (stoppedText.length === 0) {
|
|
328
339
|
waitWarnings.add(`The interrupted assistant text (~${snapshot.text.length} chars observed) could not be re-read at wait exit; call messages.readLatest on the same thread to capture it.`);
|
|
329
340
|
}
|
|
@@ -361,6 +372,9 @@ export async function waitForMessage(env, args = {}) {
|
|
|
361
372
|
};
|
|
362
373
|
if (fetchedMetadata.hash === snapshot.text.hash && isResponseComplete(completionSnapshot)) {
|
|
363
374
|
const data = waitDataFromText(args, true, latestText, latestAssistantCount, Date.now() - started);
|
|
375
|
+
data.completionState = "complete";
|
|
376
|
+
data.generationActive = false;
|
|
377
|
+
data.generationSignals = snapshot.generation.signals;
|
|
364
378
|
return withCommandOutputText(resultOk(data, await contextFromPage(page), [...waitWarnings, ...responseContentWarnings(args, true)]));
|
|
365
379
|
}
|
|
366
380
|
if (latestText.length > 0) {
|
|
@@ -374,7 +388,11 @@ export async function waitForMessage(env, args = {}) {
|
|
|
374
388
|
if (lastObservedTextLength > 0) {
|
|
375
389
|
const partialText = await fetchLatestAssistantText(page);
|
|
376
390
|
if (partialText !== undefined && normalizeWhitespace(partialText).length > 0) {
|
|
377
|
-
const
|
|
391
|
+
const normalizedPartialText = normalizeWhitespace(partialText);
|
|
392
|
+
const data = waitDataFromText(args, false, normalizedPartialText, latestAssistantCount, Date.now() - started);
|
|
393
|
+
data.completionState = completionStateFromGeneration(lastGeneration, false, normalizedPartialText.length > 0);
|
|
394
|
+
data.generationActive = lastGeneration.active;
|
|
395
|
+
data.generationSignals = lastGeneration.signals;
|
|
378
396
|
return withCommandOutputText({
|
|
379
397
|
ok: false,
|
|
380
398
|
status: "partial",
|
|
@@ -552,9 +570,51 @@ export async function readLatest(env, args = {}) {
|
|
|
552
570
|
data.thoughtDurationText = latest.thoughtDurationText;
|
|
553
571
|
if (latest.sourcesAvailable !== undefined)
|
|
554
572
|
data.sourcesAvailable = latest.sourcesAvailable;
|
|
573
|
+
if (role === "assistant") {
|
|
574
|
+
const generation = await readAssistantGenerationState(page).catch(() => EMPTY_GENERATION_STATE);
|
|
575
|
+
data.completionState = completionStateFromGeneration(generation, undefined, latest.text.trim().length > 0);
|
|
576
|
+
data.generationActive = generation.active;
|
|
577
|
+
data.generationSignals = generation.signals;
|
|
578
|
+
}
|
|
555
579
|
return withCommandOutputText(resultOk(data, await contextFromPage(page), data.warnings ?? []));
|
|
556
580
|
}
|
|
581
|
+
export async function messageStatus(env, args = {}) {
|
|
582
|
+
const boot = await ensurePage(env);
|
|
583
|
+
if (!boot.ok) {
|
|
584
|
+
return boot;
|
|
585
|
+
}
|
|
586
|
+
const page = env.page;
|
|
587
|
+
// Reuse the combined wait-snapshot evaluate for counts and generation state in a single
|
|
588
|
+
// round trip; it never carries full assistant text, so a bounded preview is fetched
|
|
589
|
+
// separately below only when the snapshot proves an assistant turn has text.
|
|
590
|
+
const snapshot = await readWaitDomSnapshot(page).catch(() => undefined) ?? await fallbackWaitSnapshot(page, 0);
|
|
591
|
+
const maxPreviewChars = Math.max(0, args.maxPreviewChars ?? 240);
|
|
592
|
+
const latestText = snapshot.text.length > 0
|
|
593
|
+
? normalizeWhitespace(await fetchLatestAssistantText(page) ?? "")
|
|
594
|
+
: "";
|
|
595
|
+
const data = {
|
|
596
|
+
turnCount: snapshot.turnCount,
|
|
597
|
+
assistantTurnCount: snapshot.assistantTurnCount,
|
|
598
|
+
completionState: completionStateFromGeneration(snapshot.generation, undefined, latestText.length > 0),
|
|
599
|
+
generationActive: snapshot.generation.active,
|
|
600
|
+
generationSignals: snapshot.generation.signals
|
|
601
|
+
};
|
|
602
|
+
if (snapshot.latestAssistantTurnIndex !== undefined)
|
|
603
|
+
data.latestAssistantTurnIndex = snapshot.latestAssistantTurnIndex;
|
|
604
|
+
if (latestText.length > 0) {
|
|
605
|
+
data.latestAssistantTextLength = latestText.length;
|
|
606
|
+
data.latestAssistantPreview = latestText.length > maxPreviewChars
|
|
607
|
+
? `${latestText.slice(0, Math.max(0, maxPreviewChars - 1))}...`
|
|
608
|
+
: latestText;
|
|
609
|
+
}
|
|
610
|
+
return resultOk(data, await contextFromPage(page));
|
|
611
|
+
}
|
|
557
612
|
export async function askMessage(env, args) {
|
|
613
|
+
const normalizedPrompt = normalizeAskPrompt(args);
|
|
614
|
+
if (!normalizedPrompt.ok) {
|
|
615
|
+
return normalizedPrompt.result;
|
|
616
|
+
}
|
|
617
|
+
const prompt = normalizedPrompt.text;
|
|
558
618
|
const boot = await ensurePage(env);
|
|
559
619
|
if (!boot.ok) {
|
|
560
620
|
return boot;
|
|
@@ -562,7 +622,7 @@ export async function askMessage(env, args) {
|
|
|
562
622
|
const page = env.page;
|
|
563
623
|
const beforeTurnCount = await countPageMessages(page).catch(() => undefined);
|
|
564
624
|
const beforeAssistantTurnCount = await countPageMessages(page, "assistant").catch(() => undefined);
|
|
565
|
-
const composeArgs = { text:
|
|
625
|
+
const composeArgs = { text: prompt, mode: "replace" };
|
|
566
626
|
if (args.timeoutMs !== undefined) {
|
|
567
627
|
composeArgs.timeoutMs = args.timeoutMs;
|
|
568
628
|
}
|
|
@@ -570,7 +630,7 @@ export async function askMessage(env, args) {
|
|
|
570
630
|
if (!compose.ok) {
|
|
571
631
|
return forwardFailure(compose);
|
|
572
632
|
}
|
|
573
|
-
const submitArgs = { text:
|
|
633
|
+
const submitArgs = { text: prompt };
|
|
574
634
|
if (beforeTurnCount !== undefined) {
|
|
575
635
|
submitArgs.previousTurnCount = beforeTurnCount;
|
|
576
636
|
}
|
|
@@ -627,7 +687,7 @@ export async function askMessage(env, args) {
|
|
|
627
687
|
return forwardFailure(waitFailure);
|
|
628
688
|
}
|
|
629
689
|
const state = await readPageState(page).catch(() => undefined);
|
|
630
|
-
const data = { prompt
|
|
690
|
+
const data = { prompt };
|
|
631
691
|
const complete = waitResult?.data?.complete ?? (waitResult === undefined ? undefined : false);
|
|
632
692
|
if (complete !== undefined) {
|
|
633
693
|
data.complete = complete;
|
|
@@ -641,6 +701,7 @@ export async function askMessage(env, args) {
|
|
|
641
701
|
if (state?.title !== undefined) {
|
|
642
702
|
data.title = state.title;
|
|
643
703
|
}
|
|
704
|
+
applyMessageState(data, submit.data, waitResult?.data);
|
|
644
705
|
if (waitFailure !== undefined) {
|
|
645
706
|
data.complete = false;
|
|
646
707
|
return withCommandOutputText({
|
|
@@ -678,7 +739,7 @@ export async function waitAndRead(env, args = {}) {
|
|
|
678
739
|
}
|
|
679
740
|
return forwardFailure(read);
|
|
680
741
|
}
|
|
681
|
-
const data = askReadData("", read.data?.text, wait.data?.complete);
|
|
742
|
+
const data = askReadData("", read.data?.text, wait.data?.complete, wait.data, read.data);
|
|
682
743
|
const warnings = [...read.warnings, ...wait.warnings];
|
|
683
744
|
if (!wait.ok && wait.status === "partial") {
|
|
684
745
|
data.complete = false;
|
|
@@ -695,12 +756,6 @@ export async function waitAndRead(env, args = {}) {
|
|
|
695
756
|
}
|
|
696
757
|
return withCommandOutputText(resultOk(data, read.context, warnings));
|
|
697
758
|
}
|
|
698
|
-
async function ensurePage(env) {
|
|
699
|
-
if (env.page !== undefined) {
|
|
700
|
-
return resultOk({}, await contextFromPage(env.page));
|
|
701
|
-
}
|
|
702
|
-
return bootstrap(env, { preferExistingTab: true });
|
|
703
|
-
}
|
|
704
759
|
async function waitForSubmittedUserTurn(page, text, previousTurnCount, timeoutMs) {
|
|
705
760
|
const started = Date.now();
|
|
706
761
|
const wanted = text === undefined ? undefined : normalizeWhitespace(text);
|
|
@@ -820,7 +875,7 @@ async function sleep(page, ms) {
|
|
|
820
875
|
}
|
|
821
876
|
await new Promise(resolve => setTimeout(resolve, ms));
|
|
822
877
|
}
|
|
823
|
-
function submitData(userTurnText, turnCount) {
|
|
878
|
+
function submitData(userTurnText, turnCount, submissionState, generation) {
|
|
824
879
|
const data = { submitted: true };
|
|
825
880
|
if (userTurnText !== undefined) {
|
|
826
881
|
data.userTurnText = userTurnText;
|
|
@@ -828,9 +883,13 @@ function submitData(userTurnText, turnCount) {
|
|
|
828
883
|
if (turnCount !== undefined) {
|
|
829
884
|
data.turnCount = turnCount;
|
|
830
885
|
}
|
|
886
|
+
data.submissionState = submissionState;
|
|
887
|
+
data.completionState = completionStateFromGeneration(generation);
|
|
888
|
+
data.generationActive = generation.active;
|
|
889
|
+
data.generationSignals = generation.signals;
|
|
831
890
|
return data;
|
|
832
891
|
}
|
|
833
|
-
function askReadData(prompt, responseText, complete) {
|
|
892
|
+
function askReadData(prompt, responseText, complete, wait, read) {
|
|
834
893
|
const data = { prompt };
|
|
835
894
|
if (responseText !== undefined) {
|
|
836
895
|
data.responseText = responseText;
|
|
@@ -838,8 +897,71 @@ function askReadData(prompt, responseText, complete) {
|
|
|
838
897
|
if (complete !== undefined) {
|
|
839
898
|
data.complete = complete;
|
|
840
899
|
}
|
|
900
|
+
applyMessageState(data, undefined, wait, read);
|
|
841
901
|
return data;
|
|
842
902
|
}
|
|
903
|
+
function applyMessageState(data, submit, wait, read) {
|
|
904
|
+
if (submit?.submissionState !== undefined)
|
|
905
|
+
data.submissionState = submit.submissionState;
|
|
906
|
+
const completionState = wait?.completionState ?? read?.completionState ?? submit?.completionState;
|
|
907
|
+
if (completionState !== undefined)
|
|
908
|
+
data.completionState = completionState;
|
|
909
|
+
const generationActive = wait?.generationActive ?? read?.generationActive ?? submit?.generationActive;
|
|
910
|
+
if (generationActive !== undefined)
|
|
911
|
+
data.generationActive = generationActive;
|
|
912
|
+
const generationSignals = wait?.generationSignals ?? read?.generationSignals ?? submit?.generationSignals;
|
|
913
|
+
if (generationSignals !== undefined)
|
|
914
|
+
data.generationSignals = generationSignals;
|
|
915
|
+
}
|
|
916
|
+
function completionStateFromGeneration(generation, complete, hasPartialText = false) {
|
|
917
|
+
if (complete === true)
|
|
918
|
+
return "complete";
|
|
919
|
+
if (generation.active)
|
|
920
|
+
return "generating";
|
|
921
|
+
if (generation.stopped)
|
|
922
|
+
return "stopped";
|
|
923
|
+
if (complete === false || hasPartialText)
|
|
924
|
+
return "partial";
|
|
925
|
+
return "unknown";
|
|
926
|
+
}
|
|
927
|
+
function normalizeAskPrompt(args) {
|
|
928
|
+
const text = args.text;
|
|
929
|
+
const prompt = args.prompt;
|
|
930
|
+
if (text !== undefined && prompt !== undefined && text !== prompt) {
|
|
931
|
+
return {
|
|
932
|
+
ok: false,
|
|
933
|
+
result: {
|
|
934
|
+
ok: false,
|
|
935
|
+
status: "error",
|
|
936
|
+
warnings: [],
|
|
937
|
+
error: {
|
|
938
|
+
name: "InvalidAskArgs",
|
|
939
|
+
message: "messages.ask received both text and prompt with different values.",
|
|
940
|
+
recoverable: false
|
|
941
|
+
},
|
|
942
|
+
context: { timestamp: new Date().toISOString() }
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const normalized = text ?? prompt;
|
|
947
|
+
if (normalized === undefined || normalized.trim().length === 0) {
|
|
948
|
+
return {
|
|
949
|
+
ok: false,
|
|
950
|
+
result: {
|
|
951
|
+
ok: false,
|
|
952
|
+
status: "error",
|
|
953
|
+
warnings: [],
|
|
954
|
+
error: {
|
|
955
|
+
name: "InvalidAskArgs",
|
|
956
|
+
message: "messages.ask requires a non-empty text or prompt argument.",
|
|
957
|
+
recoverable: false
|
|
958
|
+
},
|
|
959
|
+
context: { timestamp: new Date().toISOString() }
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
return { ok: true, text: normalized };
|
|
964
|
+
}
|
|
843
965
|
function readRole(read) {
|
|
844
966
|
return typeof read === "object" ? read.role : undefined;
|
|
845
967
|
}
|
|
@@ -4,11 +4,12 @@ import { localeLabels } from "../dom/locale-labels.js";
|
|
|
4
4
|
import { isShortLatinToken, normalizeForLabelMatch, visibleLabelMatches } from "../dom/label-match.js";
|
|
5
5
|
import { normalizeLabel, normalizeWhitespace } from "../dom/visible-text.js";
|
|
6
6
|
import { contextFromPage } from "./context.js";
|
|
7
|
-
import {
|
|
7
|
+
import { ensurePage } from "./session.js";
|
|
8
8
|
const DEFAULT_MODE_EFFORT = "Thinking";
|
|
9
9
|
const CURRENT_MODE_LABELS = dedupeLabels([
|
|
10
10
|
...localeLabels.modeLabels,
|
|
11
11
|
...Object.values(localeLabels.modeOptions).flat(),
|
|
12
|
+
...Object.values(localeLabels.configurationOptions).flat(),
|
|
12
13
|
]);
|
|
13
14
|
const MODE_OPENER_LABELS = [...CURRENT_MODE_LABELS.filter(label => label !== "Pro"), ...localeLabels.modeOpenerExtra];
|
|
14
15
|
const MODEL_VERSION_FAMILY_PATTERN = /^gpt[\s-]/i;
|
|
@@ -224,12 +225,6 @@ export async function selectTool(env, args) {
|
|
|
224
225
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
|
-
async function ensurePage(env) {
|
|
228
|
-
if (env.page !== undefined) {
|
|
229
|
-
return resultOk({}, await contextFromPage(env.page));
|
|
230
|
-
}
|
|
231
|
-
return bootstrap(env, { preferExistingTab: true });
|
|
232
|
-
}
|
|
233
228
|
async function clickFirstUniqueButton(page, labels) {
|
|
234
229
|
for (const label of labels) {
|
|
235
230
|
const roleLocator = page.getByRole?.("button", { name: label, exact: true });
|
|
@@ -508,22 +503,35 @@ async function selectModelVersion(page, requestedVersion, currentCandidates, tim
|
|
|
508
503
|
candidates = await enumerateVisibleMenuItems(page);
|
|
509
504
|
}
|
|
510
505
|
}
|
|
511
|
-
let exact =
|
|
506
|
+
let exact = findExactSelectableMenuItem(candidates, requestedVersion);
|
|
512
507
|
if (exact !== undefined) {
|
|
513
|
-
return await
|
|
508
|
+
return await clickResolvedMenuItem(page, exact)
|
|
514
509
|
? { selected: exact.label, candidates: candidates.map(candidate => candidate.label) }
|
|
515
510
|
: { candidates: candidates.map(candidate => candidate.label) };
|
|
516
511
|
}
|
|
517
512
|
const opened = await openModelVersionSubmenu(page, currentCandidates);
|
|
518
513
|
candidates = await enumerateVisibleMenuItems(page);
|
|
519
|
-
exact =
|
|
514
|
+
exact = findExactSelectableMenuItem(candidates, requestedVersion);
|
|
520
515
|
if (!opened || exact === undefined) {
|
|
521
516
|
return { candidates: candidates.map(candidate => candidate.label) };
|
|
522
517
|
}
|
|
523
|
-
return await
|
|
518
|
+
return await clickResolvedMenuItem(page, exact)
|
|
524
519
|
? { selected: exact.label, candidates: candidates.map(candidate => candidate.label) }
|
|
525
520
|
: { candidates: candidates.map(candidate => candidate.label) };
|
|
526
521
|
}
|
|
522
|
+
function isModelVersionSubmenuOpener(item) {
|
|
523
|
+
return item.hasPopup === true
|
|
524
|
+
|| (item.role !== "menuitemradio" && MODEL_VERSION_FAMILY_PATTERN.test(item.label));
|
|
525
|
+
}
|
|
526
|
+
async function clickResolvedMenuItem(page, item) {
|
|
527
|
+
if (item.testId !== undefined && await clickIfUnique(page.locator?.(`[data-testid="${escapeAttributeValue(item.testId)}"]`))) {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
if (item.role !== undefined && await clickIfUnique(page.getByRole?.(item.role, { name: item.label, exact: true }))) {
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
return clickMenuItem(page, item.label);
|
|
534
|
+
}
|
|
527
535
|
async function openModelVersionSubmenu(page, candidates) {
|
|
528
536
|
const submenuOpeners = candidates.filter(item => item.hasPopup === true || MODEL_VERSION_FAMILY_PATTERN.test(item.label));
|
|
529
537
|
if (submenuOpeners.length === 0) {
|
|
@@ -604,9 +612,10 @@ async function modelVersionMenuItemsAreVisible(page) {
|
|
|
604
612
|
return (await enumerateVisibleMenuItems(page))
|
|
605
613
|
.some(candidate => candidate.role === "menuitemradio" && MODEL_VERSION_LABEL_PATTERN.test(candidate.label));
|
|
606
614
|
}
|
|
607
|
-
function
|
|
615
|
+
function findExactSelectableMenuItem(items, wanted) {
|
|
608
616
|
const normalized = normalizeLabel(wanted);
|
|
609
|
-
const matches = items.filter(item => item.normalized === normalized
|
|
617
|
+
const matches = items.filter(item => item.normalized === normalized
|
|
618
|
+
&& !isModelVersionSubmenuOpener(item));
|
|
610
619
|
return matches.length === 1 ? matches[0] : undefined;
|
|
611
620
|
}
|
|
612
621
|
function dedupeLabels(labels) {
|
|
@@ -666,9 +675,12 @@ async function visibleModeButtonLabelList(page) {
|
|
|
666
675
|
}
|
|
667
676
|
return text.includes(token);
|
|
668
677
|
};
|
|
678
|
+
const scopedRoots = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i], main [class*='composer' i]"));
|
|
669
679
|
return Array.from(document.querySelectorAll("button, [role='button']"))
|
|
670
680
|
.map(node => {
|
|
671
681
|
const element = node;
|
|
682
|
+
if (scopedRoots.length > 0 && !scopedRoots.some(root => root.contains(node)))
|
|
683
|
+
return "";
|
|
672
684
|
const visibleText = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
673
685
|
const ariaLabel = (element.getAttribute("aria-label") ?? "").replace(/\s+/g, " ").trim();
|
|
674
686
|
const label = visibleText.length > 0 ? visibleText : ariaLabel;
|
|
@@ -680,7 +692,9 @@ async function visibleModeButtonLabelList(page) {
|
|
|
680
692
|
if (visibleText.length === 0 && /feedback|conversation options|dismiss/i.test(ariaLabel))
|
|
681
693
|
return "";
|
|
682
694
|
const normalized = label.toLowerCase();
|
|
683
|
-
|
|
695
|
+
const structuralModelControl = /model-switcher|model-selector|mode-selector/i.test(testId)
|
|
696
|
+
|| /\b(?:gpt|sol|luna|terra)\b/i.test(label);
|
|
697
|
+
if (!structuralModelControl && !normalizedModeLabels.some(modeLabel => tokenMatches(normalized, modeLabel)))
|
|
684
698
|
return "";
|
|
685
699
|
return label;
|
|
686
700
|
})
|