codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.3
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 +15 -0
- package/README.md +27 -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/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 +155 -1
- package/contracts/v1/parity-suite.json +96 -1
- 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 +43897 -9094
- package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
- 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/downloads.d.ts +2 -0
- package/dist/src/client.d.ts +38 -0
- package/dist/src/client.js +1222 -59
- package/dist/src/commands/configuration.js +13 -7
- package/dist/src/commands/doctor.d.ts +1 -1
- package/dist/src/commands/doctor.js +26 -3
- package/dist/src/commands/experience.js +35 -0
- package/dist/src/commands/files.js +4 -27
- package/dist/src/commands/modes.js +49 -17
- package/dist/src/commands/power-discovery.d.ts +130 -0
- package/dist/src/commands/power-discovery.js +627 -0
- package/dist/src/commands/project-sources.js +325 -31
- package/dist/src/commands/sequence.js +59 -32
- package/dist/src/commands/session.js +7 -2
- package/dist/src/commands/work.js +31 -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 +161 -0
- package/dist/src/operations/browser-adapter.js +2023 -0
- package/dist/src/operations/browser-observation.d.ts +110 -0
- package/dist/src/operations/browser-observation.js +1208 -0
- package/dist/src/operations/browser-target.d.ts +103 -0
- package/dist/src/operations/browser-target.js +489 -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 +1150 -0
- package/dist/src/operations/client.d.ts +138 -0
- package/dist/src/operations/client.js +1143 -0
- package/dist/src/operations/collector.d.ts +195 -0
- package/dist/src/operations/collector.js +1039 -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 +22 -0
- package/dist/src/operations/index.js +22 -0
- package/dist/src/operations/journal.d.ts +142 -0
- package/dist/src/operations/journal.js +1931 -0
- package/dist/src/operations/production-attachments.d.ts +116 -0
- package/dist/src/operations/production-attachments.js +1018 -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 +71 -0
- package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
- package/dist/src/operations/production-configuration.d.ts +42 -0
- package/dist/src/operations/production-configuration.js +1363 -0
- package/dist/src/operations/production-primitives.d.ts +53 -0
- package/dist/src/operations/production-primitives.js +1073 -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 +118 -0
- package/dist/src/operations/runtime-adapter.js +786 -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 +281 -0
- package/dist/src/operations/service.js +2810 -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/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 +39 -0
- package/dist/src/runtime/coordinated-page.js +634 -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/untrusted-output.js +2 -1
- package/dist/src/scripts/backend-server.js +4 -1
- package/dist/src/scripts/live-smoke/harness.js +79 -7
- package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
- package/dist/src/scripts/live-smoke/scenarios.js +110 -18
- 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 +1 -1
- package/references/2026-08-16-transactional-operations.md +459 -0
- package/references/agents-runner.md +34 -0
- package/references/backend-protocol.md +73 -0
- package/references/python-parity.md +72 -0
- package/references/responses-adapter.md +19 -0
- package/references/streaming.md +6 -0
|
@@ -10,6 +10,8 @@ import { ensurePage } from "./session.js";
|
|
|
10
10
|
const WORK_AXES = ["model", "effort", "speed"];
|
|
11
11
|
const CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
12
12
|
const CONFIGURATION_CONTROL_POLL_MS = 250;
|
|
13
|
+
const CONFIGURATION_SELECTION_MAX_ATTEMPTS = 6;
|
|
14
|
+
const CONFIGURATION_SELECTION_RETRY_MS = 400;
|
|
13
15
|
const CONFIGURATION_AXIS_ORDER = [
|
|
14
16
|
"model",
|
|
15
17
|
"intelligence",
|
|
@@ -158,7 +160,7 @@ export async function applyConfiguration(env, args) {
|
|
|
158
160
|
continue;
|
|
159
161
|
}
|
|
160
162
|
const selection = before.experience === "work"
|
|
161
|
-
? await selectWorkAxis(env, axis, requested)
|
|
163
|
+
? await selectWorkAxis(env, axis, requested, args.timeoutMs)
|
|
162
164
|
: await selectChatAxis(env, axis, requested, args.timeoutMs);
|
|
163
165
|
if (selection === undefined) {
|
|
164
166
|
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));
|
|
@@ -261,24 +263,28 @@ async function inspectWorkAxisOptions(env, axis) {
|
|
|
261
263
|
await closeConfigurationSubmenu(page);
|
|
262
264
|
return dedupeOptions(options);
|
|
263
265
|
}
|
|
264
|
-
async function selectWorkAxis(env, axis, requested) {
|
|
266
|
+
async function selectWorkAxis(env, axis, requested, timeoutMs) {
|
|
265
267
|
const page = env.page;
|
|
266
268
|
if (!WORK_AXES.includes(axis)) {
|
|
267
269
|
return undefined;
|
|
268
270
|
}
|
|
269
|
-
|
|
271
|
+
const retryWindowMs = Math.min(timeoutMs ?? CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS, CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS);
|
|
272
|
+
const attempts = Math.max(2, Math.min(CONFIGURATION_SELECTION_MAX_ATTEMPTS, Math.ceil(Math.max(0, retryWindowMs) / CONFIGURATION_SELECTION_RETRY_MS) + 1));
|
|
273
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
270
274
|
const candidates = await openWorkAxisOptions(env, axis);
|
|
271
275
|
const match = findConfigurationOption(candidates, requested);
|
|
272
276
|
if (match !== undefined && await clickVisibleMenuItem(page, match)) {
|
|
273
277
|
await page.waitForTimeout?.(150);
|
|
274
278
|
return match.label;
|
|
275
279
|
}
|
|
276
|
-
if (attempt
|
|
280
|
+
if (attempt + 1 < attempts) {
|
|
277
281
|
// The submenu can disappear between enumeration and the visible click
|
|
278
|
-
// when ChatGPT's pointer-grace timer fires
|
|
279
|
-
//
|
|
282
|
+
// when ChatGPT's pointer-grace timer fires, and Work may briefly retain
|
|
283
|
+
// the previous submenu immediately after a selection. Reopen from a
|
|
284
|
+
// fresh DOM instance for a small bounded window instead of treating the
|
|
285
|
+
// transient state as permanent selector drift.
|
|
280
286
|
await closeConfigurationMenus(page);
|
|
281
|
-
await page.waitForTimeout?.(
|
|
287
|
+
await page.waitForTimeout?.(CONFIGURATION_SELECTION_RETRY_MS);
|
|
282
288
|
}
|
|
283
289
|
}
|
|
284
290
|
return undefined;
|
|
@@ -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.");
|
|
@@ -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
|
}
|
|
@@ -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";
|
|
@@ -34,6 +35,9 @@ export async function openExperience(env, args) {
|
|
|
34
35
|
const page = env.page;
|
|
35
36
|
try {
|
|
36
37
|
const before = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
38
|
+
const initialBlocker = await experiencePageBlocker(page, before);
|
|
39
|
+
if (initialBlocker !== undefined)
|
|
40
|
+
return initialBlocker;
|
|
37
41
|
if (before.experience === args.experience) {
|
|
38
42
|
return resultOk({
|
|
39
43
|
experience: args.experience,
|
|
@@ -52,6 +56,9 @@ export async function openExperience(env, args) {
|
|
|
52
56
|
let controlClicked = await clickUniqueExperienceControl(page, labels);
|
|
53
57
|
if (!controlClicked && await navigateConversationToSurfaceHome(page, args.timeoutMs)) {
|
|
54
58
|
observed = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
|
|
59
|
+
const navigationBlocker = await experiencePageBlocker(page, observed);
|
|
60
|
+
if (navigationBlocker !== undefined)
|
|
61
|
+
return navigationBlocker;
|
|
55
62
|
if (observed.experience === args.experience) {
|
|
56
63
|
return resultOk({
|
|
57
64
|
experience: args.experience,
|
|
@@ -85,6 +92,9 @@ export async function openExperience(env, args) {
|
|
|
85
92
|
controlClicked = await clickUniqueExperienceControl(page, labels);
|
|
86
93
|
}
|
|
87
94
|
if (!controlClicked) {
|
|
95
|
+
const discoveryBlocker = await experiencePageBlocker(page, observed);
|
|
96
|
+
if (discoveryBlocker !== undefined)
|
|
97
|
+
return discoveryBlocker;
|
|
88
98
|
return experienceSelectorDrift(page, `No unique visible ChatGPT ${args.experience === "work" ? "Work" : "Chat"} surface control was found.`, observed);
|
|
89
99
|
}
|
|
90
100
|
let after = before;
|
|
@@ -103,6 +113,9 @@ export async function openExperience(env, args) {
|
|
|
103
113
|
}));
|
|
104
114
|
}
|
|
105
115
|
}
|
|
116
|
+
const postconditionBlocker = await experiencePageBlocker(page, after);
|
|
117
|
+
if (postconditionBlocker !== undefined)
|
|
118
|
+
return postconditionBlocker;
|
|
106
119
|
return {
|
|
107
120
|
ok: false,
|
|
108
121
|
status: "blocked",
|
|
@@ -125,6 +138,28 @@ export async function openExperience(env, args) {
|
|
|
125
138
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
126
139
|
}
|
|
127
140
|
}
|
|
141
|
+
async function experiencePageBlocker(page, observed) {
|
|
142
|
+
const state = await readPageState(page);
|
|
143
|
+
if (state.blocker === undefined)
|
|
144
|
+
return undefined;
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
status: "blocked",
|
|
148
|
+
warnings: [],
|
|
149
|
+
blocker: {
|
|
150
|
+
kind: state.blocker.kind,
|
|
151
|
+
code: `experience_blocked_${state.blocker.kind}`,
|
|
152
|
+
fieldPath: "experience",
|
|
153
|
+
message: state.blocker.message,
|
|
154
|
+
...(state.blocker.visibleText === undefined ? {} : { visibleText: state.blocker.visibleText }),
|
|
155
|
+
resumable: true
|
|
156
|
+
},
|
|
157
|
+
context: await contextFromPage(page, {
|
|
158
|
+
experience: observed.experience,
|
|
159
|
+
selectorProfile: observed.selectorProfile
|
|
160
|
+
})
|
|
161
|
+
};
|
|
162
|
+
}
|
|
128
163
|
function pollAttempts(timeoutMs, pollMs) {
|
|
129
164
|
return Math.max(1, Math.ceil(Math.max(0, timeoutMs) / pollMs));
|
|
130
165
|
}
|
|
@@ -3,8 +3,9 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { downloadLatestArtifact, locatorCountWithTimeout } from "./artifacts.js";
|
|
6
|
+
import { ACTIVE_COMPOSER_FILE_INPUT_CLICK_EXPRESSION } from "../browser/active-composer-file-input.js";
|
|
6
7
|
import { waitForDownloadFromClick } from "../browser/downloads.js";
|
|
7
|
-
import { resultError, resultOk } from "../errors.js";
|
|
8
|
+
import { nodeErrorCode, resultError, resultOk } from "../errors.js";
|
|
8
9
|
import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.js";
|
|
9
10
|
import { escapeRegExp, localeLabels } from "../dom/locale-labels.js";
|
|
10
11
|
import { basenameForHostPath, currentHostPathPlatform, isHostAbsolutePath, resolveForHostPath } from "../platform/local-paths.js";
|
|
@@ -410,7 +411,7 @@ function guessFileType(extension) {
|
|
|
410
411
|
}
|
|
411
412
|
}
|
|
412
413
|
function isNodeError(error) {
|
|
413
|
-
return error
|
|
414
|
+
return nodeErrorCode(error) !== undefined;
|
|
414
415
|
}
|
|
415
416
|
async function readAttachmentEvidenceBaseline(page, timeoutMs) {
|
|
416
417
|
if (typeof page.evaluate !== "function") {
|
|
@@ -762,31 +763,7 @@ async function clickHiddenFileInputWithCdp(page, paths, deadline, mutationState)
|
|
|
762
763
|
const chooserPromise = waitForFileChooser(page, deadline).then(chooser => ({ ok: true, chooser }), error => ({ ok: false, error }));
|
|
763
764
|
try {
|
|
764
765
|
const evaluation = await withinNativeAttachmentDeadline(deadline, timeoutMs => Promise.resolve(cdp.send("Runtime.evaluate", {
|
|
765
|
-
expression:
|
|
766
|
-
const visible = element => {
|
|
767
|
-
if (element.hidden || element.closest("[hidden], [inert], [aria-hidden='true']")) return false;
|
|
768
|
-
const style = getComputedStyle(element);
|
|
769
|
-
const rect = element.getBoundingClientRect();
|
|
770
|
-
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0"
|
|
771
|
-
&& (rect.width > 0 || rect.height > 0);
|
|
772
|
-
};
|
|
773
|
-
const textboxes = [...document.querySelectorAll("textarea, [contenteditable='true'], [role='textbox']")].filter(visible);
|
|
774
|
-
const composers = [...new Set(textboxes.map(textbox =>
|
|
775
|
-
textbox.closest("form")
|
|
776
|
-
?? textbox.closest("[data-testid*='composer' i]")
|
|
777
|
-
?? textbox.closest("[aria-label*='composer' i]")
|
|
778
|
-
?? textbox.closest("[class*='composer' i]")
|
|
779
|
-
).filter(Boolean))];
|
|
780
|
-
if (composers.length !== 1) return { ok: false, reason: "active composer was not unique" };
|
|
781
|
-
const all = [...composers[0].querySelectorAll("input[type='file']")]
|
|
782
|
-
.filter(input => !input.disabled && input.getAttribute("aria-disabled") !== "true");
|
|
783
|
-
const preferred = all.filter(input => input.id === "upload-files");
|
|
784
|
-
const nonImage = all.filter(input => input.getAttribute("accept") !== "image/*");
|
|
785
|
-
const candidates = preferred.length ? preferred : nonImage.length ? nonImage : all;
|
|
786
|
-
if (candidates.length !== 1) return { ok: false, reason: "active composer file input was not unique" };
|
|
787
|
-
candidates[0].click();
|
|
788
|
-
return { ok: true };
|
|
789
|
-
})()`,
|
|
766
|
+
expression: ACTIVE_COMPOSER_FILE_INPUT_CLICK_EXPRESSION,
|
|
790
767
|
userGesture: true,
|
|
791
768
|
awaitPromise: true,
|
|
792
769
|
returnByValue: true
|
|
@@ -4,6 +4,7 @@ import { localeLabels } from "../dom/locale-labels.js";
|
|
|
4
4
|
import { isShortLatinToken, normalizeForLabelMatch, visibleLabelMatches } from "../dom/label-match.js";
|
|
5
5
|
import { normalizeLabel, normalizeWhitespace } from "../dom/visible-text.js";
|
|
6
6
|
import { contextFromPage } from "./context.js";
|
|
7
|
+
import { discoverPowerSlider, observedPowerSlider, resolvePowerTarget } from "./power-discovery.js";
|
|
7
8
|
import { ensurePage } from "./session.js";
|
|
8
9
|
const DEFAULT_MODE_EFFORT = "Thinking";
|
|
9
10
|
const CURRENT_MODE_LABELS = dedupeLabels([
|
|
@@ -81,7 +82,11 @@ export async function setMode(env, args) {
|
|
|
81
82
|
if (match === undefined) {
|
|
82
83
|
const sliderSelection = await selectModeWithPowerSlider(page, request);
|
|
83
84
|
if (sliderSelection !== undefined) {
|
|
84
|
-
|
|
85
|
+
// A slider press is already a mutation. Even if the postcondition
|
|
86
|
+
// label is unavailable, do not fall through to Advanced and issue a
|
|
87
|
+
// second UI action against the same request. The final warning pass
|
|
88
|
+
// will expose that this selection remains unverified.
|
|
89
|
+
selected.push(sliderSelection.selected);
|
|
85
90
|
continue;
|
|
86
91
|
}
|
|
87
92
|
const nested = await openEffortSubmenu(page, candidates, request);
|
|
@@ -127,18 +132,23 @@ export async function setMode(env, args) {
|
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
134
|
/**
|
|
130
|
-
* Chat's compact picker
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
+
* Chat's compact picker can expose a Power/reasoning ARIA slider. Discover the
|
|
136
|
+
* control from its semantic relationship to the localized Power axis and its
|
|
137
|
+
* owning menu, then use only a complete, bounded, DOM-provided value mapping.
|
|
138
|
+
* The Advanced submenu remains the fallback for every unrecognized or
|
|
139
|
+
* unprobed shape; in particular, this path never infers labels from a fixed
|
|
140
|
+
* five-position range.
|
|
135
141
|
*/
|
|
136
142
|
async function selectModeWithPowerSlider(page, request) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
const discovery = await discoverPowerSlider(page, {
|
|
144
|
+
powerLabels: localeLabels.configurationAxes.power
|
|
145
|
+
});
|
|
146
|
+
if (!discovery.ok || await isWorkPowerSurface(page, discovery.evidence.surface)) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
const target = resolvePowerTarget(discovery, request.labels);
|
|
150
|
+
const slider = observedPowerSlider(page, discovery);
|
|
151
|
+
if (target === undefined
|
|
142
152
|
|| slider?.count === undefined
|
|
143
153
|
|| slider.evaluate === undefined
|
|
144
154
|
|| slider.press === undefined
|
|
@@ -147,25 +157,47 @@ async function selectModeWithPowerSlider(page, request) {
|
|
|
147
157
|
|| await slider.count().catch(() => 0) !== 1) {
|
|
148
158
|
return undefined;
|
|
149
159
|
}
|
|
160
|
+
// Re-read the three values on the selected locator so a menu replacement
|
|
161
|
+
// between discovery and selection cannot apply an old operation's plan to a
|
|
162
|
+
// newly rendered slider.
|
|
150
163
|
const state = await slider.evaluate(element => ({
|
|
151
164
|
min: Number(element.getAttribute("aria-valuemin")),
|
|
152
165
|
max: Number(element.getAttribute("aria-valuemax")),
|
|
153
166
|
now: Number(element.getAttribute("aria-valuenow"))
|
|
154
167
|
})).catch(() => undefined);
|
|
155
168
|
if (state === undefined
|
|
156
|
-
|| state.min !==
|
|
157
|
-
|| state.max !==
|
|
169
|
+
|| state.min !== discovery.range.minimum
|
|
170
|
+
|| state.max !== discovery.range.maximum
|
|
158
171
|
|| !Number.isInteger(state.now)
|
|
159
172
|
|| state.now < state.min
|
|
160
|
-
|| state.now > state.max
|
|
173
|
+
|| state.now > state.max
|
|
174
|
+
|| target < state.min
|
|
175
|
+
|| target > state.max) {
|
|
161
176
|
return undefined;
|
|
162
177
|
}
|
|
163
|
-
const key =
|
|
164
|
-
for (let step = 0; step < Math.abs(
|
|
178
|
+
const key = target > state.now ? "ArrowRight" : "ArrowLeft";
|
|
179
|
+
for (let step = 0; step < Math.abs(target - state.now); step += 1) {
|
|
165
180
|
await slider.press(key);
|
|
166
181
|
}
|
|
167
182
|
await page.waitForTimeout?.(150);
|
|
168
|
-
|
|
183
|
+
const after = await slider.evaluate(element => Number(element.getAttribute("aria-valuenow")))
|
|
184
|
+
.catch(() => undefined);
|
|
185
|
+
const reflected = findUniqueVisibleLabelForRequest(await visibleModeButtonLabelList(page), request);
|
|
186
|
+
return {
|
|
187
|
+
// A silent/no-op keypress must not be reported as the requested level. We
|
|
188
|
+
// still return a handled selection to prevent a second Advanced mutation;
|
|
189
|
+
// the normal visible-mode verification warning exposes the unresolved
|
|
190
|
+
// postcondition to the caller.
|
|
191
|
+
selected: after === target ? reflected ?? request.requested : request.requested
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async function isWorkPowerSurface(page, discoveredSurface) {
|
|
195
|
+
if (discoveredSurface === "work")
|
|
196
|
+
return true;
|
|
197
|
+
const url = typeof page.url === "function"
|
|
198
|
+
? await Promise.resolve(page.url()).catch(() => "")
|
|
199
|
+
: "";
|
|
200
|
+
return /\/work(?:\/|$|\?)/i.test(url);
|
|
169
201
|
}
|
|
170
202
|
/**
|
|
171
203
|
* Current Chat exposes the intelligence choices inside an Advanced "Effort <value>"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { LocatorLike, PageLike } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The upper bound is deliberately conservative. A Power selector is expected
|
|
4
|
+
* to expose a small, discrete set of reasoning levels; accepting an enormous
|
|
5
|
+
* range would turn an unverified selector into a long mutation loop and would
|
|
6
|
+
* make an aria typo surprisingly expensive.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_POWER_LEVELS = 32;
|
|
9
|
+
export declare const MAX_POWER_SLIDERS = 32;
|
|
10
|
+
/** Maximum browser-realm nodes inspected by one Power discovery probe. */
|
|
11
|
+
export declare const MAX_POWER_DOM_NODES = 4096;
|
|
12
|
+
export type PowerSurface = "chat" | "work" | "unknown";
|
|
13
|
+
export type PowerOptionEvidence = {
|
|
14
|
+
label: string;
|
|
15
|
+
value?: number;
|
|
16
|
+
};
|
|
17
|
+
export type PowerSliderRange = {
|
|
18
|
+
minimum: number;
|
|
19
|
+
maximum: number;
|
|
20
|
+
current: number;
|
|
21
|
+
count: number;
|
|
22
|
+
};
|
|
23
|
+
export type PowerSliderEvidence = {
|
|
24
|
+
role: "slider";
|
|
25
|
+
sliderIndex: number;
|
|
26
|
+
relationship: "aria-label" | "aria-labelledby" | "owner" | "menu-label";
|
|
27
|
+
matchedPowerLabels: string[];
|
|
28
|
+
surface: PowerSurface;
|
|
29
|
+
selectorProfile: string;
|
|
30
|
+
menuRole: string;
|
|
31
|
+
ownerRole?: string;
|
|
32
|
+
valueText?: string;
|
|
33
|
+
options: PowerOptionEvidence[];
|
|
34
|
+
range: PowerSliderRange;
|
|
35
|
+
};
|
|
36
|
+
export type PowerDiscoveryFailureReason = "no_visible_slider" | "no_semantic_power_slider" | "ambiguous_power_slider" | "missing_menu_relationship" | "invalid_aria_range" | "unsupported_range" | "observation_limit_exceeded" | "surface_mismatch";
|
|
37
|
+
export type PowerDiscoveryFailureEvidence = {
|
|
38
|
+
visibleSliderCount: number;
|
|
39
|
+
semanticSliderCount: number;
|
|
40
|
+
invalidSemanticSliderCount: number;
|
|
41
|
+
hiddenSliderCount: number;
|
|
42
|
+
observedProfiles: string[];
|
|
43
|
+
observationTruncated: boolean;
|
|
44
|
+
};
|
|
45
|
+
export type PowerDiscoveryResult = {
|
|
46
|
+
ok: true;
|
|
47
|
+
evidence: PowerSliderEvidence;
|
|
48
|
+
sliderIndex: number;
|
|
49
|
+
range: PowerSliderRange;
|
|
50
|
+
options: PowerOptionEvidence[];
|
|
51
|
+
/** The non-English/visible label reported by the slider, if available. */
|
|
52
|
+
valueText?: string;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
reason: PowerDiscoveryFailureReason;
|
|
56
|
+
evidence: PowerDiscoveryFailureEvidence;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* A serializable DOM observation used by the pure classifier below. Keeping
|
|
60
|
+
* the classifier independent from a browser implementation makes ambiguity and
|
|
61
|
+
* range rejection deterministic in unit tests, and prevents a later caller
|
|
62
|
+
* from accidentally turning inspection into a mutation.
|
|
63
|
+
*/
|
|
64
|
+
export type PowerSliderDomObservation = {
|
|
65
|
+
index: number;
|
|
66
|
+
visible: boolean;
|
|
67
|
+
ariaLabel?: string;
|
|
68
|
+
labelledByText?: string;
|
|
69
|
+
valueText?: string;
|
|
70
|
+
minimum?: string;
|
|
71
|
+
maximum?: string;
|
|
72
|
+
current?: string;
|
|
73
|
+
step?: string;
|
|
74
|
+
owner?: {
|
|
75
|
+
role?: string;
|
|
76
|
+
label?: string;
|
|
77
|
+
text?: string;
|
|
78
|
+
visible: boolean;
|
|
79
|
+
};
|
|
80
|
+
menu?: {
|
|
81
|
+
role: string;
|
|
82
|
+
label?: string;
|
|
83
|
+
text?: string;
|
|
84
|
+
visible: boolean;
|
|
85
|
+
};
|
|
86
|
+
surface?: {
|
|
87
|
+
experience?: PowerSurface;
|
|
88
|
+
selectorProfile?: string;
|
|
89
|
+
};
|
|
90
|
+
options?: Array<{
|
|
91
|
+
label: string;
|
|
92
|
+
value?: string;
|
|
93
|
+
visible: boolean;
|
|
94
|
+
}>;
|
|
95
|
+
optionSource?: "datalist" | "owner" | "power_menu";
|
|
96
|
+
optionsTruncated?: boolean;
|
|
97
|
+
};
|
|
98
|
+
export type PowerDomObservation = {
|
|
99
|
+
sliders: PowerSliderDomObservation[];
|
|
100
|
+
slidersTruncated?: boolean;
|
|
101
|
+
};
|
|
102
|
+
export type PowerDiscoveryOptions = {
|
|
103
|
+
powerLabels?: readonly string[];
|
|
104
|
+
expectedSurface?: PowerSurface;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Inspect the visible DOM once. This function is intentionally read-only: it
|
|
108
|
+
* never focuses, presses, clicks, hovers, waits, or writes to the page.
|
|
109
|
+
*/
|
|
110
|
+
export declare function discoverPowerSlider(page: PageLike, options?: PowerDiscoveryOptions): Promise<PowerDiscoveryResult>;
|
|
111
|
+
/**
|
|
112
|
+
* Classify one bounded, read-only DOM observation. No state is retained; each
|
|
113
|
+
* call starts from the supplied snapshot and therefore cannot leak a previous
|
|
114
|
+
* operation's discovered range or value mapping.
|
|
115
|
+
*/
|
|
116
|
+
export declare function classifyPowerSliderObservation(observation: PowerDomObservation, options?: PowerDiscoveryOptions): PowerDiscoveryResult;
|
|
117
|
+
/**
|
|
118
|
+
* Resolve a visible requested label to a numeric value only when the DOM gave
|
|
119
|
+
* us a complete semantic mapping. A current aria-valuetext is enough to
|
|
120
|
+
* recognize that no mutation is required; it is not enough to guess another
|
|
121
|
+
* level. This is the important boundary that keeps ordinary inspection and
|
|
122
|
+
* unprobed slider ranges fail-closed.
|
|
123
|
+
*/
|
|
124
|
+
export declare function resolvePowerTarget(discovery: Extract<PowerDiscoveryResult, {
|
|
125
|
+
ok: true;
|
|
126
|
+
}>, requestedLabels: readonly string[]): number | undefined;
|
|
127
|
+
/** Return a locator for the observed slider without broadening the selector. */
|
|
128
|
+
export declare function observedPowerSlider(page: PageLike, discovery: Extract<PowerDiscoveryResult, {
|
|
129
|
+
ok: true;
|
|
130
|
+
}>): LocatorLike | undefined;
|