codex-chatgpt-control 0.5.1-alpha.1 → 0.5.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/contracts/v1/fixtures/backend-capabilities.json +1 -0
- package/contracts/v1/fixtures/command-descriptors.json +21 -0
- package/contracts/v1/fixtures/doctor-bridge-upload.json +2 -2
- package/contracts/v1/fixtures/doctor-scenario-preflight.json +26 -4
- package/contracts/v1/fixtures/files-attach-indeterminate.json +30 -0
- package/contracts/v1/fixtures/help-root.json +1 -1
- package/contracts/v1/fixtures/messages-stop-indeterminate.json +31 -0
- package/contracts/v1/fixtures/messages-stop-needs-confirmation.json +20 -0
- package/contracts/v1/fixtures/messages-stop-noop.json +19 -0
- package/contracts/v1/fixtures/messages-stop-success.json +21 -0
- package/contracts/v1/fixtures/primitive-bootstrap-blocker.json +3 -3
- package/contracts/v1/fixtures/reports-create-redacted.json +3 -3
- package/contracts/v1/fixtures/reports-summarize-redacted.json +1 -1
- package/contracts/v1/fixtures/run-browser-bridge-blocker.json +12 -12
- package/contracts/v1/fixtures/stream-blocked.ndjson +2 -2
- package/contracts/v1/manifest.json +25 -0
- package/contracts/v1/parity-suite.json +8 -1
- package/dist/codex-chatgpt-control-backend.mjs +2259 -498
- package/dist/codex-chatgpt-control.bundle.mjs +2263 -472
- package/dist/src/backend/client.d.ts +2 -1
- package/dist/src/backend/client.js +1 -0
- package/dist/src/backend/protocol.d.ts +1 -1
- package/dist/src/backend/protocol.js +1 -0
- package/dist/src/backend/session.js +2 -0
- package/dist/src/browser/attach.d.ts +1 -0
- package/dist/src/browser/attach.js +69 -35
- package/dist/src/browser/chatgpt-url.d.ts +8 -0
- package/dist/src/browser/chatgpt-url.js +28 -0
- package/dist/src/browser/page-state.js +211 -4
- package/dist/src/client.d.ts +2 -1
- package/dist/src/client.js +2 -1
- package/dist/src/commands/configuration.js +46 -12
- package/dist/src/commands/context.d.ts +5 -1
- package/dist/src/commands/context.js +2 -2
- package/dist/src/commands/conversation.js +13 -1
- package/dist/src/commands/doctor.js +24 -2
- package/dist/src/commands/files.d.ts +1 -0
- package/dist/src/commands/files.js +742 -191
- package/dist/src/commands/messages.d.ts +2 -1
- package/dist/src/commands/messages.js +310 -2
- package/dist/src/commands/modes.js +197 -52
- package/dist/src/commands/registry.js +3 -0
- package/dist/src/commands/sequence.js +3 -1
- package/dist/src/commands/session.d.ts +4 -1
- package/dist/src/commands/session.js +34 -3
- package/dist/src/dom/generation-state.d.ts +6 -1
- package/dist/src/dom/generation-state.js +206 -45
- package/dist/src/dom/locale/am.d.ts +10 -1
- package/dist/src/dom/locale/am.js +10 -1
- package/dist/src/dom/locale/ar.d.ts +8 -1
- package/dist/src/dom/locale/ar.js +9 -2
- package/dist/src/dom/locale/bg.d.ts +6 -1
- package/dist/src/dom/locale/bg.js +6 -1
- package/dist/src/dom/locale/bn.d.ts +10 -1
- package/dist/src/dom/locale/bn.js +10 -1
- package/dist/src/dom/locale/bs.d.ts +5 -1
- package/dist/src/dom/locale/bs.js +5 -1
- package/dist/src/dom/locale/ca.d.ts +8 -1
- package/dist/src/dom/locale/ca.js +8 -1
- package/dist/src/dom/locale/cs.d.ts +5 -1
- package/dist/src/dom/locale/cs.js +6 -2
- package/dist/src/dom/locale/da.d.ts +5 -1
- package/dist/src/dom/locale/da.js +6 -2
- package/dist/src/dom/locale/de.d.ts +4 -1
- package/dist/src/dom/locale/de.js +5 -2
- package/dist/src/dom/locale/el.d.ts +5 -1
- package/dist/src/dom/locale/el.js +6 -2
- package/dist/src/dom/locale/en.d.ts +2 -1
- package/dist/src/dom/locale/en.js +2 -1
- package/dist/src/dom/locale/es-419.d.ts +5 -1
- package/dist/src/dom/locale/es-419.js +5 -1
- package/dist/src/dom/locale/es-ES.d.ts +5 -1
- package/dist/src/dom/locale/es-ES.js +5 -1
- package/dist/src/dom/locale/et.d.ts +4 -1
- package/dist/src/dom/locale/et.js +5 -2
- package/dist/src/dom/locale/fa.d.ts +9 -1
- package/dist/src/dom/locale/fa.js +10 -2
- package/dist/src/dom/locale/fi.d.ts +5 -1
- package/dist/src/dom/locale/fi.js +6 -2
- package/dist/src/dom/locale/fr-CA.d.ts +4 -1
- package/dist/src/dom/locale/fr-CA.js +4 -1
- package/dist/src/dom/locale/fr-FR.d.ts +5 -1
- package/dist/src/dom/locale/fr-FR.js +7 -3
- package/dist/src/dom/locale/gu.d.ts +10 -1
- package/dist/src/dom/locale/gu.js +10 -1
- package/dist/src/dom/locale/hi.d.ts +10 -1
- package/dist/src/dom/locale/hi.js +11 -2
- package/dist/src/dom/locale/hr.d.ts +4 -1
- package/dist/src/dom/locale/hr.js +4 -1
- package/dist/src/dom/locale/hu.d.ts +4 -1
- package/dist/src/dom/locale/hu.js +5 -2
- package/dist/src/dom/locale/hy.d.ts +10 -1
- package/dist/src/dom/locale/hy.js +11 -2
- package/dist/src/dom/locale/id.d.ts +5 -1
- package/dist/src/dom/locale/id.js +5 -1
- package/dist/src/dom/locale/index.js +1 -0
- package/dist/src/dom/locale/is.d.ts +9 -1
- package/dist/src/dom/locale/is.js +10 -2
- package/dist/src/dom/locale/it.d.ts +4 -1
- package/dist/src/dom/locale/it.js +6 -3
- package/dist/src/dom/locale/ja.d.ts +6 -1
- package/dist/src/dom/locale/ja.js +7 -2
- package/dist/src/dom/locale/ka.d.ts +10 -1
- package/dist/src/dom/locale/ka.js +11 -2
- package/dist/src/dom/locale/kk.d.ts +10 -1
- package/dist/src/dom/locale/kk.js +11 -2
- package/dist/src/dom/locale/kn.d.ts +9 -1
- package/dist/src/dom/locale/kn.js +10 -2
- package/dist/src/dom/locale/ko.d.ts +6 -1
- package/dist/src/dom/locale/ko.js +7 -2
- package/dist/src/dom/locale/lt.d.ts +5 -1
- package/dist/src/dom/locale/lt.js +6 -2
- package/dist/src/dom/locale/lv.d.ts +5 -1
- package/dist/src/dom/locale/lv.js +5 -1
- package/dist/src/dom/locale/mk.d.ts +9 -1
- package/dist/src/dom/locale/mk.js +10 -2
- package/dist/src/dom/locale/ml.d.ts +10 -1
- package/dist/src/dom/locale/ml.js +10 -1
- package/dist/src/dom/locale/mn.d.ts +10 -1
- package/dist/src/dom/locale/mn.js +13 -4
- package/dist/src/dom/locale/mr.d.ts +10 -1
- package/dist/src/dom/locale/mr.js +10 -1
- package/dist/src/dom/locale/ms.d.ts +9 -1
- package/dist/src/dom/locale/ms.js +9 -1
- package/dist/src/dom/locale/my.d.ts +9 -1
- package/dist/src/dom/locale/my.js +9 -1
- package/dist/src/dom/locale/nb.d.ts +5 -1
- package/dist/src/dom/locale/nb.js +6 -2
- package/dist/src/dom/locale/nl.d.ts +8 -2
- package/dist/src/dom/locale/nl.js +8 -2
- package/dist/src/dom/locale/pa.d.ts +10 -1
- package/dist/src/dom/locale/pa.js +10 -1
- package/dist/src/dom/locale/pl.d.ts +5 -1
- package/dist/src/dom/locale/pl.js +6 -2
- package/dist/src/dom/locale/pt-BR.d.ts +5 -1
- package/dist/src/dom/locale/pt-BR.js +6 -2
- package/dist/src/dom/locale/pt-PT.d.ts +5 -1
- package/dist/src/dom/locale/pt-PT.js +5 -1
- package/dist/src/dom/locale/ro.d.ts +4 -1
- package/dist/src/dom/locale/ro.js +4 -1
- package/dist/src/dom/locale/ru.d.ts +8 -1
- package/dist/src/dom/locale/ru.js +9 -2
- package/dist/src/dom/locale/sk.d.ts +5 -1
- package/dist/src/dom/locale/sk.js +5 -1
- package/dist/src/dom/locale/sl.d.ts +9 -1
- package/dist/src/dom/locale/sl.js +9 -1
- package/dist/src/dom/locale/so.d.ts +9 -1
- package/dist/src/dom/locale/so.js +9 -1
- package/dist/src/dom/locale/sq.d.ts +10 -1
- package/dist/src/dom/locale/sq.js +12 -3
- package/dist/src/dom/locale/sr.d.ts +9 -1
- package/dist/src/dom/locale/sr.js +10 -2
- package/dist/src/dom/locale/sv.d.ts +4 -1
- package/dist/src/dom/locale/sv.js +4 -1
- package/dist/src/dom/locale/sw.d.ts +5 -1
- package/dist/src/dom/locale/sw.js +7 -3
- package/dist/src/dom/locale/ta.d.ts +10 -1
- package/dist/src/dom/locale/ta.js +12 -3
- package/dist/src/dom/locale/te.d.ts +10 -1
- package/dist/src/dom/locale/te.js +11 -2
- package/dist/src/dom/locale/th.d.ts +6 -1
- package/dist/src/dom/locale/th.js +6 -1
- package/dist/src/dom/locale/tl.d.ts +1 -1
- package/dist/src/dom/locale/tl.js +2 -2
- package/dist/src/dom/locale/tr.d.ts +9 -1
- package/dist/src/dom/locale/tr.js +10 -2
- package/dist/src/dom/locale/types.d.ts +1 -1
- package/dist/src/dom/locale/uk.d.ts +10 -1
- package/dist/src/dom/locale/uk.js +11 -2
- package/dist/src/dom/locale/ur.d.ts +9 -1
- package/dist/src/dom/locale/ur.js +9 -1
- package/dist/src/dom/locale/vi.d.ts +9 -1
- package/dist/src/dom/locale/vi.js +10 -2
- package/dist/src/dom/locale/zh-HK.d.ts +8 -1
- package/dist/src/dom/locale/zh-HK.js +9 -2
- package/dist/src/dom/locale/zh-Hans.d.ts +10 -1
- package/dist/src/dom/locale/zh-Hans.js +11 -2
- package/dist/src/dom/locale/zh-TW.d.ts +10 -1
- package/dist/src/dom/locale/zh-TW.js +11 -2
- package/dist/src/dom/selectors.d.ts +1 -0
- package/dist/src/dom/selectors.js +9 -1
- package/dist/src/dom/wait-snapshot.d.ts +1 -0
- package/dist/src/dom/wait-snapshot.js +45 -22
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js +2 -2
- package/dist/src/safety/risk.d.ts +1 -0
- package/dist/src/safety/risk.js +1 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +2 -2
- package/dist/src/scripts/apply-intelligence-locale-captures.js +54 -14
- package/dist/src/scripts/capture-intelligence-locales.d.ts +114 -1
- package/dist/src/scripts/capture-intelligence-locales.js +437 -304
- package/dist/src/scripts/live-smoke/scenarios.d.ts +2 -1
- package/dist/src/scripts/live-smoke/scenarios.js +9 -10
- package/dist/src/scripts/locale-capture/surface-graph.d.ts +7 -2
- package/dist/src/scripts/locale-capture/surface-graph.js +7 -0
- package/dist/src/types.d.ts +27 -6
- package/package.json +2 -1
- package/references/2026-08-08-model-selection-locale-sweep.md +32 -0
- package/references/backend-protocol.md +11 -3
- package/references/language-coverage.md +14 -14
- package/references/localization.md +17 -13
- package/references/python-parity.md +8 -2
- package/references/troubleshooting.md +10 -2
|
@@ -63,7 +63,8 @@ export async function setMode(env, args) {
|
|
|
63
63
|
return selectorDrift(page, "No unique ChatGPT mode menu opener was found.");
|
|
64
64
|
}
|
|
65
65
|
await page.waitForTimeout?.(250);
|
|
66
|
-
|
|
66
|
+
let candidates = await enumerateVisibleMenuItems(page);
|
|
67
|
+
const observedCandidates = [...candidates];
|
|
67
68
|
const selected = [];
|
|
68
69
|
if (requested.length > 0 && shouldRejectAsWrongModeMenu(candidates)) {
|
|
69
70
|
const candidateLabels = candidates.map(candidate => candidate.label);
|
|
@@ -76,9 +77,20 @@ export async function setMode(env, args) {
|
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
for (const request of requested) {
|
|
79
|
-
|
|
80
|
+
let match = findModeMenuItem(candidates, request);
|
|
81
|
+
if (match === undefined) {
|
|
82
|
+
const sliderSelection = await selectModeWithPowerSlider(page, request);
|
|
83
|
+
if (sliderSelection !== undefined) {
|
|
84
|
+
selected.push(sliderSelection);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const nested = await openEffortSubmenu(page, candidates, request);
|
|
88
|
+
observedCandidates.push(...nested);
|
|
89
|
+
candidates = nested;
|
|
90
|
+
match = findModeMenuItem(candidates, request);
|
|
91
|
+
}
|
|
80
92
|
if (match === undefined) {
|
|
81
|
-
const candidateLabels =
|
|
93
|
+
const candidateLabels = dedupeLabels(observedCandidates.map(candidate => candidate.label));
|
|
82
94
|
return {
|
|
83
95
|
ok: false,
|
|
84
96
|
status: "unsupported",
|
|
@@ -87,12 +99,12 @@ export async function setMode(env, args) {
|
|
|
87
99
|
context: await contextFromPage(page)
|
|
88
100
|
};
|
|
89
101
|
}
|
|
90
|
-
if (!await
|
|
102
|
+
if (!await clickResolvedMenuItem(page, match)) {
|
|
91
103
|
return selectorDrift(page, `Mode option "${match.label}" was visible but could not be clicked.`, candidates.map(candidate => candidate.label));
|
|
92
104
|
}
|
|
93
105
|
selected.push(match.label);
|
|
94
106
|
}
|
|
95
|
-
let candidateLabels =
|
|
107
|
+
let candidateLabels = dedupeLabels(observedCandidates.map(candidate => candidate.label));
|
|
96
108
|
if (requestedVersion !== undefined) {
|
|
97
109
|
const versionResult = await selectModelVersion(page, requestedVersion, candidates, args.timeoutMs ?? 30000);
|
|
98
110
|
candidateLabels = dedupeLabels([...candidateLabels, ...versionResult.candidates]);
|
|
@@ -114,6 +126,87 @@ export async function setMode(env, args) {
|
|
|
114
126
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
115
127
|
}
|
|
116
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Chat's compact picker exposes the five canonical intelligence levels as an
|
|
131
|
+
* ARIA slider. Use it only when its visible min/max/current metadata exactly
|
|
132
|
+
* matches that five-step scale, and accept the move only when the composer
|
|
133
|
+
* visibly echoes the requested label. The Advanced submenu remains the
|
|
134
|
+
* fallback for every unrecognized shape.
|
|
135
|
+
*/
|
|
136
|
+
async function selectModeWithPowerSlider(page, request) {
|
|
137
|
+
const wanted = request.modeId === undefined
|
|
138
|
+
? undefined
|
|
139
|
+
: CANONICAL_INTELLIGENCE_ORDER.get(request.modeId);
|
|
140
|
+
const slider = page.locator?.("[role='slider'][aria-valuemin='0'][aria-valuemax='4']");
|
|
141
|
+
if (wanted === undefined
|
|
142
|
+
|| slider?.count === undefined
|
|
143
|
+
|| slider.evaluate === undefined
|
|
144
|
+
|| slider.press === undefined
|
|
145
|
+
|| slider.isVisible === undefined
|
|
146
|
+
|| !await slider.isVisible().catch(() => false)
|
|
147
|
+
|| await slider.count().catch(() => 0) !== 1) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const state = await slider.evaluate(element => ({
|
|
151
|
+
min: Number(element.getAttribute("aria-valuemin")),
|
|
152
|
+
max: Number(element.getAttribute("aria-valuemax")),
|
|
153
|
+
now: Number(element.getAttribute("aria-valuenow"))
|
|
154
|
+
})).catch(() => undefined);
|
|
155
|
+
if (state === undefined
|
|
156
|
+
|| state.min !== 0
|
|
157
|
+
|| state.max !== CANONICAL_INTELLIGENCE_ORDER.size - 1
|
|
158
|
+
|| !Number.isInteger(state.now)
|
|
159
|
+
|| state.now < state.min
|
|
160
|
+
|| state.now > state.max) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
const key = wanted > state.now ? "ArrowRight" : "ArrowLeft";
|
|
164
|
+
for (let step = 0; step < Math.abs(wanted - state.now); step += 1) {
|
|
165
|
+
await slider.press(key);
|
|
166
|
+
}
|
|
167
|
+
await page.waitForTimeout?.(150);
|
|
168
|
+
return findUniqueVisibleLabelForRequest(await visibleModeButtonLabelList(page), request);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Current Chat exposes the intelligence choices inside an Advanced "Effort <value>"
|
|
172
|
+
* submenu. Only traverse a uniquely labelled visible Effort/Intelligence row and
|
|
173
|
+
* only accept the submenu when it contains the exact requested mode. This keeps the
|
|
174
|
+
* legacy flat picker working while avoiding any inference from the five-position
|
|
175
|
+
* power slider.
|
|
176
|
+
*/
|
|
177
|
+
async function openEffortSubmenu(page, rootItems, request) {
|
|
178
|
+
const axisLabels = [
|
|
179
|
+
...localeLabels.configurationAxes.effort,
|
|
180
|
+
...localeLabels.configurationAxes.intelligence,
|
|
181
|
+
].map(normalizeForLabelMatch);
|
|
182
|
+
const effortRows = (items) => items.filter(item => {
|
|
183
|
+
if (item.role === "menuitemradio")
|
|
184
|
+
return false;
|
|
185
|
+
const normalized = normalizeForLabelMatch(item.label);
|
|
186
|
+
return axisLabels.some(axis => normalized === axis || normalized.startsWith(`${axis} `));
|
|
187
|
+
});
|
|
188
|
+
let visibleRootItems = rootItems;
|
|
189
|
+
let rows = effortRows(visibleRootItems);
|
|
190
|
+
if (rows.length === 0) {
|
|
191
|
+
const advancedLabels = localeLabels.configurationAxes.advanced.map(normalizeForLabelMatch);
|
|
192
|
+
const advancedRows = visibleRootItems.filter(item => {
|
|
193
|
+
const normalized = normalizeForLabelMatch(item.label);
|
|
194
|
+
return advancedLabels.some(label => normalized === label || visibleLabelMatches(item.label, label));
|
|
195
|
+
});
|
|
196
|
+
if (advancedRows.length !== 1 || !await clickResolvedMenuItem(page, advancedRows[0])) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
await page.waitForTimeout?.(250);
|
|
200
|
+
visibleRootItems = await enumerateVisibleMenuItems(page);
|
|
201
|
+
rows = effortRows(visibleRootItems);
|
|
202
|
+
}
|
|
203
|
+
if (rows.length !== 1 || !await clickResolvedMenuItem(page, rows[0])) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
await page.waitForTimeout?.(250);
|
|
207
|
+
const nested = await enumerateVisibleMenuItems(page);
|
|
208
|
+
return findModeMenuItem(nested, request) === undefined ? [] : nested;
|
|
209
|
+
}
|
|
117
210
|
/**
|
|
118
211
|
* Post-condition check: after clicking mode rows, the composer's mode-labelled controls
|
|
119
212
|
* should reflect the requested selection. A mismatch does not fail the command — some
|
|
@@ -216,7 +309,7 @@ export async function selectTool(env, args) {
|
|
|
216
309
|
context: await contextFromPage(page)
|
|
217
310
|
};
|
|
218
311
|
}
|
|
219
|
-
if (!await clickMenuItem(page, match
|
|
312
|
+
if (!await clickMenuItem(page, match)) {
|
|
220
313
|
return selectorDrift(page, `Tool "${match.label}" was visible but could not be clicked.`, candidates.map(candidate => candidate.label));
|
|
221
314
|
}
|
|
222
315
|
return resultOk({ selected: match.label, candidates: candidates.map(candidate => candidate.label) }, await contextFromPage(page));
|
|
@@ -295,25 +388,24 @@ function shouldRejectAsWrongModeMenu(items) {
|
|
|
295
388
|
}
|
|
296
389
|
return items.some(item => isThreadActionLabel(item.label));
|
|
297
390
|
}
|
|
298
|
-
async function clickMenuItem(page,
|
|
299
|
-
if (await clickModelSwitcherMenuItem(page,
|
|
391
|
+
async function clickMenuItem(page, item) {
|
|
392
|
+
if (await clickModelSwitcherMenuItem(page, item)) {
|
|
300
393
|
return true;
|
|
301
394
|
}
|
|
302
|
-
if (await clickMenuItemByPointer(page,
|
|
395
|
+
if (await clickMenuItemByPointer(page, item)) {
|
|
303
396
|
return true;
|
|
304
397
|
}
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (await clickIfUnique(roleLocator)) {
|
|
310
|
-
return true;
|
|
398
|
+
if (item.role !== undefined) {
|
|
399
|
+
const accessibleName = item.ariaLabel ?? item.label;
|
|
400
|
+
if (await clickIfUniqueMenuControl(page.getByRole?.(item.role, { name: accessibleName, exact: true }), item))
|
|
401
|
+
return true;
|
|
311
402
|
}
|
|
312
|
-
const
|
|
313
|
-
|
|
403
|
+
const roleLocator = page.locator?.("[role='menuitem'], [role='menuitemradio'], [role='option']")
|
|
404
|
+
?.filter?.({ hasText: new RegExp(`^\\s*${escapeRegExp(item.label)}\\s*$`, "i") });
|
|
405
|
+
return clickIfUniqueMenuControl(roleLocator, item);
|
|
314
406
|
}
|
|
315
|
-
async function clickMenuItemByPointer(page,
|
|
316
|
-
const point = await menuItemCenter(page,
|
|
407
|
+
async function clickMenuItemByPointer(page, item) {
|
|
408
|
+
const point = await menuItemCenter(page, item);
|
|
317
409
|
if (point === undefined) {
|
|
318
410
|
return false;
|
|
319
411
|
}
|
|
@@ -328,12 +420,12 @@ async function clickMenuItemByPointer(page, label) {
|
|
|
328
420
|
}
|
|
329
421
|
return false;
|
|
330
422
|
}
|
|
331
|
-
async function clickModelSwitcherMenuItem(page,
|
|
423
|
+
async function clickModelSwitcherMenuItem(page, item) {
|
|
332
424
|
if (typeof page.evaluate !== "function" || typeof page.locator !== "function") {
|
|
333
425
|
return false;
|
|
334
426
|
}
|
|
335
427
|
const testId = await page.evaluate((wanted) => {
|
|
336
|
-
const normalizedWanted = wanted.replace(/\s+/g, " ").trim().toLowerCase();
|
|
428
|
+
const normalizedWanted = wanted.label.replace(/\s+/g, " ").trim().toLowerCase();
|
|
337
429
|
const candidates = Array.from(document.querySelectorAll("[data-testid^='model-switcher-']"));
|
|
338
430
|
const matches = candidates
|
|
339
431
|
.filter(node => {
|
|
@@ -341,35 +433,19 @@ async function clickModelSwitcherMenuItem(page, label) {
|
|
|
341
433
|
const candidateTestId = element.getAttribute("data-testid") ?? "";
|
|
342
434
|
if (candidateTestId.endsWith("-effort"))
|
|
343
435
|
return false;
|
|
436
|
+
if (wanted.testId !== undefined && candidateTestId !== wanted.testId)
|
|
437
|
+
return false;
|
|
344
438
|
const text = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
345
439
|
return text === normalizedWanted;
|
|
346
440
|
})
|
|
347
441
|
.map(node => node.getAttribute("data-testid"))
|
|
348
442
|
.filter((value) => value !== null);
|
|
349
443
|
return matches.length === 1 ? matches[0] : undefined;
|
|
350
|
-
}, label).catch(() => undefined);
|
|
444
|
+
}, item.testId === undefined ? { label: item.label } : { label: item.label, testId: item.testId }).catch(() => undefined);
|
|
351
445
|
if (testId === undefined) {
|
|
352
446
|
return false;
|
|
353
447
|
}
|
|
354
|
-
return
|
|
355
|
-
}
|
|
356
|
-
async function clickMenuItemByDom(page, label) {
|
|
357
|
-
if (typeof page.evaluate !== "function") {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
return page.evaluate((wanted) => {
|
|
361
|
-
const normalizedWanted = wanted.replace(/\s+/g, " ").trim().toLowerCase();
|
|
362
|
-
const candidates = Array.from(document.querySelectorAll("[role='menuitem'], [role='menuitemradio'], [role='option']"));
|
|
363
|
-
const matches = candidates.filter(node => {
|
|
364
|
-
const element = node;
|
|
365
|
-
const text = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
366
|
-
return text === normalizedWanted;
|
|
367
|
-
});
|
|
368
|
-
if (matches.length !== 1)
|
|
369
|
-
return false;
|
|
370
|
-
matches[0].click();
|
|
371
|
-
return true;
|
|
372
|
-
}, label).catch(() => false);
|
|
448
|
+
return clickIfUniqueMenuControl(page.locator(`[data-testid="${escapeAttributeValue(testId)}"]`), item);
|
|
373
449
|
}
|
|
374
450
|
async function clickIfUnique(locator) {
|
|
375
451
|
if (locator === undefined || typeof locator.count !== "function" || typeof locator.click !== "function") {
|
|
@@ -382,6 +458,46 @@ async function clickIfUnique(locator) {
|
|
|
382
458
|
await locator.click();
|
|
383
459
|
return true;
|
|
384
460
|
}
|
|
461
|
+
async function clickIfUniqueMenuControl(locator, item) {
|
|
462
|
+
if (locator === undefined
|
|
463
|
+
|| typeof locator.count !== "function"
|
|
464
|
+
|| typeof locator.evaluate !== "function"
|
|
465
|
+
|| typeof locator.click !== "function") {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
if (await locator.count().catch(() => 0) !== 1)
|
|
469
|
+
return false;
|
|
470
|
+
const safe = await locator.evaluate((element) => {
|
|
471
|
+
const control = element;
|
|
472
|
+
if (control.disabled || control.getAttribute("aria-disabled") === "true")
|
|
473
|
+
return false;
|
|
474
|
+
if (control.hidden || control.closest("[hidden], [inert], [aria-hidden='true']") !== null)
|
|
475
|
+
return false;
|
|
476
|
+
const style = window.getComputedStyle(control);
|
|
477
|
+
const rect = control.getBoundingClientRect();
|
|
478
|
+
if (rect.width <= 0 || rect.height <= 0
|
|
479
|
+
|| style.display === "none"
|
|
480
|
+
|| style.visibility === "hidden"
|
|
481
|
+
|| style.opacity === "0"
|
|
482
|
+
|| style.pointerEvents === "none")
|
|
483
|
+
return false;
|
|
484
|
+
const containers = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper]")).filter(container => {
|
|
485
|
+
if (container.hidden || container.closest("[hidden], [inert], [aria-hidden='true']") !== null)
|
|
486
|
+
return false;
|
|
487
|
+
const containerStyle = window.getComputedStyle(container);
|
|
488
|
+
const containerRect = container.getBoundingClientRect();
|
|
489
|
+
return containerRect.width > 0 && containerRect.height > 0
|
|
490
|
+
&& containerStyle.display !== "none"
|
|
491
|
+
&& containerStyle.visibility !== "hidden"
|
|
492
|
+
&& containerStyle.opacity !== "0";
|
|
493
|
+
});
|
|
494
|
+
return containers.length > 0 && containers.some(container => container.contains(control));
|
|
495
|
+
}).catch(() => false);
|
|
496
|
+
if (!safe)
|
|
497
|
+
return false;
|
|
498
|
+
await locator.click();
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
385
501
|
function toolLabels(tool) {
|
|
386
502
|
const known = localeLabels.tools[tool];
|
|
387
503
|
return known !== undefined ? [...known] : [tool];
|
|
@@ -524,13 +640,13 @@ function isModelVersionSubmenuOpener(item) {
|
|
|
524
640
|
|| (item.role !== "menuitemradio" && MODEL_VERSION_FAMILY_PATTERN.test(item.label));
|
|
525
641
|
}
|
|
526
642
|
async function clickResolvedMenuItem(page, item) {
|
|
527
|
-
if (item.testId !== undefined && await
|
|
643
|
+
if (item.testId !== undefined && await clickIfUniqueMenuControl(page.locator?.(`[data-testid="${escapeAttributeValue(item.testId)}"]`), item)) {
|
|
528
644
|
return true;
|
|
529
645
|
}
|
|
530
|
-
if (item.role !== undefined && await
|
|
646
|
+
if (item.role !== undefined && await clickIfUniqueMenuControl(page.getByRole?.(item.role, { name: item.ariaLabel ?? item.label, exact: true }), item)) {
|
|
531
647
|
return true;
|
|
532
648
|
}
|
|
533
|
-
return clickMenuItem(page, item
|
|
649
|
+
return clickMenuItem(page, item);
|
|
534
650
|
}
|
|
535
651
|
async function openModelVersionSubmenu(page, candidates) {
|
|
536
652
|
const submenuOpeners = candidates.filter(item => item.hasPopup === true || MODEL_VERSION_FAMILY_PATTERN.test(item.label));
|
|
@@ -544,7 +660,7 @@ async function openModelVersionSubmenu(page, candidates) {
|
|
|
544
660
|
return true;
|
|
545
661
|
}
|
|
546
662
|
}
|
|
547
|
-
if (await clickMenuItem(page, candidate
|
|
663
|
+
if (await clickMenuItem(page, candidate)) {
|
|
548
664
|
await page.waitForTimeout?.(250);
|
|
549
665
|
if (await modelVersionMenuItemsAreVisible(page)) {
|
|
550
666
|
return true;
|
|
@@ -581,23 +697,34 @@ async function menuItemCenter(page, item, roles = ["menuitem", "menuitemradio",
|
|
|
581
697
|
const normalize = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
582
698
|
const normalizedLabel = normalize(target.label);
|
|
583
699
|
const roleSelector = target.roles.map(role => `[role='${role}']`).join(",");
|
|
700
|
+
const visible = (element) => {
|
|
701
|
+
if (element.hidden || element.closest("[hidden], [inert], [aria-hidden='true']") !== null)
|
|
702
|
+
return false;
|
|
703
|
+
const rect = element.getBoundingClientRect();
|
|
704
|
+
const style = window.getComputedStyle(element);
|
|
705
|
+
return rect.width > 0
|
|
706
|
+
&& rect.height > 0
|
|
707
|
+
&& style.visibility !== "hidden"
|
|
708
|
+
&& style.display !== "none"
|
|
709
|
+
&& style.opacity !== "0"
|
|
710
|
+
&& style.pointerEvents !== "none";
|
|
711
|
+
};
|
|
712
|
+
const containers = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper]")).filter(visible);
|
|
584
713
|
const matches = Array.from(document.querySelectorAll(roleSelector))
|
|
585
714
|
.filter(node => {
|
|
586
715
|
const element = node;
|
|
587
716
|
if (target.testId !== undefined && element.getAttribute("data-testid") !== target.testId) {
|
|
588
717
|
return false;
|
|
589
718
|
}
|
|
719
|
+
if (element.disabled || element.getAttribute("aria-disabled") === "true")
|
|
720
|
+
return false;
|
|
590
721
|
const label = normalize(element.innerText ?? element.textContent ?? "");
|
|
591
722
|
if (label !== normalizedLabel) {
|
|
592
723
|
return false;
|
|
593
724
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
&& rect.height > 0
|
|
598
|
-
&& style.visibility !== "hidden"
|
|
599
|
-
&& style.display !== "none"
|
|
600
|
-
&& style.opacity !== "0";
|
|
725
|
+
return visible(element)
|
|
726
|
+
&& containers.length > 0
|
|
727
|
+
&& containers.some(container => container.contains(element));
|
|
601
728
|
});
|
|
602
729
|
if (matches.length !== 1)
|
|
603
730
|
return undefined;
|
|
@@ -679,6 +806,24 @@ async function visibleModeButtonLabelList(page) {
|
|
|
679
806
|
return Array.from(document.querySelectorAll("button, [role='button']"))
|
|
680
807
|
.map(node => {
|
|
681
808
|
const element = node;
|
|
809
|
+
if (element.hidden
|
|
810
|
+
|| element.getAttribute("aria-hidden") === "true"
|
|
811
|
+
|| (typeof element.closest === "function"
|
|
812
|
+
&& element.closest("[hidden], [inert], [aria-hidden='true']") !== null))
|
|
813
|
+
return "";
|
|
814
|
+
const style = typeof window?.getComputedStyle === "function"
|
|
815
|
+
? window.getComputedStyle(element)
|
|
816
|
+
: undefined;
|
|
817
|
+
if (style?.display === "none"
|
|
818
|
+
|| style?.visibility === "hidden"
|
|
819
|
+
|| style?.opacity === "0"
|
|
820
|
+
|| style?.pointerEvents === "none")
|
|
821
|
+
return "";
|
|
822
|
+
const rect = typeof element.getBoundingClientRect === "function"
|
|
823
|
+
? element.getBoundingClientRect()
|
|
824
|
+
: { width: 1, height: 1 };
|
|
825
|
+
if (rect.width <= 0 && rect.height <= 0)
|
|
826
|
+
return "";
|
|
682
827
|
if (scopedRoots.length > 0 && !scopedRoots.some(root => root.contains(node)))
|
|
683
828
|
return "";
|
|
684
829
|
const visibleText = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
@@ -84,6 +84,7 @@ const descriptors = [
|
|
|
84
84
|
primitive("messages.wait", "Wait for the latest assistant response to stabilize.", 120000),
|
|
85
85
|
primitive("messages.readLatest", "Read the latest message as Markdown, normalized text, blocks, or HTML.", 30000),
|
|
86
86
|
primitive("messages.status", "Read a compact latest-assistant progress snapshot without treating partial text as complete.", 5000),
|
|
87
|
+
primitive("messages.stop", "Stop the current visible ChatGPT response after explicit confirmation and verify generation became inactive.", 5000),
|
|
87
88
|
primitive("messages.waitAndRead", "Wait for completion and read the latest message.", 120000),
|
|
88
89
|
primitive("artifacts.listLatest", "Detect the latest visible generated ChatGPT artifact, such as an image-only result.", 30000),
|
|
89
90
|
primitive("artifacts.wait", "Wait for a visible generated ChatGPT artifact to appear and stabilize.", 120000),
|
|
@@ -273,6 +274,8 @@ function primitiveArgs(name) {
|
|
|
273
274
|
return { role: "assistant or user", format: "markdown, normalized_text, visible_text, html, blocks, or all" };
|
|
274
275
|
if (name === "messages.status")
|
|
275
276
|
return { maxPreviewChars: "maximum latest-assistant preview characters to return; defaults to 240" };
|
|
277
|
+
if (name === "messages.stop")
|
|
278
|
+
return { confirmStop: "must be true to stop a visible response", timeoutMs: "maximum time to verify generation became inactive" };
|
|
276
279
|
if (name === "experience.detect")
|
|
277
280
|
return { timeoutMs: "optional timeout for attaching to the ChatGPT page" };
|
|
278
281
|
if (name === "experience.open")
|
|
@@ -3,7 +3,7 @@ import { applyConfiguration, inspectConfiguration } from "./configuration.js";
|
|
|
3
3
|
import { detectExperience, openExperience } from "./experience.js";
|
|
4
4
|
import { attachFiles, downloadLatestFile } from "./files.js";
|
|
5
5
|
import { addProjectSources, buildProjectSourceAddPlan, listProjectSources } from "./project-sources.js";
|
|
6
|
-
import { askMessage, composeMessage, messageStatus, readLatest, submitMessage, waitAndRead, waitForMessage } from "./messages.js";
|
|
6
|
+
import { askMessage, composeMessage, messageStatus, readLatest, stopGeneration, submitMessage, waitAndRead, waitForMessage } from "./messages.js";
|
|
7
7
|
import { copyResponse } from "./response-actions.js";
|
|
8
8
|
import { bootstrap } from "./session.js";
|
|
9
9
|
import { newThread, openThread, searchThreads } from "./threads.js";
|
|
@@ -82,6 +82,8 @@ export async function executeStep(step, env, previousResults) {
|
|
|
82
82
|
return readLatest(env, step.args);
|
|
83
83
|
case "messages.status":
|
|
84
84
|
return messageStatus(env, step.args);
|
|
85
|
+
case "messages.stop":
|
|
86
|
+
return stopGeneration(env, step.args);
|
|
85
87
|
case "messages.waitAndRead":
|
|
86
88
|
return waitAndRead(env, step.args);
|
|
87
89
|
case "artifacts.listLatest":
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { BootstrapArgs, BootstrapData, CommandResult, RuntimeEnv } from "../types.js";
|
|
2
|
+
export type EnsurePageOptions = {
|
|
3
|
+
minimalContext?: boolean;
|
|
4
|
+
};
|
|
2
5
|
export declare function bootstrap(env: RuntimeEnv, args?: BootstrapArgs): Promise<CommandResult<BootstrapData>>;
|
|
3
|
-
export declare function ensurePage(env: RuntimeEnv): Promise<CommandResult<unknown>>;
|
|
6
|
+
export declare function ensurePage(env: RuntimeEnv, options?: EnsurePageOptions): Promise<CommandResult<unknown>>;
|
|
4
7
|
export declare function verifyTabAffinity(env: RuntimeEnv): Promise<CommandResult<unknown> | undefined>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attachChatGPTBrowser, tabIdFromPage } from "../browser/attach.js";
|
|
1
|
+
import { attachChatGPTBrowser, isChatGPTUrl, tabIdFromPage } from "../browser/attach.js";
|
|
2
2
|
import { readPageState } from "../browser/page-state.js";
|
|
3
3
|
import { resultError, resultOk } from "../errors.js";
|
|
4
4
|
import { contextFromPage } from "./context.js";
|
|
@@ -26,7 +26,7 @@ export async function bootstrap(env, args = {}) {
|
|
|
26
26
|
return resultError(error instanceof Error ? error : new Error(String(error)));
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
export async function ensurePage(env) {
|
|
29
|
+
export async function ensurePage(env, options = {}) {
|
|
30
30
|
if (env.page === undefined) {
|
|
31
31
|
return bootstrap(env, { preferExistingTab: true });
|
|
32
32
|
}
|
|
@@ -34,7 +34,38 @@ export async function ensurePage(env) {
|
|
|
34
34
|
if (affinity !== undefined) {
|
|
35
35
|
return affinity;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
const origin = await verifyChatGPTOrigin(env);
|
|
38
|
+
if (origin !== undefined) {
|
|
39
|
+
return origin;
|
|
40
|
+
}
|
|
41
|
+
return resultOk({}, await contextFromPage(env.page, tabContext(env), { minimal: options.minimalContext === true }));
|
|
42
|
+
}
|
|
43
|
+
async function verifyChatGPTOrigin(env) {
|
|
44
|
+
if (env.page === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
const actualUrl = await Promise.resolve(env.page.url?.()).catch(() => undefined);
|
|
47
|
+
if (isChatGPTUrl(actualUrl))
|
|
48
|
+
return undefined;
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
status: "blocked",
|
|
52
|
+
warnings: [],
|
|
53
|
+
blocker: {
|
|
54
|
+
kind: "selector_drift",
|
|
55
|
+
code: "unsafe_chatgpt_origin",
|
|
56
|
+
message: "ChatGPT command refused to operate because the controlled tab is not on an allowlisted ChatGPT origin.",
|
|
57
|
+
visibleText: actualUrl ?? "The current tab URL could not be verified.",
|
|
58
|
+
remediation: [
|
|
59
|
+
{
|
|
60
|
+
label: "Reopen ChatGPT",
|
|
61
|
+
instruction: "Run session.bootstrap against https://chatgpt.com or claim an exact supported ChatGPT tab before retrying.",
|
|
62
|
+
userActionRequired: false
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
resumable: false
|
|
66
|
+
},
|
|
67
|
+
context: await contextFromPage(env.page, tabContext(env))
|
|
68
|
+
};
|
|
38
69
|
}
|
|
39
70
|
export async function verifyTabAffinity(env) {
|
|
40
71
|
if (env.expectedTabId === undefined || env.page === undefined) {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { PageLike } from "../types.js";
|
|
2
|
+
export type GenerationStateReadOptions = {
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
};
|
|
2
5
|
export type AssistantGenerationState = {
|
|
6
|
+
/** True only when the page DOM was inspected successfully. */
|
|
7
|
+
observed: boolean;
|
|
3
8
|
active: boolean;
|
|
4
9
|
stopped: boolean;
|
|
5
10
|
signals: string[];
|
|
@@ -11,5 +16,5 @@ export type AssistantGenerationState = {
|
|
|
11
16
|
* response is complete.
|
|
12
17
|
*/
|
|
13
18
|
export declare const EMPTY_GENERATION_STATE: AssistantGenerationState;
|
|
14
|
-
export declare function readAssistantGenerationState(page: PageLike): Promise<AssistantGenerationState>;
|
|
19
|
+
export declare function readAssistantGenerationState(page: PageLike, options?: GenerationStateReadOptions): Promise<AssistantGenerationState>;
|
|
15
20
|
export declare function latestAssistantTurnHasResponseActions(page: PageLike): Promise<boolean>;
|