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.
Files changed (191) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  7. package/contracts/v1/fixtures/operation-action.json +13 -0
  8. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  9. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  10. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  11. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  12. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  13. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  14. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  15. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  16. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  17. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  18. package/contracts/v1/fixtures/operation-event.json +18 -0
  19. package/contracts/v1/fixtures/operation-handle.json +10 -0
  20. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  21. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  22. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  23. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  24. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  25. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  26. package/contracts/v1/fixtures/operation-request.json +33 -0
  27. package/contracts/v1/fixtures/operation-state.json +144 -0
  28. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  29. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  30. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  31. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  32. package/contracts/v1/manifest.json +155 -1
  33. package/contracts/v1/parity-suite.json +96 -1
  34. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  35. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  36. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  37. package/contracts/v1/schemas/manifest.schema.json +57 -0
  38. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  39. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  40. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  41. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  42. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  43. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  44. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  45. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  46. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  47. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  48. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  49. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  50. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  51. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  52. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  53. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  54. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  55. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  56. package/contracts/v1/surface-drift-policy.json +50 -0
  57. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  58. package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
  59. package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
  60. package/dist/src/backend/client.d.ts +103 -2
  61. package/dist/src/backend/client.js +1436 -97
  62. package/dist/src/backend/compatibility.d.ts +12 -0
  63. package/dist/src/backend/compatibility.js +208 -0
  64. package/dist/src/backend/protocol.d.ts +84 -1
  65. package/dist/src/backend/protocol.js +31 -5
  66. package/dist/src/backend/runtime-identity.d.ts +10 -0
  67. package/dist/src/backend/runtime-identity.js +141 -0
  68. package/dist/src/backend/session.d.ts +10 -2
  69. package/dist/src/backend/session.js +478 -7
  70. package/dist/src/backend/stdio-server.d.ts +4 -1
  71. package/dist/src/backend/stdio-server.js +240 -28
  72. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  73. package/dist/src/browser/active-composer-file-input.js +33 -0
  74. package/dist/src/browser/attach.d.ts +4 -1
  75. package/dist/src/browser/attach.js +136 -28
  76. package/dist/src/browser/downloads.d.ts +2 -0
  77. package/dist/src/client.d.ts +38 -0
  78. package/dist/src/client.js +1222 -59
  79. package/dist/src/commands/configuration.js +13 -7
  80. package/dist/src/commands/doctor.d.ts +1 -1
  81. package/dist/src/commands/doctor.js +26 -3
  82. package/dist/src/commands/experience.js +35 -0
  83. package/dist/src/commands/files.js +4 -27
  84. package/dist/src/commands/modes.js +49 -17
  85. package/dist/src/commands/power-discovery.d.ts +130 -0
  86. package/dist/src/commands/power-discovery.js +627 -0
  87. package/dist/src/commands/project-sources.js +325 -31
  88. package/dist/src/commands/sequence.js +59 -32
  89. package/dist/src/commands/session.js +7 -2
  90. package/dist/src/commands/work.js +31 -0
  91. package/dist/src/errors.d.ts +9 -0
  92. package/dist/src/errors.js +22 -0
  93. package/dist/src/index.d.ts +6 -0
  94. package/dist/src/index.js +9 -0
  95. package/dist/src/operations/artifact-output.d.ts +69 -0
  96. package/dist/src/operations/artifact-output.js +1748 -0
  97. package/dist/src/operations/artifact-stream.d.ts +19 -0
  98. package/dist/src/operations/artifact-stream.js +35 -0
  99. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  100. package/dist/src/operations/artifact-transfer.js +1312 -0
  101. package/dist/src/operations/browser-adapter.d.ts +161 -0
  102. package/dist/src/operations/browser-adapter.js +2023 -0
  103. package/dist/src/operations/browser-observation.d.ts +110 -0
  104. package/dist/src/operations/browser-observation.js +1208 -0
  105. package/dist/src/operations/browser-target.d.ts +103 -0
  106. package/dist/src/operations/browser-target.js +489 -0
  107. package/dist/src/operations/canonical.d.ts +4 -0
  108. package/dist/src/operations/canonical.js +412 -0
  109. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  110. package/dist/src/operations/chatgpt-runtime.js +1150 -0
  111. package/dist/src/operations/client.d.ts +138 -0
  112. package/dist/src/operations/client.js +1143 -0
  113. package/dist/src/operations/collector.d.ts +195 -0
  114. package/dist/src/operations/collector.js +1039 -0
  115. package/dist/src/operations/control.d.ts +372 -0
  116. package/dist/src/operations/control.js +1498 -0
  117. package/dist/src/operations/file-identity.d.ts +33 -0
  118. package/dist/src/operations/file-identity.js +153 -0
  119. package/dist/src/operations/handle.d.ts +19 -0
  120. package/dist/src/operations/handle.js +727 -0
  121. package/dist/src/operations/index.d.ts +22 -0
  122. package/dist/src/operations/index.js +22 -0
  123. package/dist/src/operations/journal.d.ts +142 -0
  124. package/dist/src/operations/journal.js +1931 -0
  125. package/dist/src/operations/production-attachments.d.ts +116 -0
  126. package/dist/src/operations/production-attachments.js +1018 -0
  127. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  128. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  129. package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
  130. package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
  131. package/dist/src/operations/production-configuration.d.ts +42 -0
  132. package/dist/src/operations/production-configuration.js +1363 -0
  133. package/dist/src/operations/production-primitives.d.ts +53 -0
  134. package/dist/src/operations/production-primitives.js +1073 -0
  135. package/dist/src/operations/production-work-steer.d.ts +203 -0
  136. package/dist/src/operations/production-work-steer.js +1218 -0
  137. package/dist/src/operations/recovery.d.ts +80 -0
  138. package/dist/src/operations/recovery.js +174 -0
  139. package/dist/src/operations/runtime-adapter.d.ts +118 -0
  140. package/dist/src/operations/runtime-adapter.js +786 -0
  141. package/dist/src/operations/send-once.d.ts +224 -0
  142. package/dist/src/operations/send-once.js +1081 -0
  143. package/dist/src/operations/service.d.ts +281 -0
  144. package/dist/src/operations/service.js +2810 -0
  145. package/dist/src/operations/staging.d.ts +136 -0
  146. package/dist/src/operations/staging.js +630 -0
  147. package/dist/src/operations/state-machine.d.ts +29 -0
  148. package/dist/src/operations/state-machine.js +2047 -0
  149. package/dist/src/operations/submission.d.ts +423 -0
  150. package/dist/src/operations/submission.js +1676 -0
  151. package/dist/src/operations/turn-ownership.d.ts +202 -0
  152. package/dist/src/operations/turn-ownership.js +700 -0
  153. package/dist/src/operations/types.d.ts +454 -0
  154. package/dist/src/operations/types.js +20 -0
  155. package/dist/src/operations/wire-requests.d.ts +21 -0
  156. package/dist/src/operations/wire-requests.js +396 -0
  157. package/dist/src/operations/wire-results.d.ts +97 -0
  158. package/dist/src/operations/wire-results.js +818 -0
  159. package/dist/src/runner/responses.d.ts +3 -1
  160. package/dist/src/runner/responses.js +19 -1
  161. package/dist/src/runner/result.js +227 -55
  162. package/dist/src/runner/types.d.ts +12 -0
  163. package/dist/src/runtime/command-routing.d.ts +149 -0
  164. package/dist/src/runtime/command-routing.js +431 -0
  165. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  166. package/dist/src/runtime/coordinated-browser.js +315 -0
  167. package/dist/src/runtime/coordinated-page.d.ts +39 -0
  168. package/dist/src/runtime/coordinated-page.js +634 -0
  169. package/dist/src/runtime/operation-context.d.ts +142 -0
  170. package/dist/src/runtime/operation-context.js +410 -0
  171. package/dist/src/runtime/runtime-session.d.ts +95 -0
  172. package/dist/src/runtime/runtime-session.js +314 -0
  173. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  174. package/dist/src/runtime/tab-coordinator.js +1200 -0
  175. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  176. package/dist/src/runtime/value-boundaries.js +49 -0
  177. package/dist/src/safety/untrusted-output.js +2 -1
  178. package/dist/src/scripts/backend-server.js +4 -1
  179. package/dist/src/scripts/live-smoke/harness.js +79 -7
  180. package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
  181. package/dist/src/scripts/live-smoke/scenarios.js +110 -18
  182. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  183. package/dist/src/scripts/release-canary-module.js +17 -4
  184. package/dist/src/types.d.ts +38 -1
  185. package/package.json +1 -1
  186. package/references/2026-08-16-transactional-operations.md +459 -0
  187. package/references/agents-runner.md +34 -0
  188. package/references/backend-protocol.md +73 -0
  189. package/references/python-parity.md +72 -0
  190. package/references/responses-adapter.md +19 -0
  191. 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
- for (let attempt = 0; attempt < 2; attempt += 1) {
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 === 0) {
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. Reopen once and click from a
279
- // fresh DOM instance instead of treating that transient race as drift.
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?.(200);
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 instanceof Error && "code" in 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 instanceof Error && "code" in 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
- selected.push(sliderSelection);
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 exposes the five canonical intelligence levels as an
131
- * ARIA slider. Use it only when its visible min/max/current metadata exactly
132
- * matches that five-step scale, and accept the move only when the composer
133
- * visibly echoes the requested label. The Advanced submenu remains the
134
- * fallback for every unrecognized shape.
135
+ * 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 wanted = request.modeId === undefined
138
- ? undefined
139
- : CANONICAL_INTELLIGENCE_ORDER.get(request.modeId);
140
- const slider = page.locator?.("[role='slider'][aria-valuemin='0'][aria-valuemax='4']");
141
- if (wanted === undefined
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 !== 0
157
- || state.max !== CANONICAL_INTELLIGENCE_ORDER.size - 1
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 = wanted > state.now ? "ArrowRight" : "ArrowLeft";
164
- for (let step = 0; step < Math.abs(wanted - state.now); step += 1) {
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
- return findUniqueVisibleLabelForRequest(await visibleModeButtonLabelList(page), request);
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;