codex-chatgpt-control 0.5.1-alpha.2 → 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 +42 -0
- package/README.md +43 -0
- package/contracts/v1/fixtures/backend-capabilities.json +40 -1
- package/contracts/v1/fixtures/backend-compatibility.json +21 -0
- package/contracts/v1/fixtures/backend-version.json +6 -1
- 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/fixtures/operation-action-prepared-event.json +42 -0
- package/contracts/v1/fixtures/operation-action.json +13 -0
- package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
- package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
- package/contracts/v1/fixtures/operation-blocker.json +10 -0
- package/contracts/v1/fixtures/operation-collect-request.json +17 -0
- package/contracts/v1/fixtures/operation-collect-result.json +42 -0
- package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
- package/contracts/v1/fixtures/operation-control-request.json +18 -0
- package/contracts/v1/fixtures/operation-control-result.json +31 -0
- package/contracts/v1/fixtures/operation-event.json +18 -0
- package/contracts/v1/fixtures/operation-handle.json +10 -0
- package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
- package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
- package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
- package/contracts/v1/fixtures/operation-receipt.json +32 -0
- package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
- package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
- package/contracts/v1/fixtures/operation-request.json +33 -0
- package/contracts/v1/fixtures/operation-state.json +144 -0
- package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
- package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
- package/contracts/v1/fixtures/operation-submit-result.json +16 -0
- package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
- package/contracts/v1/manifest.json +185 -1
- package/contracts/v1/parity-suite.json +106 -8
- package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
- package/contracts/v1/schemas/backend-request.schema.json +8 -2
- package/contracts/v1/schemas/capabilities.schema.json +81 -1
- package/contracts/v1/schemas/manifest.schema.json +57 -0
- package/contracts/v1/schemas/operation-action.schema.json +155 -0
- package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
- package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
- package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
- package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
- package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
- package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
- package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
- package/contracts/v1/schemas/operation-event.schema.json +756 -0
- package/contracts/v1/schemas/operation-handle.schema.json +55 -0
- package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
- package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
- package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
- package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
- package/contracts/v1/schemas/operation-request.schema.json +158 -0
- package/contracts/v1/schemas/operation-state.schema.json +691 -0
- package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
- package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
- package/contracts/v1/surface-drift-policy.json +50 -0
- package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
- package/dist/codex-chatgpt-control-backend.mjs +45882 -9361
- 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 +48392 -10260
- package/dist/src/backend/client.d.ts +103 -2
- package/dist/src/backend/client.js +1436 -97
- package/dist/src/backend/compatibility.d.ts +12 -0
- package/dist/src/backend/compatibility.js +208 -0
- package/dist/src/backend/protocol.d.ts +84 -1
- package/dist/src/backend/protocol.js +31 -5
- package/dist/src/backend/runtime-identity.d.ts +10 -0
- package/dist/src/backend/runtime-identity.js +141 -0
- package/dist/src/backend/session.d.ts +10 -2
- package/dist/src/backend/session.js +478 -7
- package/dist/src/backend/stdio-server.d.ts +4 -1
- package/dist/src/backend/stdio-server.js +240 -28
- package/dist/src/browser/active-composer-file-input.d.ts +9 -0
- package/dist/src/browser/active-composer-file-input.js +33 -0
- package/dist/src/browser/attach.d.ts +4 -1
- package/dist/src/browser/attach.js +136 -28
- 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 +17 -1
- package/dist/src/browser/downloads.js +150 -28
- package/dist/src/browser/page-state.js +6 -0
- package/dist/src/client.d.ts +43 -0
- package/dist/src/client.js +1279 -59
- 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 +134 -21
- package/dist/src/commands/context.js +10 -3
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +27 -4
- package/dist/src/commands/experience.d.ts +5 -0
- package/dist/src/commands/experience.js +104 -21
- package/dist/src/commands/files.js +14 -33
- package/dist/src/commands/modes.js +85 -21
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +669 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +19 -6
- package/dist/src/commands/work.js +31 -0
- package/dist/src/dom/composer-text.d.ts +6 -0
- package/dist/src/dom/composer-text.js +85 -0
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +9 -0
- package/dist/src/operations/artifact-output.d.ts +69 -0
- package/dist/src/operations/artifact-output.js +1748 -0
- package/dist/src/operations/artifact-stream.d.ts +19 -0
- package/dist/src/operations/artifact-stream.js +35 -0
- package/dist/src/operations/artifact-transfer.d.ts +149 -0
- package/dist/src/operations/artifact-transfer.js +1312 -0
- package/dist/src/operations/browser-adapter.d.ts +167 -0
- package/dist/src/operations/browser-adapter.js +2103 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1256 -0
- package/dist/src/operations/browser-target.d.ts +105 -0
- package/dist/src/operations/browser-target.js +531 -0
- package/dist/src/operations/canonical.d.ts +4 -0
- package/dist/src/operations/canonical.js +412 -0
- package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
- package/dist/src/operations/chatgpt-runtime.js +1232 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1145 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -0
- package/dist/src/operations/configuration-routing.d.ts +4 -0
- package/dist/src/operations/configuration-routing.js +14 -0
- package/dist/src/operations/control.d.ts +372 -0
- package/dist/src/operations/control.js +1498 -0
- package/dist/src/operations/file-identity.d.ts +33 -0
- package/dist/src/operations/file-identity.js +153 -0
- package/dist/src/operations/handle.d.ts +19 -0
- package/dist/src/operations/handle.js +727 -0
- package/dist/src/operations/index.d.ts +25 -0
- package/dist/src/operations/index.js +25 -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 +143 -0
- package/dist/src/operations/journal.js +1957 -0
- package/dist/src/operations/production-attachments.d.ts +118 -0
- package/dist/src/operations/production-attachments.js +1035 -0
- package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
- package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
- package/dist/src/operations/production-chatgpt-attachments.d.ts +80 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1639 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1544 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +856 -0
- package/dist/src/operations/production-work-steer.d.ts +203 -0
- package/dist/src/operations/production-work-steer.js +1218 -0
- package/dist/src/operations/recovery.d.ts +80 -0
- package/dist/src/operations/recovery.js +174 -0
- package/dist/src/operations/runtime-adapter.d.ts +126 -0
- package/dist/src/operations/runtime-adapter.js +802 -0
- package/dist/src/operations/send-once.d.ts +224 -0
- package/dist/src/operations/send-once.js +1081 -0
- package/dist/src/operations/service.d.ts +286 -0
- package/dist/src/operations/service.js +2831 -0
- package/dist/src/operations/staging.d.ts +136 -0
- package/dist/src/operations/staging.js +630 -0
- package/dist/src/operations/state-machine.d.ts +29 -0
- package/dist/src/operations/state-machine.js +2047 -0
- package/dist/src/operations/submission.d.ts +423 -0
- package/dist/src/operations/submission.js +1676 -0
- package/dist/src/operations/transactional-chat-power.d.ts +17 -0
- package/dist/src/operations/transactional-chat-power.js +212 -0
- package/dist/src/operations/turn-ownership.d.ts +202 -0
- package/dist/src/operations/turn-ownership.js +700 -0
- package/dist/src/operations/types.d.ts +454 -0
- package/dist/src/operations/types.js +20 -0
- package/dist/src/operations/wire-requests.d.ts +21 -0
- package/dist/src/operations/wire-requests.js +396 -0
- package/dist/src/operations/wire-results.d.ts +97 -0
- package/dist/src/operations/wire-results.js +818 -0
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +19 -1
- package/dist/src/runner/result.js +227 -55
- package/dist/src/runner/types.d.ts +12 -0
- package/dist/src/runtime/command-routing.d.ts +149 -0
- package/dist/src/runtime/command-routing.js +431 -0
- package/dist/src/runtime/coordinated-browser.d.ts +24 -0
- package/dist/src/runtime/coordinated-browser.js +315 -0
- package/dist/src/runtime/coordinated-page.d.ts +41 -0
- package/dist/src/runtime/coordinated-page.js +651 -0
- package/dist/src/runtime/operation-context.d.ts +142 -0
- package/dist/src/runtime/operation-context.js +410 -0
- package/dist/src/runtime/runtime-session.d.ts +95 -0
- package/dist/src/runtime/runtime-session.js +314 -0
- package/dist/src/runtime/tab-coordinator.d.ts +200 -0
- package/dist/src/runtime/tab-coordinator.js +1200 -0
- package/dist/src/runtime/value-boundaries.d.ts +18 -0
- package/dist/src/runtime/value-boundaries.js +49 -0
- package/dist/src/safety/blockers.js +8 -1
- package/dist/src/safety/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -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/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +42 -1
- package/dist/src/scripts/live-smoke/scenarios.js +147 -30
- package/dist/src/scripts/live-smoke/transactional.d.ts +17 -0
- package/dist/src/scripts/live-smoke/transactional.js +254 -0
- package/dist/src/scripts/live-smoke/types.d.ts +3 -0
- package/dist/src/scripts/release-canary-module.js +17 -4
- package/dist/src/types.d.ts +38 -1
- package/package.json +8 -3
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/2026-09-06-journal-service.md +140 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +176 -0
- package/references/python-parity.md +137 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -6,16 +6,19 @@ 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;
|
|
12
13
|
const CONFIGURATION_CONTROL_POLL_MS = 250;
|
|
14
|
+
const CONFIGURATION_SELECTION_MAX_ATTEMPTS = 6;
|
|
15
|
+
const CONFIGURATION_SELECTION_RETRY_MS = 400;
|
|
13
16
|
const CONFIGURATION_AXIS_ORDER = [
|
|
17
|
+
"modelVersion",
|
|
14
18
|
"model",
|
|
15
19
|
"intelligence",
|
|
16
20
|
"effort",
|
|
17
21
|
"speed",
|
|
18
|
-
"modelVersion",
|
|
19
22
|
];
|
|
20
23
|
export async function inspectConfiguration(env, args = {}) {
|
|
21
24
|
const boot = await ensurePage(env);
|
|
@@ -47,11 +50,25 @@ export async function inspectConfiguration(env, args = {}) {
|
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
const experience = detected.data.experience;
|
|
53
|
+
const initialChatPopover = experience !== "unknown" ? (await readChatPopover(page)).snapshot : undefined;
|
|
50
54
|
const initialPanel = await readConfigurationPanel(page);
|
|
51
55
|
const rootOpened = experience !== "unknown" && await waitForConfigurationRoot(page, experience, args.timeoutMs);
|
|
52
56
|
if (rootOpened) {
|
|
53
57
|
await page.waitForTimeout?.(150);
|
|
54
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
|
+
}
|
|
55
72
|
const workAdvancedOpened = experience !== "work"
|
|
56
73
|
|| (rootOpened && await ensureWorkAdvancedPanel(page));
|
|
57
74
|
if (experience === "work" && workAdvancedOpened) {
|
|
@@ -93,6 +110,32 @@ export async function inspectConfiguration(env, args = {}) {
|
|
|
93
110
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
94
111
|
}
|
|
95
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
|
+
}
|
|
96
139
|
async function waitForConfigurationRoot(page, experience, timeoutMs) {
|
|
97
140
|
const discoveryMs = Math.min(timeoutMs ?? CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS, CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS);
|
|
98
141
|
const attempts = Math.max(1, Math.ceil(Math.max(0, discoveryMs) / CONFIGURATION_CONTROL_POLL_MS));
|
|
@@ -152,13 +195,21 @@ export async function applyConfiguration(env, args) {
|
|
|
152
195
|
}
|
|
153
196
|
const selected = [];
|
|
154
197
|
for (const [axis, requested] of selectionEntries(desired)) {
|
|
155
|
-
|
|
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);
|
|
156
207
|
if (active !== undefined && configurationValueMatches(active, requested)) {
|
|
157
208
|
selected.push({ axis, requested, selected: active });
|
|
158
209
|
continue;
|
|
159
210
|
}
|
|
160
211
|
const selection = before.experience === "work"
|
|
161
|
-
? await selectWorkAxis(env, axis, requested)
|
|
212
|
+
? await selectWorkAxis(env, axis, requested, args.timeoutMs)
|
|
162
213
|
: await selectChatAxis(env, axis, requested, args.timeoutMs);
|
|
163
214
|
if (selection === undefined) {
|
|
164
215
|
return configurationFailure(page, before, desired, selected, `Configuration option "${requested}" for ${axis} was not found or was ambiguous on the ${before.experience} surface.`, "configuration_option_not_found", before.options[axis]?.map(option => option.label));
|
|
@@ -226,22 +277,37 @@ export function configurationInspectionFromSurface(experience, detectedProfile,
|
|
|
226
277
|
const simplified = chatMenuLooksSimplified(menuItems);
|
|
227
278
|
selectorProfile = simplified ? "chat_simplified_v1" : detectedProfile;
|
|
228
279
|
const axis = simplified ? "intelligence" : "effort";
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
active[axis] = panel.openerLabel;
|
|
234
|
-
}
|
|
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))));
|
|
235
284
|
const chatOptions = menuItems
|
|
236
|
-
.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")
|
|
237
290
|
.map(menuItemToOption);
|
|
238
|
-
|
|
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)
|
|
239
302
|
options[axis] = chatOptions;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
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) {
|
|
243
306
|
availableAxes.push("modelVersion");
|
|
244
|
-
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;
|
|
245
311
|
}
|
|
246
312
|
}
|
|
247
313
|
return {
|
|
@@ -261,24 +327,46 @@ async function inspectWorkAxisOptions(env, axis) {
|
|
|
261
327
|
await closeConfigurationSubmenu(page);
|
|
262
328
|
return dedupeOptions(options);
|
|
263
329
|
}
|
|
264
|
-
async function selectWorkAxis(env, axis, requested) {
|
|
330
|
+
async function selectWorkAxis(env, axis, requested, timeoutMs) {
|
|
265
331
|
const page = env.page;
|
|
266
332
|
if (!WORK_AXES.includes(axis)) {
|
|
267
333
|
return undefined;
|
|
268
334
|
}
|
|
269
|
-
|
|
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
|
+
}
|
|
353
|
+
const retryWindowMs = Math.min(timeoutMs ?? CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS, CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS);
|
|
354
|
+
const attempts = Math.max(2, Math.min(CONFIGURATION_SELECTION_MAX_ATTEMPTS, Math.ceil(Math.max(0, retryWindowMs) / CONFIGURATION_SELECTION_RETRY_MS) + 1));
|
|
355
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
270
356
|
const candidates = await openWorkAxisOptions(env, axis);
|
|
271
357
|
const match = findConfigurationOption(candidates, requested);
|
|
272
358
|
if (match !== undefined && await clickVisibleMenuItem(page, match)) {
|
|
273
359
|
await page.waitForTimeout?.(150);
|
|
274
360
|
return match.label;
|
|
275
361
|
}
|
|
276
|
-
if (attempt
|
|
362
|
+
if (attempt + 1 < attempts) {
|
|
277
363
|
// The submenu can disappear between enumeration and the visible click
|
|
278
|
-
// when ChatGPT's pointer-grace timer fires
|
|
279
|
-
//
|
|
364
|
+
// when ChatGPT's pointer-grace timer fires, and Work may briefly retain
|
|
365
|
+
// the previous submenu immediately after a selection. Reopen from a
|
|
366
|
+
// fresh DOM instance for a small bounded window instead of treating the
|
|
367
|
+
// transient state as permanent selector drift.
|
|
280
368
|
await closeConfigurationMenus(page);
|
|
281
|
-
await page.waitForTimeout?.(
|
|
369
|
+
await page.waitForTimeout?.(CONFIGURATION_SELECTION_RETRY_MS);
|
|
282
370
|
}
|
|
283
371
|
}
|
|
284
372
|
return undefined;
|
|
@@ -418,6 +506,22 @@ function workAxisOptionLabelMatches(axis, label) {
|
|
|
418
506
|
return candidates.some(candidate => visibleLabelMatches(label, candidate));
|
|
419
507
|
}
|
|
420
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
|
+
}
|
|
421
525
|
const legacyArgs = axis === "modelVersion"
|
|
422
526
|
? { modelVersion: requested }
|
|
423
527
|
: axis === "intelligence"
|
|
@@ -437,6 +541,8 @@ async function selectChatAxis(env, axis, requested, timeoutMs) {
|
|
|
437
541
|
return result.ok ? result.data?.selected.at(-1) : undefined;
|
|
438
542
|
}
|
|
439
543
|
async function openConfigurationRoot(page, experience) {
|
|
544
|
+
if (experience !== "unknown" && (await readChatPopover(page)).snapshot !== undefined)
|
|
545
|
+
return true;
|
|
440
546
|
const existing = await readConfigurationPanel(page);
|
|
441
547
|
if (existing.axisRows.length > 0) {
|
|
442
548
|
return true;
|
|
@@ -648,6 +754,7 @@ async function readConfigurationPanel(page) {
|
|
|
648
754
|
const html = control;
|
|
649
755
|
return {
|
|
650
756
|
label: normalize(control.getAttribute("aria-label") ?? html.innerText ?? control.textContent ?? ""),
|
|
757
|
+
value: normalize(html.innerText ?? control.textContent ?? ""),
|
|
651
758
|
testId: control.getAttribute("data-testid") ?? ""
|
|
652
759
|
};
|
|
653
760
|
})
|
|
@@ -660,6 +767,8 @@ async function readConfigurationPanel(page) {
|
|
|
660
767
|
};
|
|
661
768
|
if (openerCandidates.length === 1 && openerCandidates[0]?.label.length) {
|
|
662
769
|
result.openerLabel = openerCandidates[0].label;
|
|
770
|
+
if (openerCandidates[0].value.length > 0)
|
|
771
|
+
result.openerValue = openerCandidates[0].value;
|
|
663
772
|
}
|
|
664
773
|
return result;
|
|
665
774
|
}, localeLabels.configurationAxes).catch(() => ({ axisRows: [], advancedVisible: false }));
|
|
@@ -826,6 +935,10 @@ function normalizeConfigurationId(value) {
|
|
|
826
935
|
.trim();
|
|
827
936
|
}
|
|
828
937
|
async function closeConfigurationMenus(page) {
|
|
938
|
+
if ((await readChatPopover(page)).snapshot !== undefined) {
|
|
939
|
+
await closeChatPopover(page);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
829
942
|
if (!await pressConfigurationEscape(page))
|
|
830
943
|
return;
|
|
831
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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BootstrapArgs, CommandResult, RuntimeEnv } from "../types.js";
|
|
2
2
|
import type { RunReportOptions } from "./reports.js";
|
|
3
|
-
export type DoctorCheckName = "bridge" | "login" | "upload" | "download" | "clipboard" | "modes" | "tools" | "selectors" | "existing_tab" | "artifacts" | "file_preflight" | "localization" | "reports";
|
|
3
|
+
export type DoctorCheckName = "compatibility" | "bridge" | "login" | "upload" | "download" | "clipboard" | "modes" | "tools" | "selectors" | "existing_tab" | "artifacts" | "file_preflight" | "localization" | "reports";
|
|
4
4
|
export type CapabilityStatus = "ok" | "blocked" | "unsupported" | "unknown";
|
|
5
5
|
export type CapabilityCheck = {
|
|
6
6
|
status: CapabilityStatus;
|
|
@@ -4,12 +4,12 @@ import { readSystemClipboard } from "../browser/clipboard.js";
|
|
|
4
4
|
import { readPageState } from "../browser/page-state.js";
|
|
5
5
|
import { localeLabels } from "../dom/locale-labels.js";
|
|
6
6
|
import { localeCoverageSummary } from "../dom/locale/index.js";
|
|
7
|
-
import { BROWSER_BRIDGE_REMEDIATION, resultOk } from "../errors.js";
|
|
7
|
+
import { BROWSER_BRIDGE_REMEDIATION, nodeErrorCode, resultOk } from "../errors.js";
|
|
8
8
|
import { explainCommandBlocker } from "../diagnostics/blockers.js";
|
|
9
9
|
import { preflightFiles } from "./files.js";
|
|
10
10
|
import { contextFromPage } from "./context.js";
|
|
11
11
|
import { bootstrap } from "./session.js";
|
|
12
|
-
const DEFAULT_CHECKS = ["bridge", "login", "upload", "download", "clipboard", "modes", "tools", "selectors"];
|
|
12
|
+
const DEFAULT_CHECKS = ["compatibility", "bridge", "login", "upload", "download", "clipboard", "modes", "tools", "selectors"];
|
|
13
13
|
const BOOTSTRAP_CHECKS = new Set(["bridge", "login", "upload", "download", "modes", "tools", "selectors"]);
|
|
14
14
|
const UPLOAD_REMEDIATION = [
|
|
15
15
|
"Codex Settings > Computer Use > Chrome > Permissions > Uploads: set to Always allow, or add chatgpt.com to the allowed upload domains.",
|
|
@@ -47,6 +47,9 @@ export async function doctor(env, args = {}) {
|
|
|
47
47
|
: undefined;
|
|
48
48
|
for (const check of wanted) {
|
|
49
49
|
switch (check) {
|
|
50
|
+
case "compatibility":
|
|
51
|
+
checks.compatibility = compatibilityCheck(env.compatibility);
|
|
52
|
+
break;
|
|
50
53
|
case "bridge":
|
|
51
54
|
checks.bridge = boot?.ok
|
|
52
55
|
? ok("Chrome bridge is available.")
|
|
@@ -95,6 +98,26 @@ export async function doctor(env, args = {}) {
|
|
|
95
98
|
const ready = Object.values(checks).every(check => check?.status === "ok" || check?.status === "unknown");
|
|
96
99
|
return resultOk({ ready, checks }, await contextFromPage(env.page));
|
|
97
100
|
}
|
|
101
|
+
function compatibilityCheck(report) {
|
|
102
|
+
if (report === undefined)
|
|
103
|
+
return unknown("Backend compatibility has not been negotiated.");
|
|
104
|
+
const firstWarning = report.warnings[0];
|
|
105
|
+
const message = firstWarning === undefined
|
|
106
|
+
? "Backend protocol and advertised capabilities are compatible."
|
|
107
|
+
: firstWarning.message;
|
|
108
|
+
return {
|
|
109
|
+
status: report.status === "blocked"
|
|
110
|
+
? "blocked"
|
|
111
|
+
: report.status === "warning"
|
|
112
|
+
? "unknown"
|
|
113
|
+
: report.status === "unknown"
|
|
114
|
+
? "unknown"
|
|
115
|
+
: "ok",
|
|
116
|
+
message,
|
|
117
|
+
...(firstWarning?.code === undefined ? {} : { code: firstWarning.code }),
|
|
118
|
+
details: report
|
|
119
|
+
};
|
|
120
|
+
}
|
|
98
121
|
function bridgeCheck(boot) {
|
|
99
122
|
if (boot === undefined) {
|
|
100
123
|
return unknown("Bridge readiness was not requested.");
|
|
@@ -138,7 +161,7 @@ function downloadCheck(env) {
|
|
|
138
161
|
if (page === undefined)
|
|
139
162
|
return unknown("Download readiness requires a bootstrapped ChatGPT page.");
|
|
140
163
|
return typeof page.waitForEvent === "function"
|
|
141
|
-
? ok("
|
|
164
|
+
? ok("The browser exposes a download event API; a completed download receipt has not been verified.")
|
|
142
165
|
: unsupported("The active browser page does not expose download events.");
|
|
143
166
|
}
|
|
144
167
|
async function clipboardCheck() {
|
|
@@ -410,5 +433,5 @@ function capability(status, message, remediation, details, nextCommand, blockerK
|
|
|
410
433
|
return check;
|
|
411
434
|
}
|
|
412
435
|
function isNodeError(error) {
|
|
413
|
-
return error
|
|
436
|
+
return nodeErrorCode(error) !== undefined;
|
|
414
437
|
}
|
|
@@ -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
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resultError, resultOk } from "../errors.js";
|
|
2
|
+
import { readPageState } from "../browser/page-state.js";
|
|
2
3
|
import { localeLabels } from "../dom/locale-labels.js";
|
|
3
4
|
import { normalizeForLabelMatch, visibleLabelMatches } from "../dom/label-match.js";
|
|
4
5
|
import { contextFromPage } from "./context.js";
|
|
@@ -6,15 +7,30 @@ import { ensurePage } from "./session.js";
|
|
|
6
7
|
const CHATGPT_HOME = "https://chatgpt.com/";
|
|
7
8
|
const EXPERIENCE_CONTROL_DISCOVERY_TIMEOUT_MS = 15_000;
|
|
8
9
|
const EXPERIENCE_POLL_MS = 250;
|
|
10
|
+
const EXPERIENCE_READINESS_TIMEOUT_MS = 1500;
|
|
9
11
|
export async function detectExperience(env, args = {}) {
|
|
10
|
-
void args;
|
|
11
12
|
const boot = await ensurePage(env);
|
|
12
13
|
if (!boot.ok) {
|
|
13
14
|
return boot;
|
|
14
15
|
}
|
|
15
16
|
const page = env.page;
|
|
16
17
|
try {
|
|
17
|
-
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
|
+
}
|
|
18
34
|
return resultOk(data, await contextFromPage(page, {
|
|
19
35
|
experience: data.experience,
|
|
20
36
|
selectorProfile: data.selectorProfile
|
|
@@ -34,6 +50,9 @@ export async function openExperience(env, args) {
|
|
|
34
50
|
const page = env.page;
|
|
35
51
|
try {
|
|
36
52
|
const before = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
53
|
+
const initialBlocker = await experiencePageBlocker(page, before);
|
|
54
|
+
if (initialBlocker !== undefined)
|
|
55
|
+
return initialBlocker;
|
|
37
56
|
if (before.experience === args.experience) {
|
|
38
57
|
return resultOk({
|
|
39
58
|
experience: args.experience,
|
|
@@ -52,6 +71,9 @@ export async function openExperience(env, args) {
|
|
|
52
71
|
let controlClicked = await clickUniqueExperienceControl(page, labels);
|
|
53
72
|
if (!controlClicked && await navigateConversationToSurfaceHome(page, args.timeoutMs)) {
|
|
54
73
|
observed = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
74
|
+
const navigationBlocker = await experiencePageBlocker(page, observed);
|
|
75
|
+
if (navigationBlocker !== undefined)
|
|
76
|
+
return navigationBlocker;
|
|
55
77
|
if (observed.experience === args.experience) {
|
|
56
78
|
return resultOk({
|
|
57
79
|
experience: args.experience,
|
|
@@ -85,6 +107,9 @@ export async function openExperience(env, args) {
|
|
|
85
107
|
controlClicked = await clickUniqueExperienceControl(page, labels);
|
|
86
108
|
}
|
|
87
109
|
if (!controlClicked) {
|
|
110
|
+
const discoveryBlocker = await experiencePageBlocker(page, observed);
|
|
111
|
+
if (discoveryBlocker !== undefined)
|
|
112
|
+
return discoveryBlocker;
|
|
88
113
|
return experienceSelectorDrift(page, `No unique visible ChatGPT ${args.experience === "work" ? "Work" : "Chat"} surface control was found.`, observed);
|
|
89
114
|
}
|
|
90
115
|
let after = before;
|
|
@@ -103,6 +128,9 @@ export async function openExperience(env, args) {
|
|
|
103
128
|
}));
|
|
104
129
|
}
|
|
105
130
|
}
|
|
131
|
+
const postconditionBlocker = await experiencePageBlocker(page, after);
|
|
132
|
+
if (postconditionBlocker !== undefined)
|
|
133
|
+
return postconditionBlocker;
|
|
106
134
|
return {
|
|
107
135
|
ok: false,
|
|
108
136
|
status: "blocked",
|
|
@@ -125,6 +153,28 @@ export async function openExperience(env, args) {
|
|
|
125
153
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
126
154
|
}
|
|
127
155
|
}
|
|
156
|
+
async function experiencePageBlocker(page, observed) {
|
|
157
|
+
const state = await readPageState(page);
|
|
158
|
+
if (state.blocker === undefined)
|
|
159
|
+
return undefined;
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
status: "blocked",
|
|
163
|
+
warnings: [],
|
|
164
|
+
blocker: {
|
|
165
|
+
kind: state.blocker.kind,
|
|
166
|
+
code: `experience_blocked_${state.blocker.kind}`,
|
|
167
|
+
fieldPath: "experience",
|
|
168
|
+
message: state.blocker.message,
|
|
169
|
+
...(state.blocker.visibleText === undefined ? {} : { visibleText: state.blocker.visibleText }),
|
|
170
|
+
resumable: true
|
|
171
|
+
},
|
|
172
|
+
context: await contextFromPage(page, {
|
|
173
|
+
experience: observed.experience,
|
|
174
|
+
selectorProfile: observed.selectorProfile
|
|
175
|
+
})
|
|
176
|
+
};
|
|
177
|
+
}
|
|
128
178
|
function pollAttempts(timeoutMs, pollMs) {
|
|
129
179
|
return Math.max(1, Math.ceil(Math.max(0, timeoutMs) / pollMs));
|
|
130
180
|
}
|
|
@@ -142,9 +192,14 @@ async function navigateConversationToSurfaceHome(page, timeoutMs) {
|
|
|
142
192
|
return true;
|
|
143
193
|
}
|
|
144
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
|
+
}
|
|
145
201
|
const evidence = [];
|
|
146
202
|
const composerLabels = snapshot.composerLabels.map(normalizeForLabelMatch);
|
|
147
|
-
const controls = snapshot.mainControls.map(normalizeForLabelMatch);
|
|
148
203
|
const mainText = normalizeForLabelMatch(snapshot.mainText);
|
|
149
204
|
const selectedSurfaceLabels = (snapshot.selectedSurfaceLabels ?? []).map(normalizeForLabelMatch);
|
|
150
205
|
const url = snapshot.url.toLowerCase();
|
|
@@ -166,13 +221,18 @@ export function detectExperienceFromSnapshot(snapshot) {
|
|
|
166
221
|
for (const label of chatComposer) {
|
|
167
222
|
evidence.push({ source: "composer", label });
|
|
168
223
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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)" });
|
|
174
233
|
}
|
|
175
|
-
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)
|
|
176
236
|
&& hasAnyLabel([label], [
|
|
177
237
|
...localeLabels.configurationOptions.light,
|
|
178
238
|
...localeLabels.configurationOptions.medium,
|
|
@@ -192,7 +252,7 @@ export function detectExperienceFromSnapshot(snapshot) {
|
|
|
192
252
|
}
|
|
193
253
|
const workScore = workComposer.length * 4
|
|
194
254
|
+ (workSurfaceSelected ? 10 : 0)
|
|
195
|
-
+ (
|
|
255
|
+
+ (workAxes ? 6 : 0)
|
|
196
256
|
// The active Work task drops the Chat/Work radio and keeps the shared
|
|
197
257
|
// "Chat with ChatGPT" textbox name. Its compound model + effort opener is
|
|
198
258
|
// therefore strong enough to disambiguate that continuation surface.
|
|
@@ -253,7 +313,19 @@ export async function readSurfaceSnapshot(page) {
|
|
|
253
313
|
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
254
314
|
const normalizeComparable = (value) => normalize(value).toLocaleLowerCase();
|
|
255
315
|
const wantedSurfaceLabels = new Set(surfaceOptionLabels.map(normalizeComparable));
|
|
256
|
-
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
|
+
}
|
|
257
329
|
const composerNodes = composerRoots.flatMap(root => [
|
|
258
330
|
root,
|
|
259
331
|
...Array.from(root.querySelectorAll("textarea, [contenteditable='true'], [role='textbox'], input"))
|
|
@@ -264,18 +336,29 @@ export async function readSurfaceSnapshot(page) {
|
|
|
264
336
|
.map(normalize)
|
|
265
337
|
.filter(Boolean)))
|
|
266
338
|
.slice(0, 16);
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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']"))
|
|
274
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))
|
|
275
354
|
.map(labelFor)
|
|
276
355
|
.map(normalize)
|
|
277
|
-
.filter(Boolean)))
|
|
278
|
-
|
|
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);
|
|
279
362
|
const surfaceTextNodes = main === null ? [] : Array.from(main.querySelectorAll("h1, h2, h3, form, [data-testid*='composer' i], [class*='composer' i]"))
|
|
280
363
|
.filter(visible)
|
|
281
364
|
.slice(0, 32);
|
|
@@ -286,7 +369,7 @@ export async function readSurfaceSnapshot(page) {
|
|
|
286
369
|
.map(normalize)
|
|
287
370
|
.filter(label => wantedSurfaceLabels.has(normalizeComparable(label)))))
|
|
288
371
|
.slice(0, 4);
|
|
289
|
-
return { composerLabels, mainControls, mainText, selectedSurfaceLabels };
|
|
372
|
+
return { composerLabels, mainControls, composerControls, controlGroups, mainText, selectedSurfaceLabels };
|
|
290
373
|
}, [
|
|
291
374
|
...localeLabels.experienceOptions.chat,
|
|
292
375
|
...localeLabels.experienceOptions.work,
|