codex-chatgpt-control 0.3.0-alpha.1 → 0.5.0-alpha.1

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 (214) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +42 -2
  3. package/contracts/v1/fixtures/backend-capabilities.json +10 -0
  4. package/contracts/v1/fixtures/command-descriptors.json +257 -6
  5. package/contracts/v1/fixtures/describe-runner-run.json +5 -2
  6. package/contracts/v1/fixtures/doctor-scenario-preflight.json +34 -2
  7. package/contracts/v1/fixtures/help-root.json +1 -1
  8. package/contracts/v1/fixtures/output-json-parse-success.json +9 -0
  9. package/contracts/v1/fixtures/run-timeout-partial.json +15 -4
  10. package/contracts/v1/fixtures/stream-in-progress.ndjson +3 -0
  11. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
  12. package/contracts/v1/fixtures/surface-chat-legacy.json +35 -0
  13. package/contracts/v1/fixtures/surface-chat-simplified.json +37 -0
  14. package/contracts/v1/fixtures/surface-sidebar-false-positive.json +29 -0
  15. package/contracts/v1/fixtures/surface-work-advanced.json +45 -0
  16. package/contracts/v1/fixtures/surface-work-basic.json +38 -0
  17. package/contracts/v1/fixtures/workflow-ask-success.json +8 -0
  18. package/contracts/v1/manifest.json +32 -1
  19. package/contracts/v1/parity-suite.json +113 -0
  20. package/contracts/v1/schemas/backend-request.schema.json +15 -0
  21. package/contracts/v1/schemas/manifest.schema.json +6 -3
  22. package/contracts/v1/schemas/surface-profile.schema.json +166 -0
  23. package/dist/codex-chatgpt-control-backend.mjs +2045 -227
  24. package/dist/codex-chatgpt-control.bundle.mjs +3817 -1960
  25. package/dist/src/backend/client.d.ts +26 -1
  26. package/dist/src/backend/client.js +23 -1
  27. package/dist/src/backend/protocol.d.ts +1 -1
  28. package/dist/src/backend/protocol.js +10 -0
  29. package/dist/src/backend/session.js +20 -0
  30. package/dist/src/browser/attach.d.ts +1 -0
  31. package/dist/src/browser/attach.js +3 -3
  32. package/dist/src/browser/page-state.js +5 -2
  33. package/dist/src/client.d.ts +31 -1
  34. package/dist/src/client.js +69 -14
  35. package/dist/src/commands/artifacts.js +1 -7
  36. package/dist/src/commands/configuration.d.ts +15 -0
  37. package/dist/src/commands/configuration.js +674 -0
  38. package/dist/src/commands/doctor.js +38 -1
  39. package/dist/src/commands/experience.d.ts +12 -0
  40. package/dist/src/commands/experience.js +288 -0
  41. package/dist/src/commands/files.js +1 -7
  42. package/dist/src/commands/messages.d.ts +2 -1
  43. package/dist/src/commands/messages.js +138 -16
  44. package/dist/src/commands/modes.js +28 -14
  45. package/dist/src/commands/registry.js +69 -7
  46. package/dist/src/commands/response-actions.js +1 -7
  47. package/dist/src/commands/sequence.js +24 -1
  48. package/dist/src/commands/session.d.ts +2 -0
  49. package/dist/src/commands/session.js +50 -1
  50. package/dist/src/commands/threads.js +1 -7
  51. package/dist/src/commands/work.d.ts +6 -0
  52. package/dist/src/commands/work.js +338 -0
  53. package/dist/src/dom/locale/am.d.ts +2 -1
  54. package/dist/src/dom/locale/am.js +2 -1
  55. package/dist/src/dom/locale/ar.d.ts +2 -1
  56. package/dist/src/dom/locale/ar.js +2 -1
  57. package/dist/src/dom/locale/bg.d.ts +2 -1
  58. package/dist/src/dom/locale/bg.js +2 -1
  59. package/dist/src/dom/locale/bn.d.ts +2 -1
  60. package/dist/src/dom/locale/bn.js +2 -1
  61. package/dist/src/dom/locale/bs.d.ts +2 -1
  62. package/dist/src/dom/locale/bs.js +2 -1
  63. package/dist/src/dom/locale/ca.d.ts +2 -1
  64. package/dist/src/dom/locale/ca.js +2 -1
  65. package/dist/src/dom/locale/cs.d.ts +2 -1
  66. package/dist/src/dom/locale/cs.js +2 -1
  67. package/dist/src/dom/locale/da.d.ts +2 -1
  68. package/dist/src/dom/locale/da.js +2 -1
  69. package/dist/src/dom/locale/de.d.ts +4 -3
  70. package/dist/src/dom/locale/de.js +4 -3
  71. package/dist/src/dom/locale/el.d.ts +2 -1
  72. package/dist/src/dom/locale/el.js +2 -1
  73. package/dist/src/dom/locale/en.d.ts +27 -2
  74. package/dist/src/dom/locale/en.js +28 -2
  75. package/dist/src/dom/locale/es-419.d.ts +2 -1
  76. package/dist/src/dom/locale/es-419.js +2 -1
  77. package/dist/src/dom/locale/es-ES.d.ts +2 -1
  78. package/dist/src/dom/locale/es-ES.js +2 -1
  79. package/dist/src/dom/locale/et.d.ts +2 -1
  80. package/dist/src/dom/locale/et.js +2 -1
  81. package/dist/src/dom/locale/fa.d.ts +2 -1
  82. package/dist/src/dom/locale/fa.js +2 -1
  83. package/dist/src/dom/locale/fi.d.ts +2 -1
  84. package/dist/src/dom/locale/fi.js +2 -1
  85. package/dist/src/dom/locale/fr-CA.d.ts +2 -1
  86. package/dist/src/dom/locale/fr-CA.js +2 -1
  87. package/dist/src/dom/locale/fr-FR.d.ts +3 -3
  88. package/dist/src/dom/locale/fr-FR.js +3 -3
  89. package/dist/src/dom/locale/gu.d.ts +2 -1
  90. package/dist/src/dom/locale/gu.js +2 -1
  91. package/dist/src/dom/locale/hi.d.ts +2 -1
  92. package/dist/src/dom/locale/hi.js +2 -1
  93. package/dist/src/dom/locale/hr.d.ts +2 -1
  94. package/dist/src/dom/locale/hr.js +2 -1
  95. package/dist/src/dom/locale/hu.d.ts +2 -1
  96. package/dist/src/dom/locale/hu.js +2 -1
  97. package/dist/src/dom/locale/hy.d.ts +2 -1
  98. package/dist/src/dom/locale/hy.js +2 -1
  99. package/dist/src/dom/locale/id.d.ts +2 -1
  100. package/dist/src/dom/locale/id.js +2 -1
  101. package/dist/src/dom/locale/index.d.ts +17 -2
  102. package/dist/src/dom/locale/index.js +65 -0
  103. package/dist/src/dom/locale/is.d.ts +2 -1
  104. package/dist/src/dom/locale/is.js +2 -1
  105. package/dist/src/dom/locale/it.d.ts +2 -1
  106. package/dist/src/dom/locale/it.js +2 -1
  107. package/dist/src/dom/locale/ja.d.ts +2 -1
  108. package/dist/src/dom/locale/ja.js +2 -1
  109. package/dist/src/dom/locale/ka.d.ts +2 -1
  110. package/dist/src/dom/locale/ka.js +2 -1
  111. package/dist/src/dom/locale/kk.d.ts +2 -1
  112. package/dist/src/dom/locale/kk.js +2 -1
  113. package/dist/src/dom/locale/kn.d.ts +2 -1
  114. package/dist/src/dom/locale/kn.js +2 -1
  115. package/dist/src/dom/locale/ko.d.ts +2 -1
  116. package/dist/src/dom/locale/ko.js +2 -1
  117. package/dist/src/dom/locale/lt.d.ts +2 -1
  118. package/dist/src/dom/locale/lt.js +2 -1
  119. package/dist/src/dom/locale/lv.d.ts +2 -1
  120. package/dist/src/dom/locale/lv.js +2 -1
  121. package/dist/src/dom/locale/mk.d.ts +2 -1
  122. package/dist/src/dom/locale/mk.js +2 -1
  123. package/dist/src/dom/locale/ml.d.ts +2 -1
  124. package/dist/src/dom/locale/ml.js +2 -1
  125. package/dist/src/dom/locale/mn.d.ts +2 -1
  126. package/dist/src/dom/locale/mn.js +2 -1
  127. package/dist/src/dom/locale/mr.d.ts +2 -1
  128. package/dist/src/dom/locale/mr.js +2 -1
  129. package/dist/src/dom/locale/ms.d.ts +2 -1
  130. package/dist/src/dom/locale/ms.js +2 -1
  131. package/dist/src/dom/locale/my.d.ts +2 -1
  132. package/dist/src/dom/locale/my.js +2 -1
  133. package/dist/src/dom/locale/nb.d.ts +2 -1
  134. package/dist/src/dom/locale/nb.js +2 -1
  135. package/dist/src/dom/locale/nl.d.ts +2 -1
  136. package/dist/src/dom/locale/nl.js +2 -1
  137. package/dist/src/dom/locale/pa.d.ts +2 -1
  138. package/dist/src/dom/locale/pa.js +2 -1
  139. package/dist/src/dom/locale/pl.d.ts +2 -1
  140. package/dist/src/dom/locale/pl.js +2 -1
  141. package/dist/src/dom/locale/pt-BR.d.ts +2 -1
  142. package/dist/src/dom/locale/pt-BR.js +2 -1
  143. package/dist/src/dom/locale/pt-PT.d.ts +2 -1
  144. package/dist/src/dom/locale/pt-PT.js +2 -1
  145. package/dist/src/dom/locale/ro.d.ts +2 -1
  146. package/dist/src/dom/locale/ro.js +2 -1
  147. package/dist/src/dom/locale/ru.d.ts +2 -1
  148. package/dist/src/dom/locale/ru.js +2 -1
  149. package/dist/src/dom/locale/sk.d.ts +2 -1
  150. package/dist/src/dom/locale/sk.js +2 -1
  151. package/dist/src/dom/locale/sl.d.ts +2 -1
  152. package/dist/src/dom/locale/sl.js +2 -1
  153. package/dist/src/dom/locale/so.d.ts +2 -1
  154. package/dist/src/dom/locale/so.js +2 -1
  155. package/dist/src/dom/locale/sq.d.ts +2 -1
  156. package/dist/src/dom/locale/sq.js +2 -1
  157. package/dist/src/dom/locale/sr.d.ts +2 -1
  158. package/dist/src/dom/locale/sr.js +2 -1
  159. package/dist/src/dom/locale/sv.d.ts +2 -1
  160. package/dist/src/dom/locale/sv.js +2 -1
  161. package/dist/src/dom/locale/sw.d.ts +2 -1
  162. package/dist/src/dom/locale/sw.js +2 -1
  163. package/dist/src/dom/locale/ta.d.ts +2 -1
  164. package/dist/src/dom/locale/ta.js +2 -1
  165. package/dist/src/dom/locale/te.d.ts +2 -1
  166. package/dist/src/dom/locale/te.js +2 -1
  167. package/dist/src/dom/locale/th.d.ts +2 -1
  168. package/dist/src/dom/locale/th.js +2 -1
  169. package/dist/src/dom/locale/tl.d.ts +4 -5
  170. package/dist/src/dom/locale/tl.js +4 -5
  171. package/dist/src/dom/locale/tr.d.ts +2 -1
  172. package/dist/src/dom/locale/tr.js +2 -1
  173. package/dist/src/dom/locale/types.d.ts +18 -1
  174. package/dist/src/dom/locale/uk.d.ts +2 -1
  175. package/dist/src/dom/locale/uk.js +2 -1
  176. package/dist/src/dom/locale/ur.d.ts +2 -1
  177. package/dist/src/dom/locale/ur.js +2 -1
  178. package/dist/src/dom/locale/vi.d.ts +2 -1
  179. package/dist/src/dom/locale/vi.js +2 -1
  180. package/dist/src/dom/locale/zh-HK.d.ts +2 -1
  181. package/dist/src/dom/locale/zh-HK.js +4 -3
  182. package/dist/src/dom/locale/zh-Hans.d.ts +2 -1
  183. package/dist/src/dom/locale/zh-Hans.js +2 -1
  184. package/dist/src/dom/locale/zh-TW.d.ts +2 -1
  185. package/dist/src/dom/locale/zh-TW.js +2 -1
  186. package/dist/src/dom/selectors.js +6 -1
  187. package/dist/src/index.d.ts +3 -0
  188. package/dist/src/index.js +3 -0
  189. package/dist/src/runner/responses.d.ts +3 -1
  190. package/dist/src/runner/responses.js +16 -1
  191. package/dist/src/runner/result.js +129 -4
  192. package/dist/src/runner/stream.d.ts +1 -1
  193. package/dist/src/runner/stream.js +6 -0
  194. package/dist/src/runner/types.d.ts +35 -1
  195. package/dist/src/safety/risk.d.ts +10 -0
  196. package/dist/src/safety/risk.js +10 -0
  197. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +6 -0
  198. package/dist/src/scripts/apply-intelligence-locale-captures.js +53 -7
  199. package/dist/src/scripts/capture-intelligence-locales.d.ts +3 -0
  200. package/dist/src/scripts/capture-intelligence-locales.js +372 -3
  201. package/dist/src/scripts/capture-surface-profile.d.ts +23 -0
  202. package/dist/src/scripts/capture-surface-profile.js +304 -0
  203. package/dist/src/scripts/live-smoke/scenarios.js +34 -1
  204. package/dist/src/scripts/live-smoke.js +1 -0
  205. package/dist/src/types.d.ts +245 -2
  206. package/package.json +3 -2
  207. package/references/2026-07-16-chat-work-surfaces.md +87 -0
  208. package/references/agents-runner.md +13 -2
  209. package/references/backend-protocol.md +15 -5
  210. package/references/language-coverage.md +40 -3
  211. package/references/localization.md +34 -6
  212. package/references/python-parity.md +11 -1
  213. package/references/responses-adapter.md +12 -1
  214. package/references/troubleshooting.md +22 -2
@@ -3,6 +3,7 @@ import { access, stat } from "node:fs/promises";
3
3
  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
+ import { localeCoverageSummary } from "../dom/locale/index.js";
6
7
  import { BROWSER_BRIDGE_REMEDIATION, resultOk } from "../errors.js";
7
8
  import { explainCommandBlocker } from "../diagnostics/blockers.js";
8
9
  import { preflightFiles } from "./files.js";
@@ -233,6 +234,18 @@ function localizationCheck(env) {
233
234
  const missingModeOptionIds = REQUIRED_MODE_OPTION_IDS.filter(id => (localeLabels.modeOptions[id]?.length ?? 0) === 0);
234
235
  const toolIds = Object.keys(localeLabels.tools);
235
236
  const modeOptionIds = Object.keys(localeLabels.modeOptions);
237
+ const proAliases = [...localeLabels.modeOptions.pro];
238
+ const supportsProExtendedAlias = proAliases.some(alias => /pro\s*(?:•\s*)?extended/i.test(alias))
239
+ && proAliases.some(alias => /extended\s+pro/i.test(alias));
240
+ const runningStateLocalizationSupport = runningStateSupport();
241
+ const runningStateLabelCoverage = {
242
+ support: runningStateLocalizationSupport,
243
+ stopControlCandidateCount: localeLabels.stopControl.length,
244
+ stoppedAssistantCandidateCount: localeLabels.stoppedAssistant.length,
245
+ ...localeCoverageSummary.runningState,
246
+ registeredLocaleCount: localeCoverageSummary.registeredLocaleCount,
247
+ nonEnglishLocaleCount: localeCoverageSummary.nonEnglishLocaleCount
248
+ };
236
249
  const englishCanonicalPresent = localeLabels.composerTextbox[0] === "Chat with ChatGPT"
237
250
  && localeLabels.sendButton[0] === "Send prompt"
238
251
  && localeLabels.modeLabels.includes("Thinking")
@@ -248,15 +261,39 @@ function localizationCheck(env) {
248
261
  missingModeOptionIds,
249
262
  toolIds,
250
263
  modeOptionIds,
264
+ proAliases,
265
+ supportsProExtendedAlias,
266
+ runningStateLabelCoverage,
251
267
  labelCandidateCount,
252
268
  pageAvailable: env.page !== undefined,
253
269
  runtimeSelectorCoverage: "registry_only_stage_2"
254
270
  };
255
271
  if (englishCanonicalPresent && requiredKeysMissing.length === 0 && missingToolIds.length === 0 && missingModeOptionIds.length === 0) {
256
- return unknown("The locale registry is loaded; localized runtime selector coverage is registry-only in Stage 2 and not fully proven.", undefined, details);
272
+ const runningStateMessage = runningStateLocalizationSupport === "complete"
273
+ ? "running-state labels are present for every registered non-English locale"
274
+ : runningStateLocalizationSupport === "partial"
275
+ ? "running-state labels are partially localized"
276
+ : runningStateLocalizationSupport === "english_only"
277
+ ? "running-state labels are English-only"
278
+ : "running-state labels are missing";
279
+ return unknown(`The locale registry is loaded; localized runtime selector coverage is registry-only in Stage 2 and ${runningStateMessage}.`, undefined, details);
257
280
  }
258
281
  return blocked("The locale registry is missing canonical labels required for selector fallback.", ["Update src/dom/locale-labels.ts or src/dom/locale/* with verified visible labels before relying on localized controls."], details, "selector_drift");
259
282
  }
283
+ function runningStateSupport() {
284
+ const coverage = localeCoverageSummary.runningState;
285
+ const hasEnglishStop = coverage.stopControlLocaleCount > coverage.nonEnglishStopControlLocaleCount;
286
+ const hasEnglishStopped = coverage.stoppedAssistantLocaleCount > coverage.nonEnglishStoppedAssistantLocaleCount;
287
+ const hasEnglishFallback = hasEnglishStop && hasEnglishStopped;
288
+ const allNonEnglishCovered = localeCoverageSummary.nonEnglishLocaleCount > 0
289
+ && coverage.nonEnglishStopControlLocaleCount >= localeCoverageSummary.nonEnglishLocaleCount
290
+ && coverage.nonEnglishStoppedAssistantLocaleCount >= localeCoverageSummary.nonEnglishLocaleCount;
291
+ if (allNonEnglishCovered)
292
+ return "complete";
293
+ if (coverage.nonEnglishStopControlLocaleCount > 0 || coverage.nonEnglishStoppedAssistantLocaleCount > 0)
294
+ return "partial";
295
+ return hasEnglishFallback ? "english_only" : "missing";
296
+ }
260
297
  async function reportsCheck(options) {
261
298
  const destDir = options?.destDir ?? "reports/runs";
262
299
  const includeContent = options?.includeContent === true;
@@ -0,0 +1,12 @@
1
+ import type { CommandResult, DetectExperienceArgs, DetectExperienceData, OpenExperienceArgs, OpenExperienceData, PageLike, RuntimeEnv } from "../types.js";
2
+ type SurfaceSnapshot = {
3
+ url: string;
4
+ composerLabels: string[];
5
+ mainControls: string[];
6
+ mainText: string;
7
+ };
8
+ export declare function detectExperience(env: RuntimeEnv, args?: DetectExperienceArgs): Promise<CommandResult<DetectExperienceData>>;
9
+ export declare function openExperience(env: RuntimeEnv, args: OpenExperienceArgs): Promise<CommandResult<OpenExperienceData>>;
10
+ export declare function detectExperienceFromSnapshot(snapshot: SurfaceSnapshot): DetectExperienceData;
11
+ export declare function readSurfaceSnapshot(page: PageLike): Promise<SurfaceSnapshot>;
12
+ export {};
@@ -0,0 +1,288 @@
1
+ import { resultError, resultOk } from "../errors.js";
2
+ import { localeLabels } from "../dom/locale-labels.js";
3
+ import { normalizeForLabelMatch, visibleLabelMatches } from "../dom/label-match.js";
4
+ import { contextFromPage } from "./context.js";
5
+ import { ensurePage } from "./session.js";
6
+ export async function detectExperience(env, args = {}) {
7
+ void args;
8
+ const boot = await ensurePage(env);
9
+ if (!boot.ok) {
10
+ return boot;
11
+ }
12
+ const page = env.page;
13
+ try {
14
+ const data = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
15
+ return resultOk(data, await contextFromPage(page, {
16
+ experience: data.experience,
17
+ selectorProfile: data.selectorProfile
18
+ }), data.experience === "unknown"
19
+ ? ["The current ChatGPT surface could not be classified as Chat or Work from scoped composer evidence."]
20
+ : []);
21
+ }
22
+ catch (error) {
23
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
24
+ }
25
+ }
26
+ export async function openExperience(env, args) {
27
+ const boot = await ensurePage(env);
28
+ if (!boot.ok) {
29
+ return boot;
30
+ }
31
+ const page = env.page;
32
+ try {
33
+ const before = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
34
+ if (before.experience === args.experience) {
35
+ return resultOk({
36
+ experience: args.experience,
37
+ previousExperience: before.experience,
38
+ changed: false,
39
+ selectorProfile: before.selectorProfile
40
+ }, await contextFromPage(page, {
41
+ experience: before.experience,
42
+ selectorProfile: before.selectorProfile
43
+ }));
44
+ }
45
+ const labels = localeLabels.experienceOptions[args.experience];
46
+ if (!await clickUniqueExperienceControl(page, labels)) {
47
+ return experienceSelectorDrift(page, `No unique visible ChatGPT ${args.experience === "work" ? "Work" : "Chat"} surface control was found.`, before);
48
+ }
49
+ const timeoutMs = args.timeoutMs ?? 30000;
50
+ const started = Date.now();
51
+ let after = before;
52
+ while (Date.now() - started < timeoutMs) {
53
+ await page.waitForTimeout?.(250);
54
+ after = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
55
+ if (after.experience === args.experience) {
56
+ return resultOk({
57
+ experience: args.experience,
58
+ previousExperience: before.experience,
59
+ changed: true,
60
+ selectorProfile: after.selectorProfile
61
+ }, await contextFromPage(page, {
62
+ experience: after.experience,
63
+ selectorProfile: after.selectorProfile
64
+ }));
65
+ }
66
+ }
67
+ return {
68
+ ok: false,
69
+ status: "blocked",
70
+ warnings: [],
71
+ blocker: {
72
+ kind: "selector_drift",
73
+ code: "experience_postcondition_unverified",
74
+ fieldPath: "experience",
75
+ message: `The ${args.experience} surface control was clicked, but the composer did not verify that ChatGPT switched to ${args.experience}.`,
76
+ candidates: labels.map(label => ({ label })),
77
+ resumable: true
78
+ },
79
+ context: await contextFromPage(page, {
80
+ experience: after.experience,
81
+ selectorProfile: after.selectorProfile
82
+ })
83
+ };
84
+ }
85
+ catch (error) {
86
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
87
+ }
88
+ }
89
+ export function detectExperienceFromSnapshot(snapshot) {
90
+ const evidence = [];
91
+ const composerLabels = snapshot.composerLabels.map(normalizeForLabelMatch);
92
+ const controls = snapshot.mainControls.map(normalizeForLabelMatch);
93
+ const mainText = normalizeForLabelMatch(snapshot.mainText);
94
+ const url = snapshot.url.toLowerCase();
95
+ const workComposer = matchingLabels(composerLabels, localeLabels.workComposerTextbox);
96
+ for (const label of workComposer) {
97
+ evidence.push({ source: "composer", label });
98
+ }
99
+ const chatComposer = matchingLabels(composerLabels, localeLabels.composerTextbox);
100
+ for (const label of chatComposer) {
101
+ evidence.push({ source: "composer", label });
102
+ }
103
+ const workAxisCount = ["model", "effort", "speed"]
104
+ .filter(axis => hasAnyLabel(controls, localeLabels.configurationAxes[axis]))
105
+ .length;
106
+ if (workAxisCount >= 2) {
107
+ evidence.push({ source: "control", label: `Work configuration axes (${workAxisCount}/3)` });
108
+ }
109
+ if (/\/work(?:\/|$|\?)/.test(url)) {
110
+ evidence.push({ source: "url", label: snapshot.url });
111
+ }
112
+ if (containsAny(mainText, ["work on something else", "work on anything"])) {
113
+ evidence.push({ source: "heading", label: "Work composer copy" });
114
+ }
115
+ const workScore = workComposer.length * 4
116
+ + (workAxisCount >= 2 ? 4 : 0)
117
+ + (/\/work(?:\/|$|\?)/.test(url) ? 3 : 0)
118
+ + (containsAny(mainText, ["work on something else", "work on anything"]) ? 2 : 0);
119
+ const chatScore = chatComposer.length * 4;
120
+ let experience = "unknown";
121
+ let confidence = "low";
122
+ if (workScore > chatScore && workScore >= 4) {
123
+ experience = "work";
124
+ confidence = workScore >= 7 ? "high" : "medium";
125
+ }
126
+ else if (chatScore > workScore && chatScore >= 4) {
127
+ experience = "chat";
128
+ confidence = "high";
129
+ }
130
+ const selectorProfile = profileFromSnapshot(snapshot, experience);
131
+ return { experience, selectorProfile, confidence, evidence };
132
+ }
133
+ export async function readSurfaceSnapshot(page) {
134
+ const url = typeof page.url === "function"
135
+ ? await Promise.resolve(page.url()).catch(() => "")
136
+ : "";
137
+ if (typeof page.evaluate !== "function") {
138
+ return { url, composerLabels: [], mainControls: [], mainText: "" };
139
+ }
140
+ const snapshot = await page.evaluate(() => {
141
+ const visible = (element) => {
142
+ const html = element;
143
+ const rect = html.getBoundingClientRect?.();
144
+ const style = typeof window !== "undefined" ? window.getComputedStyle?.(html) : undefined;
145
+ return (rect === undefined || (rect.width > 0 && rect.height > 0))
146
+ && style?.display !== "none"
147
+ && style?.visibility !== "hidden"
148
+ && style?.opacity !== "0";
149
+ };
150
+ const labelFor = (element) => {
151
+ const html = element;
152
+ return element.getAttribute("aria-label")
153
+ ?? element.getAttribute("placeholder")
154
+ ?? html.innerText
155
+ ?? element.textContent
156
+ ?? "";
157
+ };
158
+ const normalize = (value) => value.replace(/\s+/g, " ").trim();
159
+ const composerRoots = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i], main [class*='composer' i]"));
160
+ const composerNodes = composerRoots.flatMap(root => [
161
+ root,
162
+ ...Array.from(root.querySelectorAll("textarea, [contenteditable='true'], [role='textbox'], input"))
163
+ ]);
164
+ const composerLabels = Array.from(new Set(composerNodes
165
+ .filter(visible)
166
+ .map(labelFor)
167
+ .map(normalize)
168
+ .filter(Boolean)))
169
+ .slice(0, 16);
170
+ const main = document.querySelector("main");
171
+ const overlayRoots = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper], [data-radix-menu-content]")).filter(visible);
172
+ const controlRoots = Array.from(new Set([...composerRoots, ...overlayRoots]));
173
+ const effectiveControlRoots = controlRoots.length > 0
174
+ ? controlRoots
175
+ : main === null ? [] : [main];
176
+ const mainControls = Array.from(new Set(effectiveControlRoots.flatMap(root => Array.from(root.querySelectorAll("button, [role='button'], [role='menuitem'], [role='menuitemradio'], [role='option']")))
177
+ .filter(visible)
178
+ .map(labelFor)
179
+ .map(normalize)
180
+ .filter(Boolean)))
181
+ .slice(0, 120);
182
+ const surfaceTextNodes = main === null ? [] : Array.from(main.querySelectorAll("h1, h2, h3, form, [data-testid*='composer' i], [class*='composer' i]"))
183
+ .filter(visible)
184
+ .slice(0, 32);
185
+ const mainText = normalize(surfaceTextNodes.map(labelFor).join(" ")).slice(0, 2000);
186
+ return { composerLabels, mainControls, mainText };
187
+ }).catch(() => ({ composerLabels: [], mainControls: [], mainText: "" }));
188
+ return { url, ...snapshot };
189
+ }
190
+ function profileFromSnapshot(snapshot, experience) {
191
+ const controls = snapshot.mainControls.map(normalizeForLabelMatch);
192
+ const mainText = normalizeForLabelMatch(snapshot.mainText);
193
+ if (experience === "work") {
194
+ return hasAnyLabel(controls, localeLabels.configurationAxes.advanced)
195
+ || containsAny(mainText, localeLabels.configurationAxes.advanced)
196
+ ? "work_advanced_v1"
197
+ : "work_basic_v1";
198
+ }
199
+ if (experience !== "chat") {
200
+ return "unknown";
201
+ }
202
+ const simplifiedOptions = [
203
+ ...localeLabels.configurationOptions.instant,
204
+ ...localeLabels.configurationOptions.medium,
205
+ ...localeLabels.configurationOptions.high,
206
+ ...localeLabels.configurationOptions.extraHigh,
207
+ ...localeLabels.configurationOptions.pro,
208
+ ];
209
+ if (hasAnyLabel(controls, simplifiedOptions)) {
210
+ return "chat_simplified_v1";
211
+ }
212
+ const legacyOptions = [
213
+ ...localeLabels.modeOptions.latest,
214
+ ...localeLabels.modeOptions.thinking,
215
+ ...localeLabels.modeOptions.extended,
216
+ ];
217
+ return hasAnyLabel(controls, legacyOptions)
218
+ ? "chat_legacy_v1"
219
+ : "chat_simplified_v1";
220
+ }
221
+ async function clickUniqueExperienceControl(page, labels) {
222
+ for (const label of labels) {
223
+ for (const role of ["button", "menuitem", "tab", "link"]) {
224
+ if (await clickIfUnique(page.getByRole?.(role, { name: label, exact: true }))) {
225
+ return true;
226
+ }
227
+ }
228
+ }
229
+ if (typeof page.evaluate !== "function") {
230
+ return false;
231
+ }
232
+ return page.evaluate((wantedLabels) => {
233
+ const normalize = (value) => value.replace(/\s+/g, " ").trim().toLocaleLowerCase();
234
+ const wanted = new Set(wantedLabels.map(normalize));
235
+ const nodes = Array.from(document.querySelectorAll("header button, header [role='button'], header [role='tab'], main [role='menuitem'], main [role='option']"));
236
+ const matches = nodes.filter(node => {
237
+ const html = node;
238
+ const label = node.getAttribute("aria-label") ?? html.innerText ?? node.textContent ?? "";
239
+ return wanted.has(normalize(label));
240
+ });
241
+ if (matches.length !== 1)
242
+ return false;
243
+ matches[0].click();
244
+ return true;
245
+ }, labels).catch(() => false);
246
+ }
247
+ async function clickIfUnique(locator) {
248
+ if (locator?.count === undefined || locator.click === undefined) {
249
+ return false;
250
+ }
251
+ if (await locator.count().catch(() => 0) !== 1) {
252
+ return false;
253
+ }
254
+ await locator.click();
255
+ return true;
256
+ }
257
+ function matchingLabels(normalizedHaystack, candidates) {
258
+ const normalizedCandidates = candidates.map(normalizeForLabelMatch);
259
+ return normalizedHaystack
260
+ .filter(label => normalizedCandidates.some(candidate => label === candidate || visibleLabelMatches(label, candidate)))
261
+ .slice(0, 4);
262
+ }
263
+ function hasAnyLabel(normalizedHaystack, candidates) {
264
+ const normalizedCandidates = candidates.map(normalizeForLabelMatch);
265
+ return normalizedHaystack.some(label => normalizedCandidates.some(candidate => label === candidate || visibleLabelMatches(label, candidate)));
266
+ }
267
+ function containsAny(normalizedText, candidates) {
268
+ return candidates.map(normalizeForLabelMatch).some(candidate => normalizedText.includes(candidate));
269
+ }
270
+ async function experienceSelectorDrift(page, message, detected) {
271
+ return {
272
+ ok: false,
273
+ status: "unsupported",
274
+ warnings: [],
275
+ blocker: {
276
+ kind: "selector_drift",
277
+ code: "experience_control_not_found",
278
+ fieldPath: "experience",
279
+ message,
280
+ candidates: detected.evidence.map(item => ({ label: `${item.source}: ${item.label}` })),
281
+ resumable: true
282
+ },
283
+ context: await contextFromPage(page, {
284
+ experience: detected.experience,
285
+ selectorProfile: detected.selectorProfile
286
+ })
287
+ };
288
+ }
@@ -9,7 +9,7 @@ import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.
9
9
  import { localeLabels } from "../dom/locale-labels.js";
10
10
  import { basenameForHostPath, isHostAbsolutePath, resolveForHostPath } from "../platform/local-paths.js";
11
11
  import { contextFromPage } from "./context.js";
12
- import { bootstrap } from "./session.js";
12
+ import { ensurePage } from "./session.js";
13
13
  import { localGuardTimeout } from "./timeouts.js";
14
14
  const CODEX_UPLOAD_PERMISSION_FIX = "Codex Settings > Computer Use > Chrome > Permissions > Uploads: set to Always allow, or add chatgpt.com to the allowed upload domains.";
15
15
  const CHROME_FILE_URL_PERMISSION_FIX = "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs.";
@@ -610,12 +610,6 @@ async function readBrowserInputDiagnostic(page) {
610
610
  };
611
611
  });
612
612
  }
613
- async function ensurePage(env) {
614
- if (env.page !== undefined) {
615
- return resultOk({}, await contextFromPage(env.page));
616
- }
617
- return bootstrap(env, { preferExistingTab: true });
618
- }
619
613
  async function setFilesViaDomDataTransfer(page, files) {
620
614
  const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
621
615
  const maxInlineBytes = 25 * 1024 * 1024;
@@ -1,5 +1,5 @@
1
1
  import { type AssistantGenerationState } from "../dom/generation-state.js";
2
- import type { AskArgs, AskReadData, CommandResult, ComposeArgs, ComposeData, ReadLatestArgs, ReadLatestData, RuntimeEnv, SubmitArgs, SubmitData, WaitAndReadArgs, WaitArgs, WaitData } from "../types.js";
2
+ import type { AskArgs, AskReadData, CommandResult, ComposeArgs, ComposeData, MessageStatusArgs, MessageStatusData, ReadLatestArgs, ReadLatestData, RuntimeEnv, SubmitArgs, SubmitData, WaitAndReadArgs, WaitArgs, WaitData } from "../types.js";
3
3
  export type CompletionSnapshot = {
4
4
  textStableForMs: number;
5
5
  stableMs: number;
@@ -12,6 +12,7 @@ export declare function composeMessage(env: RuntimeEnv, args: ComposeArgs): Prom
12
12
  export declare function submitMessage(env: RuntimeEnv, args?: SubmitArgs): Promise<CommandResult<SubmitData>>;
13
13
  export declare function waitForMessage(env: RuntimeEnv, args?: WaitArgs): Promise<CommandResult<WaitData>>;
14
14
  export declare function readLatest(env: RuntimeEnv, args?: ReadLatestArgs): Promise<CommandResult<ReadLatestData>>;
15
+ export declare function messageStatus(env: RuntimeEnv, args?: MessageStatusArgs): Promise<CommandResult<MessageStatusData>>;
15
16
  export declare function askMessage(env: RuntimeEnv, args: AskArgs): Promise<CommandResult<AskReadData>>;
16
17
  export declare function waitAndRead(env: RuntimeEnv, args?: WaitAndReadArgs): Promise<CommandResult<AskReadData>>;
17
18
  export declare function submittedUserTurnMatches(actual: string | undefined, wanted: string | undefined): boolean;