codex-chatgpt-control 0.1.0-alpha.1 → 0.2.0-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 +6 -0
- package/README.md +55 -0
- package/contracts/v1/fixtures/backend-capabilities.json +7 -0
- package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
- package/contracts/v1/fixtures/command-descriptors.json +192 -15
- package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
- package/contracts/v1/fixtures/doctor-scenario-preflight.json +108 -0
- package/contracts/v1/fixtures/existing-tab-diagnostics-blocker.json +57 -0
- package/contracts/v1/fixtures/files-preflight-success.json +32 -0
- package/contracts/v1/fixtures/help-root.json +1 -1
- package/contracts/v1/fixtures/output-json-parse-success.json +17 -2
- package/contracts/v1/fixtures/project-sources-plan-add.json +71 -0
- package/contracts/v1/fixtures/read-latest-clipped.json +31 -0
- package/contracts/v1/fixtures/reports-create-redacted.json +15 -0
- package/contracts/v1/fixtures/responses-basic-success.json +11 -0
- package/contracts/v1/fixtures/run-basic-success.json +15 -0
- package/contracts/v1/fixtures/run-browser-bridge-blocker.json +2 -2
- package/contracts/v1/fixtures/run-timeout-partial.json +2 -2
- package/contracts/v1/fixtures/runner-budget-blocker.json +1 -1
- package/contracts/v1/fixtures/stream-blocked.ndjson +1 -1
- package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
- package/contracts/v1/manifest.json +30 -0
- package/contracts/v1/parity-suite.json +72 -6
- package/contracts/v1/schemas/backend-request.schema.json +3 -0
- package/contracts/v1/schemas/command-result.schema.json +48 -0
- package/contracts/v1/schemas/response.schema.json +28 -0
- package/contracts/v1/schemas/run-result.schema.json +84 -0
- package/contracts/v1/surface-drift-policy.json +210 -0
- package/dist/codex-chatgpt-control-backend.mjs +5137 -488
- package/dist/codex-chatgpt-control.bundle.mjs +5001 -286
- package/dist/src/backend/client.d.ts +6 -1
- package/dist/src/backend/client.js +5 -0
- package/dist/src/backend/protocol.d.ts +1 -1
- package/dist/src/backend/protocol.js +7 -0
- package/dist/src/backend/session.js +14 -0
- package/dist/src/browser/attach.js +128 -20
- package/dist/src/browser/downloads.js +2 -1
- package/dist/src/browser/page-state.js +6 -3
- package/dist/src/client.d.ts +16 -1
- package/dist/src/client.js +86 -7
- package/dist/src/commands/artifacts.d.ts +5 -0
- package/dist/src/commands/artifacts.js +432 -0
- package/dist/src/commands/context.js +5 -2
- package/dist/src/commands/doctor.d.ts +10 -2
- package/dist/src/commands/doctor.js +242 -14
- package/dist/src/commands/files.d.ts +2 -1
- package/dist/src/commands/files.js +352 -18
- package/dist/src/commands/messages.d.ts +2 -1
- package/dist/src/commands/messages.js +232 -40
- package/dist/src/commands/modes.js +202 -20
- package/dist/src/commands/project-sources.d.ts +10 -0
- package/dist/src/commands/project-sources.js +643 -0
- package/dist/src/commands/registry.js +76 -14
- package/dist/src/commands/reports.d.ts +9 -0
- package/dist/src/commands/reports.js +83 -6
- package/dist/src/commands/response-actions.js +33 -0
- package/dist/src/commands/sequence.js +14 -0
- package/dist/src/commands/threads.js +2 -1
- package/dist/src/commands/timeouts.d.ts +2 -0
- package/dist/src/commands/timeouts.js +18 -0
- package/dist/src/diagnostics/blockers.d.ts +54 -0
- package/dist/src/diagnostics/blockers.js +299 -0
- package/dist/src/dom/artifacts.d.ts +7 -0
- package/dist/src/dom/artifacts.js +198 -0
- package/dist/src/dom/generation-state.d.ts +8 -0
- package/dist/src/dom/generation-state.js +103 -0
- package/dist/src/dom/locale/am.d.ts +28 -0
- package/dist/src/dom/locale/am.js +28 -0
- package/dist/src/dom/locale/ar.d.ts +28 -0
- package/dist/src/dom/locale/ar.js +28 -0
- package/dist/src/dom/locale/bg.d.ts +28 -0
- package/dist/src/dom/locale/bg.js +28 -0
- package/dist/src/dom/locale/bn.d.ts +28 -0
- package/dist/src/dom/locale/bn.js +28 -0
- package/dist/src/dom/locale/bs.d.ts +28 -0
- package/dist/src/dom/locale/bs.js +28 -0
- package/dist/src/dom/locale/ca.d.ts +28 -0
- package/dist/src/dom/locale/ca.js +28 -0
- package/dist/src/dom/locale/cs.d.ts +28 -0
- package/dist/src/dom/locale/cs.js +28 -0
- package/dist/src/dom/locale/da.d.ts +28 -0
- package/dist/src/dom/locale/da.js +28 -0
- package/dist/src/dom/locale/de.d.ts +30 -0
- package/dist/src/dom/locale/de.js +30 -0
- package/dist/src/dom/locale/el.d.ts +28 -0
- package/dist/src/dom/locale/el.js +28 -0
- package/dist/src/dom/locale/en.d.ts +50 -0
- package/dist/src/dom/locale/en.js +56 -0
- package/dist/src/dom/locale/es-419.d.ts +28 -0
- package/dist/src/dom/locale/es-419.js +28 -0
- package/dist/src/dom/locale/es-ES.d.ts +28 -0
- package/dist/src/dom/locale/es-ES.js +28 -0
- package/dist/src/dom/locale/et.d.ts +28 -0
- package/dist/src/dom/locale/et.js +28 -0
- package/dist/src/dom/locale/fa.d.ts +28 -0
- package/dist/src/dom/locale/fa.js +28 -0
- package/dist/src/dom/locale/fi.d.ts +28 -0
- package/dist/src/dom/locale/fi.js +28 -0
- package/dist/src/dom/locale/fr-CA.d.ts +29 -0
- package/dist/src/dom/locale/fr-CA.js +29 -0
- package/dist/src/dom/locale/fr-FR.d.ts +30 -0
- package/dist/src/dom/locale/fr-FR.js +30 -0
- package/dist/src/dom/locale/gu.d.ts +28 -0
- package/dist/src/dom/locale/gu.js +28 -0
- package/dist/src/dom/locale/hi.d.ts +28 -0
- package/dist/src/dom/locale/hi.js +28 -0
- package/dist/src/dom/locale/hr.d.ts +28 -0
- package/dist/src/dom/locale/hr.js +28 -0
- package/dist/src/dom/locale/hu.d.ts +28 -0
- package/dist/src/dom/locale/hu.js +28 -0
- package/dist/src/dom/locale/hy.d.ts +28 -0
- package/dist/src/dom/locale/hy.js +28 -0
- package/dist/src/dom/locale/id.d.ts +28 -0
- package/dist/src/dom/locale/id.js +28 -0
- package/dist/src/dom/locale/index.d.ts +79 -0
- package/dist/src/dom/locale/index.js +202 -0
- package/dist/src/dom/locale/is.d.ts +28 -0
- package/dist/src/dom/locale/is.js +28 -0
- package/dist/src/dom/locale/it.d.ts +27 -0
- package/dist/src/dom/locale/it.js +27 -0
- package/dist/src/dom/locale/ja.d.ts +27 -0
- package/dist/src/dom/locale/ja.js +27 -0
- package/dist/src/dom/locale/ka.d.ts +28 -0
- package/dist/src/dom/locale/ka.js +28 -0
- package/dist/src/dom/locale/kk.d.ts +28 -0
- package/dist/src/dom/locale/kk.js +28 -0
- package/dist/src/dom/locale/kn.d.ts +28 -0
- package/dist/src/dom/locale/kn.js +28 -0
- package/dist/src/dom/locale/ko.d.ts +28 -0
- package/dist/src/dom/locale/ko.js +28 -0
- package/dist/src/dom/locale/lt.d.ts +28 -0
- package/dist/src/dom/locale/lt.js +28 -0
- package/dist/src/dom/locale/lv.d.ts +28 -0
- package/dist/src/dom/locale/lv.js +28 -0
- package/dist/src/dom/locale/mk.d.ts +28 -0
- package/dist/src/dom/locale/mk.js +28 -0
- package/dist/src/dom/locale/ml.d.ts +28 -0
- package/dist/src/dom/locale/ml.js +28 -0
- package/dist/src/dom/locale/mn.d.ts +28 -0
- package/dist/src/dom/locale/mn.js +28 -0
- package/dist/src/dom/locale/mr.d.ts +28 -0
- package/dist/src/dom/locale/mr.js +28 -0
- package/dist/src/dom/locale/ms.d.ts +28 -0
- package/dist/src/dom/locale/ms.js +28 -0
- package/dist/src/dom/locale/my.d.ts +28 -0
- package/dist/src/dom/locale/my.js +28 -0
- package/dist/src/dom/locale/nb.d.ts +28 -0
- package/dist/src/dom/locale/nb.js +28 -0
- package/dist/src/dom/locale/nl.d.ts +28 -0
- package/dist/src/dom/locale/nl.js +28 -0
- package/dist/src/dom/locale/pa.d.ts +28 -0
- package/dist/src/dom/locale/pa.js +28 -0
- package/dist/src/dom/locale/pl.d.ts +28 -0
- package/dist/src/dom/locale/pl.js +28 -0
- package/dist/src/dom/locale/pt-BR.d.ts +28 -0
- package/dist/src/dom/locale/pt-BR.js +28 -0
- package/dist/src/dom/locale/pt-PT.d.ts +28 -0
- package/dist/src/dom/locale/pt-PT.js +28 -0
- package/dist/src/dom/locale/ro.d.ts +28 -0
- package/dist/src/dom/locale/ro.js +28 -0
- package/dist/src/dom/locale/ru.d.ts +28 -0
- package/dist/src/dom/locale/ru.js +28 -0
- package/dist/src/dom/locale/sk.d.ts +28 -0
- package/dist/src/dom/locale/sk.js +28 -0
- package/dist/src/dom/locale/sl.d.ts +28 -0
- package/dist/src/dom/locale/sl.js +28 -0
- package/dist/src/dom/locale/so.d.ts +28 -0
- package/dist/src/dom/locale/so.js +28 -0
- package/dist/src/dom/locale/sq.d.ts +28 -0
- package/dist/src/dom/locale/sq.js +28 -0
- package/dist/src/dom/locale/sr.d.ts +28 -0
- package/dist/src/dom/locale/sr.js +28 -0
- package/dist/src/dom/locale/sv.d.ts +28 -0
- package/dist/src/dom/locale/sv.js +28 -0
- package/dist/src/dom/locale/sw.d.ts +28 -0
- package/dist/src/dom/locale/sw.js +28 -0
- package/dist/src/dom/locale/ta.d.ts +28 -0
- package/dist/src/dom/locale/ta.js +28 -0
- package/dist/src/dom/locale/te.d.ts +28 -0
- package/dist/src/dom/locale/te.js +28 -0
- package/dist/src/dom/locale/th.d.ts +28 -0
- package/dist/src/dom/locale/th.js +28 -0
- package/dist/src/dom/locale/tl.d.ts +28 -0
- package/dist/src/dom/locale/tl.js +28 -0
- package/dist/src/dom/locale/tr.d.ts +28 -0
- package/dist/src/dom/locale/tr.js +28 -0
- package/dist/src/dom/locale/types.d.ts +45 -0
- package/dist/src/dom/locale/types.js +1 -0
- package/dist/src/dom/locale/uk.d.ts +28 -0
- package/dist/src/dom/locale/uk.js +28 -0
- package/dist/src/dom/locale/ur.d.ts +28 -0
- package/dist/src/dom/locale/ur.js +28 -0
- package/dist/src/dom/locale/vi.d.ts +28 -0
- package/dist/src/dom/locale/vi.js +28 -0
- package/dist/src/dom/locale/zh-HK.d.ts +28 -0
- package/dist/src/dom/locale/zh-HK.js +28 -0
- package/dist/src/dom/locale/zh-Hans.d.ts +28 -0
- package/dist/src/dom/locale/zh-Hans.js +28 -0
- package/dist/src/dom/locale/zh-TW.d.ts +29 -0
- package/dist/src/dom/locale/zh-TW.js +29 -0
- package/dist/src/dom/locale-labels.d.ts +26 -0
- package/dist/src/dom/locale-labels.js +26 -0
- package/dist/src/dom/menus.d.ts +6 -0
- package/dist/src/dom/menus.js +34 -8
- package/dist/src/dom/message-format.d.ts +2 -1
- package/dist/src/dom/message-format.js +67 -10
- package/dist/src/dom/messages.d.ts +2 -1
- package/dist/src/dom/messages.js +2 -4
- package/dist/src/dom/selectors.d.ts +1 -0
- package/dist/src/dom/selectors.js +42 -15
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +5 -0
- package/dist/src/platform/local-paths.d.ts +4 -0
- package/dist/src/platform/local-paths.js +20 -0
- package/dist/src/runner/interruptions.js +15 -4
- package/dist/src/runner/responses.js +18 -1
- package/dist/src/runner/result.js +15 -0
- package/dist/src/runner/resume.js +1 -0
- package/dist/src/runner/types.d.ts +3 -0
- package/dist/src/safety/risk.d.ts +7 -0
- package/dist/src/safety/risk.js +7 -0
- package/dist/src/safety/untrusted-output.d.ts +73 -0
- package/dist/src/safety/untrusted-output.js +199 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +1 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.js +184 -0
- package/dist/src/scripts/capture-intelligence-locales.d.ts +25 -0
- package/dist/src/scripts/capture-intelligence-locales.js +669 -0
- package/dist/src/scripts/check-surface-drift.d.ts +1 -0
- package/dist/src/scripts/check-surface-drift.js +11 -0
- package/dist/src/scripts/live-smoke/scenarios.js +84 -0
- package/dist/src/scripts/locale-capture/language-coverage.d.ts +10 -0
- package/dist/src/scripts/locale-capture/language-coverage.js +39 -0
- package/dist/src/testing/surface-drift.d.ts +66 -0
- package/dist/src/testing/surface-drift.js +363 -0
- package/dist/src/types.d.ts +201 -2
- package/package.json +2 -1
- package/references/backend-protocol.md +93 -1
- package/references/language-coverage.md +142 -0
- package/references/localization.md +247 -0
- package/references/python-parity.md +63 -1
- package/references/safety.md +2 -0
- package/references/troubleshooting.md +58 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readPageState } from "../browser/page-state.js";
|
|
2
2
|
import { resultError, resultOk } from "../errors.js";
|
|
3
|
+
import { latestAssistantTurnHasResponseActions, readAssistantGenerationState } from "../dom/generation-state.js";
|
|
3
4
|
import { countPageMessages, isTransientAssistantText, readLatestMessage, readLatestMessageText, readLatestMessageTextSnapshot, readMessages } from "../dom/messages.js";
|
|
4
|
-
import { composerTextbox,
|
|
5
|
+
import { composerTextbox, sendButton } from "../dom/selectors.js";
|
|
5
6
|
import { normalizeLineBreaks, normalizeWhitespace } from "../dom/visible-text.js";
|
|
6
7
|
import { contextFromPage } from "./context.js";
|
|
7
8
|
import { withCommandOutputText } from "./output.js";
|
|
@@ -10,7 +11,8 @@ export function isResponseComplete(snapshot) {
|
|
|
10
11
|
return snapshot.latestText.trim().length > 0
|
|
11
12
|
&& !isTransientAssistantText(snapshot.latestText)
|
|
12
13
|
&& snapshot.textStableForMs >= snapshot.stableMs
|
|
13
|
-
&& !snapshot.
|
|
14
|
+
&& !snapshot.generation.active
|
|
15
|
+
&& !snapshot.generation.stopped
|
|
14
16
|
&& snapshot.hasResponseActions;
|
|
15
17
|
}
|
|
16
18
|
export async function composeMessage(env, args) {
|
|
@@ -55,13 +57,41 @@ export async function submitMessage(env, args = {}) {
|
|
|
55
57
|
const page = env.page;
|
|
56
58
|
const previousTurnCount = args.previousTurnCount ?? await countPageMessages(page).catch(() => undefined);
|
|
57
59
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
const ready = await waitForSendButtonReady(page, args.timeoutMs ?? 30000);
|
|
61
|
+
if (!ready.ready) {
|
|
62
|
+
const blocker = {
|
|
63
|
+
kind: ready.code === "attachment_processing" ? "upload_failed" : "selector_drift",
|
|
64
|
+
code: ready.code,
|
|
65
|
+
message: ready.message,
|
|
66
|
+
remediation: [
|
|
67
|
+
{
|
|
68
|
+
label: "Wait for composer",
|
|
69
|
+
instruction: "Wait for ChatGPT's composer and attachments to become ready, then retry without manually changing the page.",
|
|
70
|
+
userActionRequired: false
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
resumable: true
|
|
74
|
+
};
|
|
75
|
+
if (ready.visibleText !== undefined) {
|
|
76
|
+
blocker.visibleText = ready.visibleText;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
status: "blocked",
|
|
81
|
+
warnings: [],
|
|
82
|
+
blocker,
|
|
83
|
+
context: await contextFromPage(page)
|
|
84
|
+
};
|
|
60
85
|
}
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
const timeoutMs = args.timeoutMs ?? 30000;
|
|
87
|
+
const startedAt = Date.now();
|
|
88
|
+
await clickSendControl(page);
|
|
89
|
+
let userTurn = await waitForSubmittedUserTurn(page, args.text, previousTurnCount, initialSubmitWaitMs(timeoutMs));
|
|
90
|
+
if (userTurn === undefined && Date.now() - startedAt < timeoutMs && await shouldRetryNoopSubmit(page, args.text)) {
|
|
91
|
+
await sleep(page, 250);
|
|
92
|
+
await clickSendControl(page);
|
|
93
|
+
userTurn = await waitForSubmittedUserTurn(page, args.text, previousTurnCount, Math.max(0, timeoutMs - (Date.now() - startedAt)));
|
|
63
94
|
}
|
|
64
|
-
const userTurn = await waitForSubmittedUserTurn(page, args.text, previousTurnCount, args.timeoutMs ?? 30000);
|
|
65
95
|
if (userTurn === undefined) {
|
|
66
96
|
const latestUser = await readLatestMessage(page, "user", "normalized_text");
|
|
67
97
|
if (submittedUserTurnMatches(latestUser?.text, args.text)) {
|
|
@@ -70,7 +100,7 @@ export async function submitMessage(env, args = {}) {
|
|
|
70
100
|
return {
|
|
71
101
|
ok: false,
|
|
72
102
|
status: "timeout",
|
|
73
|
-
warnings:
|
|
103
|
+
warnings: await sendTimeoutWarnings(page),
|
|
74
104
|
error: {
|
|
75
105
|
name: "SubmitTimeout",
|
|
76
106
|
message: "No matching submitted user turn appeared before the timeout.",
|
|
@@ -85,6 +115,140 @@ export async function submitMessage(env, args = {}) {
|
|
|
85
115
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
86
116
|
}
|
|
87
117
|
}
|
|
118
|
+
async function clickSendControl(page) {
|
|
119
|
+
try {
|
|
120
|
+
await sendButton(page).click?.();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
await page.keyboard?.press?.("Enter");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function initialSubmitWaitMs(timeoutMs) {
|
|
127
|
+
return Math.min(3000, Math.max(500, Math.floor(timeoutMs / 3)));
|
|
128
|
+
}
|
|
129
|
+
async function shouldRetryNoopSubmit(page, text) {
|
|
130
|
+
const state = await readSendButtonState(page).catch(() => ({ available: false }));
|
|
131
|
+
if (!isSendButtonReady(state)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (text === undefined) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
const composerText = await readLocatorText(composerTextbox(page)).catch(() => "");
|
|
138
|
+
return submittedUserTurnMatches(composerText, text);
|
|
139
|
+
}
|
|
140
|
+
async function waitForSendButtonReady(page, timeoutMs) {
|
|
141
|
+
const started = Date.now();
|
|
142
|
+
let lastState;
|
|
143
|
+
let lastVisibleText;
|
|
144
|
+
while (Date.now() - started < timeoutMs) {
|
|
145
|
+
const state = await readSendButtonState(page).catch(() => ({ available: true }));
|
|
146
|
+
lastState = state;
|
|
147
|
+
if (isSendButtonReady(state)) {
|
|
148
|
+
return { ready: true };
|
|
149
|
+
}
|
|
150
|
+
const visibleText = await readVisibleTextForSubmit(page).catch(() => undefined);
|
|
151
|
+
if (visibleText !== undefined && /uploading|processing|attaching|preparing|reading|scanning/i.test(visibleText)) {
|
|
152
|
+
lastVisibleText = visibleText.slice(0, 500);
|
|
153
|
+
}
|
|
154
|
+
await sleep(page, 250);
|
|
155
|
+
}
|
|
156
|
+
if (lastVisibleText !== undefined) {
|
|
157
|
+
return {
|
|
158
|
+
ready: false,
|
|
159
|
+
code: "attachment_processing",
|
|
160
|
+
message: "ChatGPT still appears to be processing an attachment, so the send button did not become ready.",
|
|
161
|
+
visibleText: lastVisibleText
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ready: false,
|
|
166
|
+
code: "send_button_not_ready",
|
|
167
|
+
message: `ChatGPT's send button did not become ready before timeout.${describeSendState(lastState)}`
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function isSendButtonReady(state) {
|
|
171
|
+
if (!state.available)
|
|
172
|
+
return false;
|
|
173
|
+
if (state.visible === false)
|
|
174
|
+
return false;
|
|
175
|
+
if (state.disabled === true)
|
|
176
|
+
return false;
|
|
177
|
+
if (state.busy === true)
|
|
178
|
+
return false;
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
async function readSendButtonState(page) {
|
|
182
|
+
const locator = sendButton(page);
|
|
183
|
+
if (typeof locator.count === "function" && await locator.count().catch(() => 1) === 0) {
|
|
184
|
+
return { available: false, reason: "not_found" };
|
|
185
|
+
}
|
|
186
|
+
const visible = typeof locator.isVisible === "function" ? await locator.isVisible({ timeoutMs: 500 }).catch(() => undefined) : undefined;
|
|
187
|
+
if (typeof locator.evaluate !== "function") {
|
|
188
|
+
const state = { available: true };
|
|
189
|
+
if (visible !== undefined)
|
|
190
|
+
state.visible = visible;
|
|
191
|
+
return state;
|
|
192
|
+
}
|
|
193
|
+
const evaluated = await locator.evaluate(element => {
|
|
194
|
+
const htmlElement = element;
|
|
195
|
+
const button = element;
|
|
196
|
+
return {
|
|
197
|
+
disabled: button.disabled === true
|
|
198
|
+
|| element.getAttribute("disabled") !== null
|
|
199
|
+
|| element.getAttribute("aria-disabled") === "true"
|
|
200
|
+
|| element.getAttribute("data-disabled") === "true",
|
|
201
|
+
busy: element.getAttribute("aria-busy") === "true"
|
|
202
|
+
|| htmlElement.className.toString().toLocaleLowerCase().includes("loading"),
|
|
203
|
+
label: element.getAttribute("aria-label")
|
|
204
|
+
?? element.getAttribute("title")
|
|
205
|
+
?? htmlElement.innerText
|
|
206
|
+
?? element.textContent
|
|
207
|
+
?? undefined
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
const state = {
|
|
211
|
+
available: true,
|
|
212
|
+
disabled: evaluated.disabled,
|
|
213
|
+
busy: evaluated.busy
|
|
214
|
+
};
|
|
215
|
+
if (visible !== undefined)
|
|
216
|
+
state.visible = visible;
|
|
217
|
+
if (evaluated.label !== undefined)
|
|
218
|
+
state.label = evaluated.label;
|
|
219
|
+
return state;
|
|
220
|
+
}
|
|
221
|
+
async function readVisibleTextForSubmit(page) {
|
|
222
|
+
if (typeof page.evaluate !== "function") {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
return page.evaluate(() => document.body?.innerText ?? "");
|
|
226
|
+
}
|
|
227
|
+
async function sendTimeoutWarnings(page) {
|
|
228
|
+
const state = await readSendButtonState(page).catch(() => undefined);
|
|
229
|
+
if (state === undefined || isSendButtonReady(state)) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
return [`Send button state after submit timeout:${describeSendState(state)}`];
|
|
233
|
+
}
|
|
234
|
+
function describeSendState(state) {
|
|
235
|
+
if (state === undefined)
|
|
236
|
+
return "";
|
|
237
|
+
const parts = [];
|
|
238
|
+
if (!state.available)
|
|
239
|
+
parts.push("available=false");
|
|
240
|
+
if (state.visible !== undefined)
|
|
241
|
+
parts.push(`visible=${state.visible}`);
|
|
242
|
+
if (state.disabled !== undefined)
|
|
243
|
+
parts.push(`disabled=${state.disabled}`);
|
|
244
|
+
if (state.busy !== undefined)
|
|
245
|
+
parts.push(`busy=${state.busy}`);
|
|
246
|
+
if (state.label !== undefined && state.label.trim().length > 0)
|
|
247
|
+
parts.push(`label=${JSON.stringify(state.label.trim().slice(0, 80))}`);
|
|
248
|
+
if (state.reason !== undefined)
|
|
249
|
+
parts.push(`reason=${state.reason}`);
|
|
250
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
251
|
+
}
|
|
88
252
|
export async function waitForMessage(env, args = {}) {
|
|
89
253
|
const boot = await ensurePage(env);
|
|
90
254
|
if (!boot.ok) {
|
|
@@ -122,9 +286,26 @@ export async function waitForMessage(env, args = {}) {
|
|
|
122
286
|
latestText,
|
|
123
287
|
stableMs,
|
|
124
288
|
textStableForMs: Date.now() - lastChangedAt,
|
|
125
|
-
|
|
126
|
-
hasResponseActions: await
|
|
289
|
+
generation: await readAssistantGenerationState(page),
|
|
290
|
+
hasResponseActions: await latestAssistantTurnHasResponseActions(page)
|
|
127
291
|
};
|
|
292
|
+
if (targetReached && snapshot.generation.stopped && latestText.length > 0) {
|
|
293
|
+
return withCommandOutputText({
|
|
294
|
+
ok: false,
|
|
295
|
+
status: "partial",
|
|
296
|
+
data: {
|
|
297
|
+
complete: false,
|
|
298
|
+
responseText: latestText,
|
|
299
|
+
assistantTurnCount: latestAssistantCount,
|
|
300
|
+
elapsedMs: Date.now() - started
|
|
301
|
+
},
|
|
302
|
+
warnings: [
|
|
303
|
+
"ChatGPT generation appears to have been stopped or interrupted before completion.",
|
|
304
|
+
...snapshot.generation.signals.map(signal => `Generation state signal: ${signal}`)
|
|
305
|
+
],
|
|
306
|
+
context: await contextFromPage(page)
|
|
307
|
+
});
|
|
308
|
+
}
|
|
128
309
|
if (targetReached && isResponseComplete(snapshot)) {
|
|
129
310
|
return withCommandOutputText(resultOk({ complete: true, responseText: latestText, assistantTurnCount: latestAssistantCount, elapsedMs: Date.now() - started }, await contextFromPage(page)));
|
|
130
311
|
}
|
|
@@ -182,6 +363,8 @@ export async function readLatest(env, args = {}) {
|
|
|
182
363
|
data.source = latest.source;
|
|
183
364
|
if (latest.fidelity !== undefined)
|
|
184
365
|
data.fidelity = latest.fidelity;
|
|
366
|
+
if (latest.captureLimit !== undefined)
|
|
367
|
+
data.captureLimit = latest.captureLimit;
|
|
185
368
|
if (latest.warnings !== undefined)
|
|
186
369
|
data.warnings = latest.warnings;
|
|
187
370
|
if (latest.markdown !== undefined)
|
|
@@ -249,11 +432,16 @@ export async function askMessage(env, args) {
|
|
|
249
432
|
waitArgs.afterAssistantTurnCount = beforeAssistantTurnCount;
|
|
250
433
|
}
|
|
251
434
|
waitResult = await waitForMessage(env, waitArgs);
|
|
252
|
-
if (!waitResult.ok
|
|
253
|
-
if (
|
|
254
|
-
|
|
435
|
+
if (!waitResult.ok) {
|
|
436
|
+
if (waitResult.status === "partial") {
|
|
437
|
+
waitFailure = waitResult;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
if (!readRequested || readRole(args.read) === "user") {
|
|
441
|
+
return forwardFailure(waitResult);
|
|
442
|
+
}
|
|
443
|
+
waitFailure = waitResult;
|
|
255
444
|
}
|
|
256
|
-
waitFailure = waitResult;
|
|
257
445
|
}
|
|
258
446
|
}
|
|
259
447
|
let responseText = waitResult?.data?.responseText;
|
|
@@ -265,6 +453,7 @@ export async function askMessage(env, args) {
|
|
|
265
453
|
return forwardFailure(waitFailure);
|
|
266
454
|
}
|
|
267
455
|
responseText = read.data?.text;
|
|
456
|
+
warnings.push(...read.warnings);
|
|
268
457
|
if (waitFailure !== undefined) {
|
|
269
458
|
warnings.push(...waitFailure.warnings, `Assistant response was read after ${waitFailure.status}, but completion was not confirmed by the wait step.`);
|
|
270
459
|
}
|
|
@@ -291,6 +480,19 @@ export async function askMessage(env, args) {
|
|
|
291
480
|
if (state?.title !== undefined) {
|
|
292
481
|
data.title = state.title;
|
|
293
482
|
}
|
|
483
|
+
if (waitFailure !== undefined) {
|
|
484
|
+
data.complete = false;
|
|
485
|
+
return withCommandOutputText({
|
|
486
|
+
ok: false,
|
|
487
|
+
status: "partial",
|
|
488
|
+
data,
|
|
489
|
+
warnings: [
|
|
490
|
+
...warnings,
|
|
491
|
+
`Assistant response was read after ${waitFailure.status}, but completion was not confirmed.`
|
|
492
|
+
],
|
|
493
|
+
context: await contextFromPage(page)
|
|
494
|
+
});
|
|
495
|
+
}
|
|
294
496
|
return withCommandOutputText(resultOk(data, await contextFromPage(page), warnings));
|
|
295
497
|
}
|
|
296
498
|
export async function waitAndRead(env, args = {}) {
|
|
@@ -315,7 +517,22 @@ export async function waitAndRead(env, args = {}) {
|
|
|
315
517
|
}
|
|
316
518
|
return forwardFailure(read);
|
|
317
519
|
}
|
|
318
|
-
|
|
520
|
+
const data = askReadData("", read.data?.text, wait.data?.complete);
|
|
521
|
+
const warnings = [...read.warnings, ...wait.warnings];
|
|
522
|
+
if (!wait.ok && wait.status === "partial") {
|
|
523
|
+
data.complete = false;
|
|
524
|
+
return withCommandOutputText({
|
|
525
|
+
ok: false,
|
|
526
|
+
status: "partial",
|
|
527
|
+
data,
|
|
528
|
+
warnings: [
|
|
529
|
+
...warnings,
|
|
530
|
+
"Assistant response was read after partial wait, but completion was not confirmed."
|
|
531
|
+
],
|
|
532
|
+
context: read.context
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return withCommandOutputText(resultOk(data, read.context, warnings));
|
|
319
536
|
}
|
|
320
537
|
async function ensurePage(env) {
|
|
321
538
|
if (env.page !== undefined) {
|
|
@@ -384,31 +601,6 @@ function renderSubmittedTurnMarkdownSyntax(text, preserveFenceLanguage = true) {
|
|
|
384
601
|
.replace(/\*([^*]+)\*/g, "$1")
|
|
385
602
|
.replace(/_([^_]+)_/g, "$1");
|
|
386
603
|
}
|
|
387
|
-
async function hasStopControl(page) {
|
|
388
|
-
if (typeof page.evaluate === "function") {
|
|
389
|
-
return page.evaluate(() => {
|
|
390
|
-
const text = document.body?.innerText ?? "";
|
|
391
|
-
return /\b(stop generating|stop streaming|cancel)\b/i.test(text);
|
|
392
|
-
}).catch(() => false);
|
|
393
|
-
}
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
async function hasResponseActions(page) {
|
|
397
|
-
try {
|
|
398
|
-
const copyButtons = copyResponseButtons(page);
|
|
399
|
-
const count = await copyButtons.count?.();
|
|
400
|
-
if (count !== undefined) {
|
|
401
|
-
return count > 0;
|
|
402
|
-
}
|
|
403
|
-
return await copyButtons.isVisible?.() === true;
|
|
404
|
-
}
|
|
405
|
-
catch {
|
|
406
|
-
if (typeof page.evaluate === "function") {
|
|
407
|
-
return page.evaluate(() => /\b(Copy response|More actions)\b/i.test(document.body?.innerText ?? "")).catch(() => false);
|
|
408
|
-
}
|
|
409
|
-
return true;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
604
|
async function readAssistantProgressSnapshot(page) {
|
|
413
605
|
if (typeof page.evaluate === "function") {
|
|
414
606
|
return page.evaluate(() => {
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { resultError, resultOk } from "../errors.js";
|
|
2
2
|
import { enumerateVisibleMenuItems, findUniqueMenuItem } from "../dom/menus.js";
|
|
3
|
+
import { localeLabels } from "../dom/locale-labels.js";
|
|
3
4
|
import { normalizeLabel, normalizeWhitespace } from "../dom/visible-text.js";
|
|
4
5
|
import { contextFromPage } from "./context.js";
|
|
5
6
|
import { bootstrap } from "./session.js";
|
|
6
7
|
const DEFAULT_MODE_EFFORT = "Thinking";
|
|
7
|
-
const CURRENT_MODE_LABELS = [
|
|
8
|
-
const MODE_OPENER_LABELS = [...CURRENT_MODE_LABELS.filter(label => label !== "Pro"),
|
|
8
|
+
const CURRENT_MODE_LABELS = [...localeLabels.modeLabels];
|
|
9
|
+
const MODE_OPENER_LABELS = [...CURRENT_MODE_LABELS.filter(label => label !== "Pro"), ...localeLabels.modeOpenerExtra];
|
|
10
|
+
const MODEL_VERSION_FAMILY_PATTERN = /^gpt[\s-]/i;
|
|
11
|
+
const MODEL_VERSION_LABEL_PATTERN = /^(?:o\d+|\d+(?:\.\d+)?)$/i;
|
|
12
|
+
const CANONICAL_INTELLIGENCE_ORDER = new Map([
|
|
13
|
+
["instant", 0],
|
|
14
|
+
["medium", 1],
|
|
15
|
+
["high", 2],
|
|
16
|
+
["extra high", 3],
|
|
17
|
+
["pro", 4],
|
|
18
|
+
]);
|
|
9
19
|
export async function setMode(env, args) {
|
|
10
20
|
const boot = await ensurePage(env);
|
|
11
21
|
if (!boot.ok) {
|
|
@@ -14,8 +24,10 @@ export async function setMode(env, args) {
|
|
|
14
24
|
const page = env.page;
|
|
15
25
|
try {
|
|
16
26
|
const requested = requestedModeLabels(args);
|
|
17
|
-
const
|
|
18
|
-
|
|
27
|
+
const requestedVersion = requestedModelVersion(args);
|
|
28
|
+
const requestedForOpening = requestedVersion === undefined ? requested : [...requested, requestedVersion];
|
|
29
|
+
const opened = await waitForModeMenu(page, requestedForOpening, args.timeoutMs ?? 30000);
|
|
30
|
+
if (requestedVersion === undefined && opened.alreadySelected.length === requested.length) {
|
|
19
31
|
return resultOk({ selected: opened.alreadySelected, candidates: opened.modeButtons }, await contextFromPage(page));
|
|
20
32
|
}
|
|
21
33
|
if (!opened.opened) {
|
|
@@ -25,7 +37,7 @@ export async function setMode(env, args) {
|
|
|
25
37
|
const candidates = await enumerateVisibleMenuItems(page);
|
|
26
38
|
const selected = [];
|
|
27
39
|
for (const item of requested) {
|
|
28
|
-
const match =
|
|
40
|
+
const match = findModeMenuItem(candidates, item);
|
|
29
41
|
if (match === undefined) {
|
|
30
42
|
const candidateLabels = candidates.map(candidate => candidate.label);
|
|
31
43
|
return {
|
|
@@ -41,7 +53,22 @@ export async function setMode(env, args) {
|
|
|
41
53
|
}
|
|
42
54
|
selected.push(match.label);
|
|
43
55
|
}
|
|
44
|
-
|
|
56
|
+
let candidateLabels = candidates.map(candidate => candidate.label);
|
|
57
|
+
if (requestedVersion !== undefined) {
|
|
58
|
+
const versionResult = await selectModelVersion(page, requestedVersion, candidates, args.timeoutMs ?? 30000);
|
|
59
|
+
candidateLabels = dedupeLabels([...candidateLabels, ...versionResult.candidates]);
|
|
60
|
+
if (!versionResult.selected) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
status: "unsupported",
|
|
64
|
+
warnings: [],
|
|
65
|
+
blocker: selectorDriftBlocker(`Model version "${requestedVersion}" was not found or was ambiguous.`, candidateLabels),
|
|
66
|
+
context: await contextFromPage(page)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
selected.push(versionResult.selected);
|
|
70
|
+
}
|
|
71
|
+
return resultOk({ selected, candidates: candidateLabels }, await contextFromPage(page));
|
|
45
72
|
}
|
|
46
73
|
catch (error) {
|
|
47
74
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
@@ -77,14 +104,23 @@ export async function selectTool(env, args) {
|
|
|
77
104
|
}
|
|
78
105
|
const page = env.page;
|
|
79
106
|
try {
|
|
80
|
-
const opened = await clickFirstUniqueButton(page, [
|
|
107
|
+
const opened = await clickFirstUniqueButton(page, [...localeLabels.addFilesOpenerCandidates]);
|
|
81
108
|
if (!opened) {
|
|
82
109
|
return selectorDrift(page, "No unique ChatGPT tool menu opener was found.");
|
|
83
110
|
}
|
|
84
111
|
await page.waitForTimeout?.(250);
|
|
85
112
|
const candidates = await enumerateVisibleMenuItems(page);
|
|
86
|
-
const
|
|
87
|
-
|
|
113
|
+
const wantedCandidates = toolLabels(args.tool);
|
|
114
|
+
let match;
|
|
115
|
+
let wanted = wantedCandidates[0] ?? args.tool;
|
|
116
|
+
for (const candidate of wantedCandidates) {
|
|
117
|
+
const found = findUniqueMenuItem(candidates, candidate);
|
|
118
|
+
if (found !== undefined) {
|
|
119
|
+
match = found;
|
|
120
|
+
wanted = candidate;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
88
124
|
if (match === undefined) {
|
|
89
125
|
const candidateLabels = candidates.map(candidate => candidate.label);
|
|
90
126
|
return {
|
|
@@ -139,6 +175,9 @@ async function clickMenuItem(page, label) {
|
|
|
139
175
|
if (await clickModelSwitcherMenuItem(page, label)) {
|
|
140
176
|
return true;
|
|
141
177
|
}
|
|
178
|
+
if (await clickMenuItemByPointer(page, label)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
142
181
|
if (await clickMenuItemByDom(page, label)) {
|
|
143
182
|
return true;
|
|
144
183
|
}
|
|
@@ -149,6 +188,22 @@ async function clickMenuItem(page, label) {
|
|
|
149
188
|
const textLocator = page.getByText?.(label, { exact: true });
|
|
150
189
|
return clickIfUnique(textLocator);
|
|
151
190
|
}
|
|
191
|
+
async function clickMenuItemByPointer(page, label) {
|
|
192
|
+
const point = await menuItemCenter(page, { label });
|
|
193
|
+
if (point === undefined) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const pageWithPointer = page;
|
|
197
|
+
if (pageWithPointer.mouse?.click !== undefined) {
|
|
198
|
+
await pageWithPointer.mouse.click(point.x, point.y);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (pageWithPointer.cua?.click !== undefined) {
|
|
202
|
+
await pageWithPointer.cua.click({ x: point.x, y: point.y });
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
152
207
|
async function clickModelSwitcherMenuItem(page, label) {
|
|
153
208
|
if (typeof page.evaluate !== "function" || typeof page.locator !== "function") {
|
|
154
209
|
return false;
|
|
@@ -203,22 +258,36 @@ async function clickIfUnique(locator) {
|
|
|
203
258
|
await locator.click();
|
|
204
259
|
return true;
|
|
205
260
|
}
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
261
|
+
function toolLabels(tool) {
|
|
262
|
+
const known = localeLabels.tools[tool];
|
|
263
|
+
return known !== undefined ? [...known] : [tool];
|
|
264
|
+
}
|
|
265
|
+
function findModeMenuItem(candidates, wanted) {
|
|
266
|
+
const exact = findUniqueMenuItem(candidates, wanted);
|
|
267
|
+
if (exact !== undefined) {
|
|
268
|
+
return exact;
|
|
269
|
+
}
|
|
270
|
+
const wantedIndex = CANONICAL_INTELLIGENCE_ORDER.get(normalizeLabel(wanted));
|
|
271
|
+
if (wantedIndex === undefined) {
|
|
272
|
+
return undefined;
|
|
216
273
|
}
|
|
274
|
+
const intelligenceItems = candidates.filter(candidate => candidate.role === "menuitemradio"
|
|
275
|
+
&& !MODEL_VERSION_LABEL_PATTERN.test(candidate.label)
|
|
276
|
+
&& !MODEL_VERSION_FAMILY_PATTERN.test(candidate.label));
|
|
277
|
+
return intelligenceItems.length >= CANONICAL_INTELLIGENCE_ORDER.size
|
|
278
|
+
? intelligenceItems[wantedIndex]
|
|
279
|
+
: undefined;
|
|
217
280
|
}
|
|
218
281
|
function requestedModeLabels(args) {
|
|
219
|
-
const requested = [args.model, args.effort].filter((value) => value !== undefined);
|
|
282
|
+
const requested = [args.model, args.intelligence, args.effort].filter((value) => value !== undefined);
|
|
283
|
+
if (requestedModelVersion(args) !== undefined && requested.length === 0) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
220
286
|
return requested.length > 0 ? requested : [DEFAULT_MODE_EFFORT];
|
|
221
287
|
}
|
|
288
|
+
function requestedModelVersion(args) {
|
|
289
|
+
return args.modelVersion ?? args.version;
|
|
290
|
+
}
|
|
222
291
|
function findUniqueVisibleLabel(labels, wanted) {
|
|
223
292
|
const normalized = normalizeLabel(wanted);
|
|
224
293
|
const exact = labels.filter(label => normalizeLabel(label) === normalized);
|
|
@@ -245,6 +314,119 @@ function findAlreadySelectedModes(visibleButtons, requested) {
|
|
|
245
314
|
.map(label => findUniqueVisibleLabel(visibleButtons, label))
|
|
246
315
|
.filter((label) => label !== undefined);
|
|
247
316
|
}
|
|
317
|
+
async function selectModelVersion(page, requestedVersion, currentCandidates, timeoutMs) {
|
|
318
|
+
let candidates = await enumerateVisibleMenuItems(page);
|
|
319
|
+
if (!looksLikeModeMenu(candidates.map(candidate => candidate.label))) {
|
|
320
|
+
const opened = await waitForModeMenu(page, [requestedVersion], timeoutMs);
|
|
321
|
+
if (opened.opened) {
|
|
322
|
+
await page.waitForTimeout?.(250);
|
|
323
|
+
candidates = await enumerateVisibleMenuItems(page);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
let exact = findExactMenuItem(candidates, requestedVersion);
|
|
327
|
+
if (exact !== undefined) {
|
|
328
|
+
return await clickMenuItem(page, exact.label)
|
|
329
|
+
? { selected: exact.label, candidates: candidates.map(candidate => candidate.label) }
|
|
330
|
+
: { candidates: candidates.map(candidate => candidate.label) };
|
|
331
|
+
}
|
|
332
|
+
const opened = await openModelVersionSubmenu(page, currentCandidates);
|
|
333
|
+
candidates = await enumerateVisibleMenuItems(page);
|
|
334
|
+
exact = findExactMenuItem(candidates, requestedVersion);
|
|
335
|
+
if (!opened || exact === undefined) {
|
|
336
|
+
return { candidates: candidates.map(candidate => candidate.label) };
|
|
337
|
+
}
|
|
338
|
+
return await clickMenuItem(page, exact.label)
|
|
339
|
+
? { selected: exact.label, candidates: candidates.map(candidate => candidate.label) }
|
|
340
|
+
: { candidates: candidates.map(candidate => candidate.label) };
|
|
341
|
+
}
|
|
342
|
+
async function openModelVersionSubmenu(page, candidates) {
|
|
343
|
+
const submenuOpeners = candidates.filter(item => item.hasPopup === true || MODEL_VERSION_FAMILY_PATTERN.test(item.label));
|
|
344
|
+
if (submenuOpeners.length === 0) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
for (const candidate of submenuOpeners) {
|
|
348
|
+
if (await openSubmenuByPointer(page, candidate)) {
|
|
349
|
+
await page.waitForTimeout?.(250);
|
|
350
|
+
if (await modelVersionMenuItemsAreVisible(page)) {
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (await clickMenuItem(page, candidate.label)) {
|
|
355
|
+
await page.waitForTimeout?.(250);
|
|
356
|
+
if (await modelVersionMenuItemsAreVisible(page)) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
async function openSubmenuByPointer(page, item) {
|
|
364
|
+
const point = await menuItemCenter(page, item);
|
|
365
|
+
if (point === undefined) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const pageWithMouse = page;
|
|
369
|
+
if (pageWithMouse.mouse?.move !== undefined) {
|
|
370
|
+
await pageWithMouse.mouse.move(point.x, point.y);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
if (pageWithMouse.cua?.move !== undefined) {
|
|
374
|
+
await pageWithMouse.cua.move({ x: point.x, y: point.y });
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
async function menuItemCenter(page, item, roles = ["menuitem", "menuitemradio", "option"]) {
|
|
380
|
+
if (typeof page.evaluate !== "function") {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
const target = { label: item.label, roles };
|
|
384
|
+
if (item.testId !== undefined) {
|
|
385
|
+
target.testId = item.testId;
|
|
386
|
+
}
|
|
387
|
+
return page.evaluate((target) => {
|
|
388
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
389
|
+
const normalizedLabel = normalize(target.label);
|
|
390
|
+
const roleSelector = target.roles.map(role => `[role='${role}']`).join(",");
|
|
391
|
+
const matches = Array.from(document.querySelectorAll(roleSelector))
|
|
392
|
+
.filter(node => {
|
|
393
|
+
const element = node;
|
|
394
|
+
if (target.testId !== undefined && element.getAttribute("data-testid") !== target.testId) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const label = normalize(element.innerText ?? element.textContent ?? "");
|
|
398
|
+
if (label !== normalizedLabel) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
const rect = element.getBoundingClientRect();
|
|
402
|
+
const style = window.getComputedStyle(element);
|
|
403
|
+
return rect.width > 0
|
|
404
|
+
&& rect.height > 0
|
|
405
|
+
&& style.visibility !== "hidden"
|
|
406
|
+
&& style.display !== "none"
|
|
407
|
+
&& style.opacity !== "0";
|
|
408
|
+
});
|
|
409
|
+
if (matches.length !== 1)
|
|
410
|
+
return undefined;
|
|
411
|
+
const rect = matches[0].getBoundingClientRect();
|
|
412
|
+
return {
|
|
413
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
414
|
+
y: Math.round(rect.top + rect.height / 2)
|
|
415
|
+
};
|
|
416
|
+
}, target).catch(() => undefined);
|
|
417
|
+
}
|
|
418
|
+
async function modelVersionMenuItemsAreVisible(page) {
|
|
419
|
+
return (await enumerateVisibleMenuItems(page))
|
|
420
|
+
.some(candidate => candidate.role === "menuitemradio" && MODEL_VERSION_LABEL_PATTERN.test(candidate.label));
|
|
421
|
+
}
|
|
422
|
+
function findExactMenuItem(items, wanted) {
|
|
423
|
+
const normalized = normalizeLabel(wanted);
|
|
424
|
+
const matches = items.filter(item => item.normalized === normalized);
|
|
425
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
426
|
+
}
|
|
427
|
+
function dedupeLabels(labels) {
|
|
428
|
+
return Array.from(new Set(labels));
|
|
429
|
+
}
|
|
248
430
|
async function selectorDrift(page, message, candidates) {
|
|
249
431
|
const visibleText = candidates?.join("\n") ?? await visibleButtonLabels(page);
|
|
250
432
|
return {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommandResult, ProjectSource, ProjectSourcesAddArgs, ProjectSourcesAddData, ProjectSourcesAddPlanData, ProjectSourcesListArgs, ProjectSourcesListData, ProjectSourcesPlanAddArgs, ProjectSourcesUrl, RuntimeEnv } from "../types.js";
|
|
2
|
+
type SafeCandidate = NonNullable<NonNullable<CommandResult["blocker"]>["candidates"]>[number];
|
|
3
|
+
export declare function normalizeProjectSourcesUrl(value: string): ProjectSourcesUrl;
|
|
4
|
+
export declare function buildProjectSourceAddPlan(env: RuntimeEnv, args: ProjectSourcesPlanAddArgs): Promise<CommandResult<ProjectSourcesAddPlanData>>;
|
|
5
|
+
export declare function listProjectSources(env: RuntimeEnv, args: ProjectSourcesListArgs): Promise<CommandResult<ProjectSourcesListData>>;
|
|
6
|
+
export declare function addProjectSources(env: RuntimeEnv, args: ProjectSourcesAddArgs): Promise<CommandResult<ProjectSourcesAddData | ProjectSourcesAddPlanData>>;
|
|
7
|
+
export declare function diffProjectSourceNames(before: ProjectSource[], after: ProjectSource[]): ProjectSource[];
|
|
8
|
+
export declare function extractProjectSourcesFromHtml(html: string): ProjectSource[];
|
|
9
|
+
export declare function safeProjectSourceCandidatesFromHtml(html: string): SafeCandidate[];
|
|
10
|
+
export {};
|