codex-chatgpt-control 0.5.1-alpha.3 → 0.5.1-alpha.4
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 +27 -0
- package/README.md +16 -0
- package/contracts/v1/fixtures/download-blocked-by-browser.json +22 -0
- package/contracts/v1/fixtures/download-receipt-failed.json +22 -0
- package/contracts/v1/fixtures/download-receipt-timeout.json +22 -0
- package/contracts/v1/fixtures/journal-rpc-indeterminate.json +32 -0
- package/contracts/v1/fixtures/journal-rpc-unsupported-platform.json +32 -0
- package/contracts/v1/fixtures/journal-runtime-unavailable.json +32 -0
- package/contracts/v1/manifest.json +30 -0
- package/contracts/v1/parity-suite.json +10 -7
- package/dist/codex-chatgpt-control-backend.mjs +3795 -2077
- package/dist/codex-chatgpt-control-journal.mjs +5547 -0
- package/dist/codex-chatgpt-control-live-smoke.bundle.mjs +51677 -0
- package/dist/codex-chatgpt-control-release-canary.bundle.mjs +52104 -0
- package/dist/codex-chatgpt-control.bundle.mjs +3403 -1369
- package/dist/src/browser/chatgpt-url.d.ts +2 -0
- package/dist/src/browser/chatgpt-url.js +9 -0
- package/dist/src/browser/downloads.d.ts +15 -1
- package/dist/src/browser/downloads.js +150 -28
- package/dist/src/browser/page-state.js +6 -0
- package/dist/src/client.d.ts +5 -0
- package/dist/src/client.js +69 -12
- package/dist/src/commands/artifacts.js +6 -4
- package/dist/src/commands/chat-popover.d.ts +37 -0
- package/dist/src/commands/chat-popover.js +537 -0
- package/dist/src/commands/configuration.d.ts +5 -0
- package/dist/src/commands/configuration.js +121 -14
- package/dist/src/commands/context.js +10 -3
- package/dist/src/commands/doctor.js +1 -1
- package/dist/src/commands/experience.d.ts +5 -0
- package/dist/src/commands/experience.js +69 -21
- package/dist/src/commands/files.js +10 -6
- package/dist/src/commands/modes.js +36 -4
- package/dist/src/commands/power-discovery.js +52 -10
- package/dist/src/commands/session.js +12 -4
- package/dist/src/dom/composer-text.d.ts +6 -0
- package/dist/src/dom/composer-text.js +85 -0
- package/dist/src/operations/artifact-transfer.d.ts +1 -1
- package/dist/src/operations/artifact-transfer.js +3 -3
- package/dist/src/operations/browser-adapter.d.ts +10 -4
- package/dist/src/operations/browser-adapter.js +95 -15
- package/dist/src/operations/browser-observation.d.ts +1 -1
- package/dist/src/operations/browser-observation.js +97 -49
- package/dist/src/operations/browser-target.d.ts +3 -1
- package/dist/src/operations/browser-target.js +49 -7
- package/dist/src/operations/chatgpt-runtime.js +85 -3
- package/dist/src/operations/client.js +2 -0
- package/dist/src/operations/configuration-routing.d.ts +4 -0
- package/dist/src/operations/configuration-routing.js +14 -0
- package/dist/src/operations/index.d.ts +3 -0
- package/dist/src/operations/index.js +3 -0
- package/dist/src/operations/journal-authority.d.ts +12 -0
- package/dist/src/operations/journal-authority.js +1 -0
- package/dist/src/operations/journal-rpc-client.d.ts +12 -0
- package/dist/src/operations/journal-rpc-client.js +225 -0
- package/dist/src/operations/journal-rpc-protocol.d.ts +32 -0
- package/dist/src/operations/journal-rpc-protocol.js +214 -0
- package/dist/src/operations/journal-rpc-server.d.ts +15 -0
- package/dist/src/operations/journal-rpc-server.js +272 -0
- package/dist/src/operations/journal.d.ts +1 -0
- package/dist/src/operations/journal.js +28 -2
- package/dist/src/operations/production-attachments.d.ts +3 -1
- package/dist/src/operations/production-attachments.js +25 -8
- package/dist/src/operations/production-chatgpt-artifacts.js +3 -3
- package/dist/src/operations/production-chatgpt-attachments.d.ts +10 -1
- package/dist/src/operations/production-chatgpt-attachments.js +133 -57
- package/dist/src/operations/production-configuration.d.ts +1 -1
- package/dist/src/operations/production-configuration.js +205 -24
- package/dist/src/operations/production-primitives.js +35 -252
- package/dist/src/operations/production-work-steer.js +35 -35
- package/dist/src/operations/runtime-adapter.d.ts +8 -0
- package/dist/src/operations/runtime-adapter.js +20 -4
- package/dist/src/operations/service.d.ts +7 -2
- package/dist/src/operations/service.js +80 -59
- package/dist/src/operations/transactional-chat-power.d.ts +17 -0
- package/dist/src/operations/transactional-chat-power.js +212 -0
- package/dist/src/runtime/coordinated-page.d.ts +2 -0
- package/dist/src/runtime/coordinated-page.js +23 -6
- package/dist/src/safety/blockers.js +8 -1
- package/dist/src/scripts/capture-surface-profile.js +7 -2
- package/dist/src/scripts/journal-server.d.ts +2 -0
- package/dist/src/scripts/journal-server.js +37 -0
- package/dist/src/scripts/live-smoke/scenarios.d.ts +16 -3
- package/dist/src/scripts/live-smoke/scenarios.js +47 -22
- package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
- package/dist/src/scripts/live-smoke/transactional.js +254 -0
- package/package.json +8 -3
- package/references/2026-09-06-journal-service.md +140 -0
- package/references/backend-protocol.md +103 -0
- package/references/python-parity.md +65 -0
|
@@ -6,6 +6,7 @@ import { tabIdFromPage } from "../browser/attach.js";
|
|
|
6
6
|
import { contextFromPage } from "./context.js";
|
|
7
7
|
import { detectExperience, openExperience } from "./experience.js";
|
|
8
8
|
import { setMode } from "./modes.js";
|
|
9
|
+
import { closeChatPopover, inspectChatPopover, readChatPopover, selectChatPopoverEffort, selectChatPopoverModel, selectChatPopoverSpeed } from "./chat-popover.js";
|
|
9
10
|
import { ensurePage } from "./session.js";
|
|
10
11
|
const WORK_AXES = ["model", "effort", "speed"];
|
|
11
12
|
const CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
@@ -13,11 +14,11 @@ const CONFIGURATION_CONTROL_POLL_MS = 250;
|
|
|
13
14
|
const CONFIGURATION_SELECTION_MAX_ATTEMPTS = 6;
|
|
14
15
|
const CONFIGURATION_SELECTION_RETRY_MS = 400;
|
|
15
16
|
const CONFIGURATION_AXIS_ORDER = [
|
|
17
|
+
"modelVersion",
|
|
16
18
|
"model",
|
|
17
19
|
"intelligence",
|
|
18
20
|
"effort",
|
|
19
21
|
"speed",
|
|
20
|
-
"modelVersion",
|
|
21
22
|
];
|
|
22
23
|
export async function inspectConfiguration(env, args = {}) {
|
|
23
24
|
const boot = await ensurePage(env);
|
|
@@ -49,11 +50,25 @@ export async function inspectConfiguration(env, args = {}) {
|
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
const experience = detected.data.experience;
|
|
53
|
+
const initialChatPopover = experience !== "unknown" ? (await readChatPopover(page)).snapshot : undefined;
|
|
52
54
|
const initialPanel = await readConfigurationPanel(page);
|
|
53
55
|
const rootOpened = experience !== "unknown" && await waitForConfigurationRoot(page, experience, args.timeoutMs);
|
|
54
56
|
if (rootOpened) {
|
|
55
57
|
await page.waitForTimeout?.(150);
|
|
56
58
|
}
|
|
59
|
+
if (experience !== "unknown" && (await readChatPopover(page)).snapshot !== undefined) {
|
|
60
|
+
try {
|
|
61
|
+
const chat = await inspectChatPopover(page);
|
|
62
|
+
const data = configurationInspectionFromPopover(experience, detected.data.evidence, chat);
|
|
63
|
+
if (args.includeOptions === false)
|
|
64
|
+
data.options = {};
|
|
65
|
+
return resultOk(data, await contextFromPage(page, { experience, selectorProfile: data.selectorProfile }), data.verified ? [] : ["The owned configuration views did not provide unique active model and effort evidence."]);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
if (initialChatPopover === undefined)
|
|
69
|
+
await closeConfigurationMenus(page);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
57
72
|
const workAdvancedOpened = experience !== "work"
|
|
58
73
|
|| (rootOpened && await ensureWorkAdvancedPanel(page));
|
|
59
74
|
if (experience === "work" && workAdvancedOpened) {
|
|
@@ -95,6 +110,32 @@ export async function inspectConfiguration(env, args = {}) {
|
|
|
95
110
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
96
111
|
}
|
|
97
112
|
}
|
|
113
|
+
/** Shared owned popover, with surface-specific model semantics and observed axes only. */
|
|
114
|
+
export function configurationInspectionFromPopover(experience, evidence, popover) {
|
|
115
|
+
const data = { experience, selectorProfile: experience === "work" ? "work_basic_v1" : "chat_simplified_v1",
|
|
116
|
+
availableAxes: [], active: {}, options: {}, verified: false, evidence };
|
|
117
|
+
if (popover?.slider !== undefined) {
|
|
118
|
+
data.availableAxes.push("effort");
|
|
119
|
+
if (popover.effort !== undefined) {
|
|
120
|
+
data.active.effort = popover.effort;
|
|
121
|
+
data.options.effort = [{ id: normalizeConfigurationId(popover.effort), label: popover.effort, selected: true }];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (popover !== undefined && popover.modelOptions.length > 0) {
|
|
125
|
+
const axis = experience === "work" ? "model" : "modelVersion";
|
|
126
|
+
data.availableAxes.push(axis);
|
|
127
|
+
if (popover.activeModel !== undefined)
|
|
128
|
+
data.active[axis] = popover.activeModel;
|
|
129
|
+
data.options[axis] = popover.modelOptions.map(option => ({ id: normalizeConfigurationId(option.label), label: option.label, selected: option.checked }));
|
|
130
|
+
}
|
|
131
|
+
if (experience === "work" && popover?.speed !== undefined) {
|
|
132
|
+
data.availableAxes.push("speed");
|
|
133
|
+
data.active.speed = popover.speed;
|
|
134
|
+
data.options.speed = ["Standard", "Fast"].map(label => ({ id: normalizeConfigurationId(label), label, selected: label === popover.speed }));
|
|
135
|
+
}
|
|
136
|
+
data.verified = popover?.effort !== undefined && popover.activeModel !== undefined;
|
|
137
|
+
return data;
|
|
138
|
+
}
|
|
98
139
|
async function waitForConfigurationRoot(page, experience, timeoutMs) {
|
|
99
140
|
const discoveryMs = Math.min(timeoutMs ?? CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS, CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS);
|
|
100
141
|
const attempts = Math.max(1, Math.ceil(Math.max(0, discoveryMs) / CONFIGURATION_CONTROL_POLL_MS));
|
|
@@ -154,7 +195,15 @@ export async function applyConfiguration(env, args) {
|
|
|
154
195
|
}
|
|
155
196
|
const selected = [];
|
|
156
197
|
for (const [axis, requested] of selectionEntries(desired)) {
|
|
157
|
-
|
|
198
|
+
// A model change can reset effort and alter its available range.
|
|
199
|
+
// Re-observe between axes instead of trusting the initial snapshot.
|
|
200
|
+
const currentResult = selected.length === 0 ? beforeResult : await inspectConfiguration(env, { includeOptions: false });
|
|
201
|
+
if (!currentResult.ok || currentResult.data === undefined)
|
|
202
|
+
return forwardFailure(currentResult);
|
|
203
|
+
const current = currentResult.data;
|
|
204
|
+
if (current.experience !== before.experience)
|
|
205
|
+
return configurationFailure(page, before, desired, selected, "The composer experience changed during configuration selection.", "experience_mismatch");
|
|
206
|
+
const active = activeConfigurationValue(current, axis);
|
|
158
207
|
if (active !== undefined && configurationValueMatches(active, requested)) {
|
|
159
208
|
selected.push({ axis, requested, selected: active });
|
|
160
209
|
continue;
|
|
@@ -228,22 +277,37 @@ export function configurationInspectionFromSurface(experience, detectedProfile,
|
|
|
228
277
|
const simplified = chatMenuLooksSimplified(menuItems);
|
|
229
278
|
selectorProfile = simplified ? "chat_simplified_v1" : detectedProfile;
|
|
230
279
|
const axis = simplified ? "intelligence" : "effort";
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
active[axis] = panel.openerLabel;
|
|
236
|
-
}
|
|
280
|
+
const modelRows = menuItems.filter(item => item.role === "menuitemradio"
|
|
281
|
+
&& item.hasPopup !== true
|
|
282
|
+
&& (/^(?:gpt[\s-]|o\d+(?:\b|$)|\d+(?:\.\d+)?$)/i.test(item.label)
|
|
283
|
+
|| localeLabels.modeOptions.latest.some(label => visibleLabelMatches(item.label, label))));
|
|
237
284
|
const chatOptions = menuItems
|
|
238
|
-
.filter(item => !isConfigurationAxisRow(item.label)
|
|
285
|
+
.filter(item => !isConfigurationAxisRow(item.label)
|
|
286
|
+
&& !modelRows.includes(item)
|
|
287
|
+
&& item.hasPopup !== true
|
|
288
|
+
&& !/^gpt[\s-]/i.test(item.label)
|
|
289
|
+
&& item.ariaLabel !== "Select model")
|
|
239
290
|
.map(menuItemToOption);
|
|
240
|
-
|
|
291
|
+
const selectedEffort = chatOptions.filter(option => option.selected === true);
|
|
292
|
+
const openerValue = panel.openerValue ?? panel.openerLabel;
|
|
293
|
+
if (chatOptions.length > 0 || openerValue !== undefined)
|
|
294
|
+
availableAxes.push(axis);
|
|
295
|
+
if (selectedEffort.length === 1)
|
|
296
|
+
active[axis] = selectedEffort[0].label;
|
|
297
|
+
else if (openerValue !== undefined
|
|
298
|
+
&& !isConfigurationAxisRow(openerValue)
|
|
299
|
+
&& !/^(?:thinking effort|select model|power)$/i.test(openerValue))
|
|
300
|
+
active[axis] = openerValue;
|
|
301
|
+
if (chatOptions.length > 0)
|
|
241
302
|
options[axis] = chatOptions;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (modelRows.length > 0) {
|
|
303
|
+
const modelOpeners = menuItems.filter(item => item.role !== "menuitemradio"
|
|
304
|
+
&& /^gpt[\s-]/i.test(item.label));
|
|
305
|
+
if (modelRows.length > 0 || modelOpeners.length > 0) {
|
|
245
306
|
availableAxes.push("modelVersion");
|
|
246
|
-
options.modelVersion = modelRows.map(menuItemToOption);
|
|
307
|
+
options.modelVersion = [...modelRows, ...modelOpeners].map(menuItemToOption);
|
|
308
|
+
const checked = modelRows.filter(item => item.checked === true);
|
|
309
|
+
if (checked.length === 1)
|
|
310
|
+
active.modelVersion = checked[0].label;
|
|
247
311
|
}
|
|
248
312
|
}
|
|
249
313
|
return {
|
|
@@ -268,6 +332,24 @@ async function selectWorkAxis(env, axis, requested, timeoutMs) {
|
|
|
268
332
|
if (!WORK_AXES.includes(axis)) {
|
|
269
333
|
return undefined;
|
|
270
334
|
}
|
|
335
|
+
const initialPopover = (await readChatPopover(page)).snapshot;
|
|
336
|
+
await openConfigurationRoot(page, "work");
|
|
337
|
+
if ((await readChatPopover(page)).snapshot !== undefined) {
|
|
338
|
+
try {
|
|
339
|
+
const labels = configurationSemanticLabels(requested);
|
|
340
|
+
if (axis === "model")
|
|
341
|
+
return await selectChatPopoverModel(page, labels);
|
|
342
|
+
if (axis === "effort")
|
|
343
|
+
return await selectChatPopoverEffort(page, labels);
|
|
344
|
+
if (axis === "speed")
|
|
345
|
+
return await selectChatPopoverSpeed(page, labels);
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
if (initialPopover === undefined)
|
|
350
|
+
await closeConfigurationMenus(page);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
271
353
|
const retryWindowMs = Math.min(timeoutMs ?? CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS, CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS);
|
|
272
354
|
const attempts = Math.max(2, Math.min(CONFIGURATION_SELECTION_MAX_ATTEMPTS, Math.ceil(Math.max(0, retryWindowMs) / CONFIGURATION_SELECTION_RETRY_MS) + 1));
|
|
273
355
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
@@ -424,6 +506,22 @@ function workAxisOptionLabelMatches(axis, label) {
|
|
|
424
506
|
return candidates.some(candidate => visibleLabelMatches(label, candidate));
|
|
425
507
|
}
|
|
426
508
|
async function selectChatAxis(env, axis, requested, timeoutMs) {
|
|
509
|
+
const page = env.page;
|
|
510
|
+
const initialPopover = (await readChatPopover(page)).snapshot;
|
|
511
|
+
await openConfigurationRoot(page, "chat");
|
|
512
|
+
if ((await readChatPopover(page)).snapshot !== undefined) {
|
|
513
|
+
try {
|
|
514
|
+
if (axis === "modelVersion")
|
|
515
|
+
return await selectChatPopoverModel(page, configurationSemanticLabels(requested));
|
|
516
|
+
if (axis === "effort" || axis === "intelligence" || axis === "model")
|
|
517
|
+
return await selectChatPopoverEffort(page, configurationSemanticLabels(requested));
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
if (initialPopover === undefined)
|
|
522
|
+
await closeConfigurationMenus(page);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
427
525
|
const legacyArgs = axis === "modelVersion"
|
|
428
526
|
? { modelVersion: requested }
|
|
429
527
|
: axis === "intelligence"
|
|
@@ -443,6 +541,8 @@ async function selectChatAxis(env, axis, requested, timeoutMs) {
|
|
|
443
541
|
return result.ok ? result.data?.selected.at(-1) : undefined;
|
|
444
542
|
}
|
|
445
543
|
async function openConfigurationRoot(page, experience) {
|
|
544
|
+
if (experience !== "unknown" && (await readChatPopover(page)).snapshot !== undefined)
|
|
545
|
+
return true;
|
|
446
546
|
const existing = await readConfigurationPanel(page);
|
|
447
547
|
if (existing.axisRows.length > 0) {
|
|
448
548
|
return true;
|
|
@@ -654,6 +754,7 @@ async function readConfigurationPanel(page) {
|
|
|
654
754
|
const html = control;
|
|
655
755
|
return {
|
|
656
756
|
label: normalize(control.getAttribute("aria-label") ?? html.innerText ?? control.textContent ?? ""),
|
|
757
|
+
value: normalize(html.innerText ?? control.textContent ?? ""),
|
|
657
758
|
testId: control.getAttribute("data-testid") ?? ""
|
|
658
759
|
};
|
|
659
760
|
})
|
|
@@ -666,6 +767,8 @@ async function readConfigurationPanel(page) {
|
|
|
666
767
|
};
|
|
667
768
|
if (openerCandidates.length === 1 && openerCandidates[0]?.label.length) {
|
|
668
769
|
result.openerLabel = openerCandidates[0].label;
|
|
770
|
+
if (openerCandidates[0].value.length > 0)
|
|
771
|
+
result.openerValue = openerCandidates[0].value;
|
|
669
772
|
}
|
|
670
773
|
return result;
|
|
671
774
|
}, localeLabels.configurationAxes).catch(() => ({ axisRows: [], advancedVisible: false }));
|
|
@@ -832,6 +935,10 @@ function normalizeConfigurationId(value) {
|
|
|
832
935
|
.trim();
|
|
833
936
|
}
|
|
834
937
|
async function closeConfigurationMenus(page) {
|
|
938
|
+
if ((await readChatPopover(page)).snapshot !== undefined) {
|
|
939
|
+
await closeChatPopover(page);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
835
942
|
if (!await pressConfigurationEscape(page))
|
|
836
943
|
return;
|
|
837
944
|
await page.waitForTimeout?.(50);
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { countPageMessages } from "../dom/messages.js";
|
|
2
2
|
import { parseConversationId } from "../browser/page-state.js";
|
|
3
3
|
import { withTimeout } from "./timeouts.js";
|
|
4
|
+
import { chatGPTAttachmentContextUrl } from "../browser/chatgpt-url.js";
|
|
4
5
|
export async function contextFromPage(page, partial = {}, options = {}) {
|
|
6
|
+
const partialAttachmentUrl = chatGPTAttachmentContextUrl(partial.url);
|
|
7
|
+
const safePartial = partialAttachmentUrl === undefined ? partial : { ...partial, url: partialAttachmentUrl };
|
|
5
8
|
if (page === undefined || options.minimal === true) {
|
|
6
|
-
return { timestamp: new Date().toISOString(), ...
|
|
9
|
+
return { timestamp: new Date().toISOString(), ...safePartial };
|
|
10
|
+
}
|
|
11
|
+
const url = typeof page.url === "function" ? await Promise.resolve().then(() => page.url()).catch(() => safePartial.url) : safePartial.url;
|
|
12
|
+
const attachmentUrl = chatGPTAttachmentContextUrl(url);
|
|
13
|
+
if (attachmentUrl !== undefined) {
|
|
14
|
+
return { timestamp: new Date().toISOString(), ...safePartial, url: attachmentUrl };
|
|
7
15
|
}
|
|
8
|
-
const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => partial.url) : partial.url;
|
|
9
16
|
const title = typeof page.title === "function"
|
|
10
17
|
? await withTimeout(page.title(), 1000, "Timed out while reading page title.").catch(() => partial.title)
|
|
11
18
|
: partial.title;
|
|
@@ -16,7 +23,7 @@ export async function contextFromPage(page, partial = {}, options = {}) {
|
|
|
16
23
|
const conversationId = url !== undefined ? parseConversationId(url) : partial.conversationId;
|
|
17
24
|
const context = {
|
|
18
25
|
timestamp: new Date().toISOString(),
|
|
19
|
-
...
|
|
26
|
+
...safePartial
|
|
20
27
|
};
|
|
21
28
|
if (url !== undefined) {
|
|
22
29
|
context.url = url;
|
|
@@ -161,7 +161,7 @@ function downloadCheck(env) {
|
|
|
161
161
|
if (page === undefined)
|
|
162
162
|
return unknown("Download readiness requires a bootstrapped ChatGPT page.");
|
|
163
163
|
return typeof page.waitForEvent === "function"
|
|
164
|
-
? ok("
|
|
164
|
+
? ok("The browser exposes a download event API; a completed download receipt has not been verified.")
|
|
165
165
|
: unsupported("The active browser page does not expose download events.");
|
|
166
166
|
}
|
|
167
167
|
async function clipboardCheck() {
|
|
@@ -3,6 +3,11 @@ type SurfaceSnapshot = {
|
|
|
3
3
|
url: string;
|
|
4
4
|
composerLabels: string[];
|
|
5
5
|
mainControls: string[];
|
|
6
|
+
/** Controls outside overlays, scoped to the composer (or main fallback). */
|
|
7
|
+
composerControls?: string[];
|
|
8
|
+
/** Separate visible control roots; unrelated overlays cannot combine axes. */
|
|
9
|
+
controlGroups?: string[][];
|
|
10
|
+
rootBudgetExceeded?: true;
|
|
6
11
|
mainText: string;
|
|
7
12
|
selectedSurfaceLabels?: string[];
|
|
8
13
|
};
|
|
@@ -7,15 +7,30 @@ import { ensurePage } from "./session.js";
|
|
|
7
7
|
const CHATGPT_HOME = "https://chatgpt.com/";
|
|
8
8
|
const EXPERIENCE_CONTROL_DISCOVERY_TIMEOUT_MS = 15_000;
|
|
9
9
|
const EXPERIENCE_POLL_MS = 250;
|
|
10
|
+
const EXPERIENCE_READINESS_TIMEOUT_MS = 1500;
|
|
10
11
|
export async function detectExperience(env, args = {}) {
|
|
11
|
-
void args;
|
|
12
12
|
const boot = await ensurePage(env);
|
|
13
13
|
if (!boot.ok) {
|
|
14
14
|
return boot;
|
|
15
15
|
}
|
|
16
16
|
const page = env.page;
|
|
17
17
|
try {
|
|
18
|
-
const
|
|
18
|
+
const readinessMs = Math.max(0, Math.min(args.timeoutMs ?? EXPERIENCE_READINESS_TIMEOUT_MS, EXPERIENCE_CONTROL_DISCOVERY_TIMEOUT_MS));
|
|
19
|
+
const readinessDeadline = Date.now() + readinessMs;
|
|
20
|
+
let data = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
21
|
+
// An empty post-navigation capture is loading evidence, not a final
|
|
22
|
+
// classification. Never wait out a known blocker or a genuine score tie.
|
|
23
|
+
for (let attempt = 1; data.experience === "unknown" && data.evidence.length === 0
|
|
24
|
+
&& Date.now() < readinessDeadline
|
|
25
|
+
&& attempt < pollAttempts(readinessMs, EXPERIENCE_POLL_MS); attempt += 1) {
|
|
26
|
+
const blocker = await experiencePageBlocker(page, data);
|
|
27
|
+
if (blocker !== undefined)
|
|
28
|
+
return blocker;
|
|
29
|
+
if (page.waitForTimeout === undefined || Date.now() >= readinessDeadline)
|
|
30
|
+
break;
|
|
31
|
+
await page.waitForTimeout(Math.min(EXPERIENCE_POLL_MS, readinessDeadline - Date.now()));
|
|
32
|
+
data = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
33
|
+
}
|
|
19
34
|
return resultOk(data, await contextFromPage(page, {
|
|
20
35
|
experience: data.experience,
|
|
21
36
|
selectorProfile: data.selectorProfile
|
|
@@ -177,9 +192,14 @@ async function navigateConversationToSurfaceHome(page, timeoutMs) {
|
|
|
177
192
|
return true;
|
|
178
193
|
}
|
|
179
194
|
export function detectExperienceFromSnapshot(snapshot) {
|
|
195
|
+
if (snapshot.rootBudgetExceeded === true) {
|
|
196
|
+
return {
|
|
197
|
+
experience: "unknown", selectorProfile: "unknown", confidence: "low",
|
|
198
|
+
evidence: [{ source: "control", label: "Experience surface root budget exceeded" }]
|
|
199
|
+
};
|
|
200
|
+
}
|
|
180
201
|
const evidence = [];
|
|
181
202
|
const composerLabels = snapshot.composerLabels.map(normalizeForLabelMatch);
|
|
182
|
-
const controls = snapshot.mainControls.map(normalizeForLabelMatch);
|
|
183
203
|
const mainText = normalizeForLabelMatch(snapshot.mainText);
|
|
184
204
|
const selectedSurfaceLabels = (snapshot.selectedSurfaceLabels ?? []).map(normalizeForLabelMatch);
|
|
185
205
|
const url = snapshot.url.toLowerCase();
|
|
@@ -201,13 +221,18 @@ export function detectExperienceFromSnapshot(snapshot) {
|
|
|
201
221
|
for (const label of chatComposer) {
|
|
202
222
|
evidence.push({ source: "composer", label });
|
|
203
223
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
224
|
+
// Model and effort also occur in the Chat composer popover. Only the
|
|
225
|
+
// complete Work axis group, including speed in the same visible pane,
|
|
226
|
+
// supplies Work evidence. Do not combine labels across unrelated overlays.
|
|
227
|
+
const workAxes = (snapshot.controlGroups ?? [snapshot.mainControls]).some(group => {
|
|
228
|
+
const groupLabels = group.map(normalizeForLabelMatch);
|
|
229
|
+
return ["model", "effort", "speed"].every(axis => hasAnyLabel(groupLabels, localeLabels.configurationAxes[axis]));
|
|
230
|
+
});
|
|
231
|
+
if (workAxes) {
|
|
232
|
+
evidence.push({ source: "control", label: "Work configuration axes (3/3)" });
|
|
209
233
|
}
|
|
210
|
-
const
|
|
234
|
+
const composerControls = (snapshot.composerControls ?? snapshot.mainControls).map(normalizeForLabelMatch);
|
|
235
|
+
const workConfigurationOpener = composerControls.some(label => /\b(?:gpt[\s-]?\d|\d+(?:\.\d+)+|sol|luna|terra)\b/i.test(label)
|
|
211
236
|
&& hasAnyLabel([label], [
|
|
212
237
|
...localeLabels.configurationOptions.light,
|
|
213
238
|
...localeLabels.configurationOptions.medium,
|
|
@@ -227,7 +252,7 @@ export function detectExperienceFromSnapshot(snapshot) {
|
|
|
227
252
|
}
|
|
228
253
|
const workScore = workComposer.length * 4
|
|
229
254
|
+ (workSurfaceSelected ? 10 : 0)
|
|
230
|
-
+ (
|
|
255
|
+
+ (workAxes ? 6 : 0)
|
|
231
256
|
// The active Work task drops the Chat/Work radio and keeps the shared
|
|
232
257
|
// "Chat with ChatGPT" textbox name. Its compound model + effort opener is
|
|
233
258
|
// therefore strong enough to disambiguate that continuation surface.
|
|
@@ -288,7 +313,19 @@ export async function readSurfaceSnapshot(page) {
|
|
|
288
313
|
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
289
314
|
const normalizeComparable = (value) => normalize(value).toLocaleLowerCase();
|
|
290
315
|
const wantedSurfaceLabels = new Set(surfaceOptionLabels.map(normalizeComparable));
|
|
291
|
-
const composerRoots = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i], main [class*='composer' i]"));
|
|
316
|
+
const composerRoots = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i], main [class*='composer' i]")).filter(visible);
|
|
317
|
+
const main = document.querySelector("main");
|
|
318
|
+
const overlayRoots = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper], [data-radix-menu-content]")).filter(visible);
|
|
319
|
+
const composerControlRoots = composerRoots.length > 0 ? composerRoots : main === null ? [] : [main];
|
|
320
|
+
const effectiveControlRoots = Array.from(new Set([...composerControlRoots, ...overlayRoots]));
|
|
321
|
+
// Reject oversized captures before expanding overlapping subtrees. A
|
|
322
|
+
// truncated prefix could omit contradictory surface evidence.
|
|
323
|
+
if (effectiveControlRoots.length > 32) {
|
|
324
|
+
return {
|
|
325
|
+
composerLabels: [], mainControls: [], composerControls: [], controlGroups: [],
|
|
326
|
+
mainText: "", selectedSurfaceLabels: [], rootBudgetExceeded: true
|
|
327
|
+
};
|
|
328
|
+
}
|
|
292
329
|
const composerNodes = composerRoots.flatMap(root => [
|
|
293
330
|
root,
|
|
294
331
|
...Array.from(root.querySelectorAll("textarea, [contenteditable='true'], [role='textbox'], input"))
|
|
@@ -299,18 +336,29 @@ export async function readSurfaceSnapshot(page) {
|
|
|
299
336
|
.map(normalize)
|
|
300
337
|
.filter(Boolean)))
|
|
301
338
|
.slice(0, 16);
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
339
|
+
const overlayRootSet = new Set(overlayRoots);
|
|
340
|
+
const owningOverlay = (node) => {
|
|
341
|
+
let current = node;
|
|
342
|
+
while (current !== null) {
|
|
343
|
+
if (overlayRootSet.has(current))
|
|
344
|
+
return current;
|
|
345
|
+
current = current.parentElement;
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
};
|
|
349
|
+
const controlsFor = (root) => Array.from(new Set(Array.from(root.querySelectorAll("button, [role='button'], [role='menuitem'], [role='menuitemradio'], [role='option']"))
|
|
309
350
|
.filter(visible)
|
|
351
|
+
// An ancestor composer/popover must not aggregate independent or
|
|
352
|
+
// nested menus. Each overlay contributes only its own nearest scope.
|
|
353
|
+
.filter(node => owningOverlay(node) === (overlayRootSet.has(root) ? root : undefined))
|
|
310
354
|
.map(labelFor)
|
|
311
355
|
.map(normalize)
|
|
312
|
-
.filter(Boolean)))
|
|
313
|
-
|
|
356
|
+
.filter(Boolean))).slice(0, 120);
|
|
357
|
+
const rootGroups = new Map(effectiveControlRoots.map(root => [root, controlsFor(root)]));
|
|
358
|
+
const controlGroups = [...rootGroups.values()];
|
|
359
|
+
const mainControls = Array.from(new Set(controlGroups.flat())).slice(0, 120);
|
|
360
|
+
const composerControls = Array.from(new Set(composerControlRoots
|
|
361
|
+
.flatMap(root => rootGroups.get(root) ?? []))).slice(0, 120);
|
|
314
362
|
const surfaceTextNodes = main === null ? [] : Array.from(main.querySelectorAll("h1, h2, h3, form, [data-testid*='composer' i], [class*='composer' i]"))
|
|
315
363
|
.filter(visible)
|
|
316
364
|
.slice(0, 32);
|
|
@@ -321,7 +369,7 @@ export async function readSurfaceSnapshot(page) {
|
|
|
321
369
|
.map(normalize)
|
|
322
370
|
.filter(label => wantedSurfaceLabels.has(normalizeComparable(label)))))
|
|
323
371
|
.slice(0, 4);
|
|
324
|
-
return { composerLabels, mainControls, mainText, selectedSurfaceLabels };
|
|
372
|
+
return { composerLabels, mainControls, composerControls, controlGroups, mainText, selectedSurfaceLabels };
|
|
325
373
|
}, [
|
|
326
374
|
...localeLabels.experienceOptions.chat,
|
|
327
375
|
...localeLabels.experienceOptions.work,
|
|
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { downloadLatestArtifact, locatorCountWithTimeout } from "./artifacts.js";
|
|
6
6
|
import { ACTIVE_COMPOSER_FILE_INPUT_CLICK_EXPRESSION } from "../browser/active-composer-file-input.js";
|
|
7
|
-
import { waitForDownloadFromClick } from "../browser/downloads.js";
|
|
7
|
+
import { isTerminalDownloadCode, isTerminalDownloadError, waitForDownloadFromClick } from "../browser/downloads.js";
|
|
8
8
|
import { nodeErrorCode, resultError, resultOk } from "../errors.js";
|
|
9
9
|
import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.js";
|
|
10
10
|
import { escapeRegExp, localeLabels } from "../dom/locale-labels.js";
|
|
@@ -987,7 +987,7 @@ export async function downloadLatestFile(env, args) {
|
|
|
987
987
|
}
|
|
988
988
|
if (count === 0) {
|
|
989
989
|
const artifactDownload = await downloadLatestArtifact(env, args);
|
|
990
|
-
if (artifactDownload.ok) {
|
|
990
|
+
if (artifactDownload.ok || isTerminalDownloadCode(artifactDownload.blocker?.code)) {
|
|
991
991
|
return artifactDownload;
|
|
992
992
|
}
|
|
993
993
|
return {
|
|
@@ -1008,7 +1008,7 @@ export async function downloadLatestFile(env, args) {
|
|
|
1008
1008
|
return resultOk(downloaded, await contextFromPage(page));
|
|
1009
1009
|
}
|
|
1010
1010
|
catch (error) {
|
|
1011
|
-
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
1011
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page, {}, { minimal: isTerminalDownloadError(error) }));
|
|
1012
1012
|
}
|
|
1013
1013
|
}
|
|
1014
1014
|
async function tryGeneratedFilePreviewDownload(page, args) {
|
|
@@ -1039,8 +1039,12 @@ async function tryGeneratedFilePreviewDownload(page, args) {
|
|
|
1039
1039
|
await affordance.click({ timeoutMs: localGuardTimeout(timeoutMs, 10000) });
|
|
1040
1040
|
const labelledPreview = requiredLocator(page, `section[aria-label="${escapeCssAttribute(selected.filename)}"]`);
|
|
1041
1041
|
const workbookPreviews = requiredLocator(page, "section[data-testid^='popcorn-']");
|
|
1042
|
-
|
|
1043
|
-
|
|
1042
|
+
// A provider without locator filtering cannot tie a workbook preview to
|
|
1043
|
+
// the selected filename. Keep the explicitly labelled preview usable.
|
|
1044
|
+
const workbookPreview = typeof workbookPreviews.filter === "function"
|
|
1045
|
+
? workbookPreviews.filter({ hasText: selected.filename })
|
|
1046
|
+
: undefined;
|
|
1047
|
+
const download = await waitForPreviewDownloadControl(page, workbookPreview === undefined ? [labelledPreview] : [labelledPreview, workbookPreview], timeoutMs);
|
|
1044
1048
|
if (download === undefined) {
|
|
1045
1049
|
throw new Error(`The artifact preview for ${selected.filename} did not expose a visible Download control.`);
|
|
1046
1050
|
}
|
|
@@ -1048,7 +1052,7 @@ async function tryGeneratedFilePreviewDownload(page, args) {
|
|
|
1048
1052
|
return resultOk(downloaded, await contextFromPage(page));
|
|
1049
1053
|
}
|
|
1050
1054
|
catch (error) {
|
|
1051
|
-
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
1055
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page, {}, { minimal: isTerminalDownloadError(error) }));
|
|
1052
1056
|
}
|
|
1053
1057
|
}
|
|
1054
1058
|
async function inspectGeneratedFileAffordances(page, timeoutMs) {
|
|
@@ -6,6 +6,7 @@ import { normalizeLabel, normalizeWhitespace } from "../dom/visible-text.js";
|
|
|
6
6
|
import { contextFromPage } from "./context.js";
|
|
7
7
|
import { discoverPowerSlider, observedPowerSlider, resolvePowerTarget } from "./power-discovery.js";
|
|
8
8
|
import { ensurePage } from "./session.js";
|
|
9
|
+
import { closeChatPopover, readChatPopover, selectChatPopoverEffort, selectChatPopoverModel, setChatPopoverView } from "./chat-popover.js";
|
|
9
10
|
const DEFAULT_MODE_EFFORT = "Thinking";
|
|
10
11
|
const CURRENT_MODE_LABELS = dedupeLabels([
|
|
11
12
|
...localeLabels.modeLabels,
|
|
@@ -14,7 +15,7 @@ const CURRENT_MODE_LABELS = dedupeLabels([
|
|
|
14
15
|
]);
|
|
15
16
|
const MODE_OPENER_LABELS = [...CURRENT_MODE_LABELS.filter(label => label !== "Pro"), ...localeLabels.modeOpenerExtra];
|
|
16
17
|
const MODEL_VERSION_FAMILY_PATTERN = /^gpt[\s-]/i;
|
|
17
|
-
const MODEL_VERSION_LABEL_PATTERN = /^(?:o\d+|\d+(?:\.\d+)
|
|
18
|
+
const MODEL_VERSION_LABEL_PATTERN = /^(?:gpt[\s-].+|o\d+|\d+(?:\.\d+)?|latest)$/i;
|
|
18
19
|
const CANONICAL_INTELLIGENCE_ORDER = new Map([
|
|
19
20
|
["instant", 0],
|
|
20
21
|
["medium", 1],
|
|
@@ -53,6 +54,7 @@ export async function setMode(env, args) {
|
|
|
53
54
|
}
|
|
54
55
|
const page = env.page;
|
|
55
56
|
try {
|
|
57
|
+
const initialPopover = (await readChatPopover(page)).snapshot;
|
|
56
58
|
const requested = requestedModeSelections(args);
|
|
57
59
|
const requestedVersion = requestedModelVersion(args);
|
|
58
60
|
const requestedForOpening = requestedVersion === undefined ? requested : [...requested, requestedModeSelection(requestedVersion)];
|
|
@@ -64,6 +66,34 @@ export async function setMode(env, args) {
|
|
|
64
66
|
return selectorDrift(page, "No unique ChatGPT mode menu opener was found.");
|
|
65
67
|
}
|
|
66
68
|
await page.waitForTimeout?.(250);
|
|
69
|
+
const scopedPopover = (await readChatPopover(page)).snapshot;
|
|
70
|
+
if (scopedPopover !== undefined) {
|
|
71
|
+
try {
|
|
72
|
+
const selected = [];
|
|
73
|
+
const candidates = [];
|
|
74
|
+
if (requestedVersion !== undefined) {
|
|
75
|
+
const model = await selectChatPopoverModel(page, [requestedVersion]);
|
|
76
|
+
if (model === undefined)
|
|
77
|
+
return selectorDrift(page, `Model version "${requestedVersion}" did not verify in the owned Chat model view.`);
|
|
78
|
+
selected.push(model);
|
|
79
|
+
candidates.push(model);
|
|
80
|
+
}
|
|
81
|
+
for (const request of requested) {
|
|
82
|
+
const effort = await selectChatPopoverEffort(page, request.labels);
|
|
83
|
+
if (effort === undefined)
|
|
84
|
+
return selectorDrift(page, `Mode option "${request.requested}" did not verify in the owned Chat effort control.`);
|
|
85
|
+
selected.push(effort);
|
|
86
|
+
candidates.push(effort);
|
|
87
|
+
}
|
|
88
|
+
return resultOk({ selected, candidates }, await contextFromPage(page));
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (initialPopover !== undefined)
|
|
92
|
+
await setChatPopoverView(page, initialPopover.view);
|
|
93
|
+
else
|
|
94
|
+
await closeChatPopover(page);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
67
97
|
let candidates = await enumerateVisibleMenuItems(page);
|
|
68
98
|
const observedCandidates = [...candidates];
|
|
69
99
|
const selected = [];
|
|
@@ -288,6 +318,8 @@ async function waitForModeMenu(page, requested, timeoutMs) {
|
|
|
288
318
|
const deadline = Date.now() + timeoutMs;
|
|
289
319
|
let modeButtons = [];
|
|
290
320
|
do {
|
|
321
|
+
if ((await readChatPopover(page)).snapshot !== undefined)
|
|
322
|
+
return { opened: true, alreadySelected: [], modeButtons };
|
|
291
323
|
modeButtons = await visibleModeButtonLabelList(page);
|
|
292
324
|
const alreadySelected = findAlreadySelectedModes(modeButtons, requested);
|
|
293
325
|
if (alreadySelected.length === requested.length) {
|
|
@@ -668,8 +700,8 @@ async function selectModelVersion(page, requestedVersion, currentCandidates, tim
|
|
|
668
700
|
: { candidates: candidates.map(candidate => candidate.label) };
|
|
669
701
|
}
|
|
670
702
|
function isModelVersionSubmenuOpener(item) {
|
|
671
|
-
return item.
|
|
672
|
-
|
|
703
|
+
return item.role !== "menuitemradio"
|
|
704
|
+
&& (MODEL_VERSION_FAMILY_PATTERN.test(item.label) || item.ariaLabel === "Select model");
|
|
673
705
|
}
|
|
674
706
|
async function clickResolvedMenuItem(page, item) {
|
|
675
707
|
if (item.testId !== undefined && await clickIfUniqueMenuControl(page.locator?.(`[data-testid="${escapeAttributeValue(item.testId)}"]`), item)) {
|
|
@@ -681,7 +713,7 @@ async function clickResolvedMenuItem(page, item) {
|
|
|
681
713
|
return clickMenuItem(page, item);
|
|
682
714
|
}
|
|
683
715
|
async function openModelVersionSubmenu(page, candidates) {
|
|
684
|
-
const submenuOpeners = candidates.filter(
|
|
716
|
+
const submenuOpeners = candidates.filter(isModelVersionSubmenuOpener);
|
|
685
717
|
if (submenuOpeners.length === 0) {
|
|
686
718
|
return false;
|
|
687
719
|
}
|