@steipete/oracle 0.9.0 → 0.11.0

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 (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,7 +1,7 @@
1
- import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from '../constants.js';
2
- import { logDomFailure } from '../domDebug.js';
3
- import { buildClickDispatcher } from './domEvents.js';
4
- export async function ensureModelSelection(Runtime, desiredModel, logger, strategy = 'select') {
1
+ import { COMPOSER_MODEL_SIGNAL_SELECTOR, MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from "../constants.js";
2
+ import { logDomFailure } from "../domDebug.js";
3
+ import { buildClickDispatcher } from "./domEvents.js";
4
+ export async function ensureModelSelection(Runtime, desiredModel, logger, strategy = "select") {
5
5
  const outcome = await Runtime.evaluate({
6
6
  expression: buildModelSelectionExpression(desiredModel, strategy),
7
7
  awaitPromise: true,
@@ -9,52 +9,88 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
9
9
  });
10
10
  const result = outcome.result?.value;
11
11
  switch (result?.status) {
12
- case 'already-selected':
13
- case 'switched':
14
- case 'switched-best-effort': {
12
+ case "already-selected":
13
+ case "switched":
14
+ case "switched-best-effort": {
15
15
  const label = result.label ?? desiredModel;
16
+ if (strategy !== "current") {
17
+ assertResolvedModelSelection(desiredModel, label);
18
+ }
16
19
  logger(`Model picker: ${label}`);
17
20
  return;
18
21
  }
19
- case 'option-not-found': {
20
- await logDomFailure(Runtime, logger, 'model-switcher-option');
22
+ case "option-not-found": {
23
+ await logDomFailure(Runtime, logger, "model-switcher-option");
21
24
  const isTemporary = result.hint?.temporaryChat ?? false;
22
25
  const available = (result.hint?.availableOptions ?? []).filter(Boolean);
23
- const availableHint = available.length > 0 ? ` Available: ${available.join(', ')}.` : '';
26
+ const availableHint = available.length > 0 ? ` Available: ${available.join(", ")}.` : "";
24
27
  const tempHint = isTemporary && /\bpro\b/i.test(desiredModel)
25
28
  ? ' You are in Temporary Chat mode; Pro models are not available there. Remove "temporary-chat=true" from --chatgpt-url or use a non-Pro model (e.g. gpt-5.2).'
26
- : '';
29
+ : "";
27
30
  throw new Error(`Unable to find model option matching "${desiredModel}" in the model switcher.${availableHint}${tempHint}`);
28
31
  }
29
32
  default: {
30
- await logDomFailure(Runtime, logger, 'model-switcher-button');
31
- throw new Error('Unable to locate the ChatGPT model selector button.');
33
+ await logDomFailure(Runtime, logger, "model-switcher-button");
34
+ throw new Error("Unable to locate the ChatGPT model selector button.");
32
35
  }
33
36
  }
34
37
  }
38
+ function assertResolvedModelSelection(desiredModel, resolvedLabel) {
39
+ const desired = desiredModel.toLowerCase();
40
+ const resolved = resolvedLabel.toLowerCase();
41
+ const wantsGpt55Pro = desired === "gpt-5.5-pro" ||
42
+ desired.includes("5.5 pro") ||
43
+ desired.includes("5-5 pro") ||
44
+ (desired.includes("pro") && desired.includes("extended"));
45
+ if (!wantsGpt55Pro || !resolved) {
46
+ return;
47
+ }
48
+ const hasProSignal = resolved.includes(" pro") ||
49
+ resolved.endsWith("pro") ||
50
+ resolved.includes("pro ") ||
51
+ resolved.includes("extended") ||
52
+ resolved.includes("gpt-5.5-pro") ||
53
+ resolved.includes("gpt 5 5 pro");
54
+ if (!hasProSignal || (resolved.includes("thinking") && !resolved.includes("pro"))) {
55
+ throw new Error(`Model picker selected "${resolvedLabel}" while "${desiredModel}" requires GPT-5.5 Pro Extended. Use model "gpt-5.5" with browser thinking time "heavy" for Thinking Heavy.`);
56
+ }
57
+ }
58
+ export function assertResolvedModelSelectionForTest(desiredModel, resolvedLabel) {
59
+ assertResolvedModelSelection(desiredModel, resolvedLabel);
60
+ }
35
61
  /**
36
62
  * Builds the DOM expression that runs inside the ChatGPT tab to select a model.
37
63
  * The string is evaluated inside Chrome, so keep it self-contained and well-commented.
38
64
  */
39
65
  function buildModelSelectionExpression(targetModel, strategy) {
40
66
  const matchers = buildModelMatchersLiteral(targetModel);
67
+ const composerSignalMatchers = buildComposerSignalMatchers(targetModel);
41
68
  const labelLiteral = JSON.stringify(matchers.labelTokens);
42
69
  const idLiteral = JSON.stringify(matchers.testIdTokens);
43
70
  const primaryLabelLiteral = JSON.stringify(targetModel);
44
71
  const strategyLiteral = JSON.stringify(strategy);
72
+ const composerSignalSelectorLiteral = JSON.stringify(COMPOSER_MODEL_SIGNAL_SELECTOR);
73
+ const composerIncludesLiteral = JSON.stringify(composerSignalMatchers.includesAny);
74
+ const composerExcludesLiteral = JSON.stringify(composerSignalMatchers.excludesAny);
75
+ const composerAllowBlankLiteral = JSON.stringify(composerSignalMatchers.allowBlank);
45
76
  const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
46
77
  const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
47
78
  return `(() => {
48
79
  ${buildClickDispatcher()}
49
80
  // Capture the selectors and matcher literals up front so the browser expression stays pure.
50
81
  const BUTTON_SELECTOR = '${MODEL_BUTTON_SELECTOR}';
82
+ const COMPOSER_MODEL_SIGNAL_SELECTOR = ${composerSignalSelectorLiteral};
51
83
  const LABEL_TOKENS = ${labelLiteral};
52
84
  const TEST_IDS = ${idLiteral};
53
85
  const PRIMARY_LABEL = ${primaryLabelLiteral};
54
86
  const MODEL_STRATEGY = ${strategyLiteral};
87
+ const COMPOSER_SIGNAL_INCLUDES = ${composerIncludesLiteral};
88
+ const COMPOSER_SIGNAL_EXCLUDES = ${composerExcludesLiteral};
89
+ const COMPOSER_SIGNAL_ALLOW_BLANK = ${composerAllowBlankLiteral};
55
90
  const INITIAL_WAIT_MS = 150;
56
91
  const REOPEN_INTERVAL_MS = 400;
57
92
  const MAX_WAIT_MS = 20000;
93
+ const SETTLE_WAIT_MS = 1500;
58
94
  const normalizeText = (value) => {
59
95
  if (!value) {
60
96
  return '';
@@ -73,16 +109,32 @@ function buildModelSelectionExpression(targetModel, strategy) {
73
109
  const targetWords = normalizedTarget.split(' ').filter(Boolean);
74
110
  const desiredVersion = normalizedTarget.includes('5 4')
75
111
  ? '5-4'
76
- : normalizedTarget.includes('5 2')
112
+ : normalizedTarget.includes('5 5')
113
+ ? '5-5'
114
+ : normalizedTarget.includes('5 2')
77
115
  ? '5-2'
78
116
  : normalizedTarget.includes('5 1')
79
117
  ? '5-1'
80
118
  : normalizedTarget.includes('5 0')
81
119
  ? '5-0'
82
- : null;
120
+ : null;
83
121
  const wantsPro = normalizedTarget.includes(' pro') || normalizedTarget.endsWith(' pro') || normalizedTokens.includes('pro');
84
122
  const wantsInstant = normalizedTarget.includes('instant');
85
123
  const wantsThinking = normalizedTarget.includes('thinking');
124
+ const isTargetGpt55VisibleAlias = (value) => {
125
+ if (desiredVersion !== '5-5') return false;
126
+ const label = normalizeText(value);
127
+ if (wantsPro) {
128
+ return label.includes('pro') && label.includes('extended') && !label.includes('thinking');
129
+ }
130
+ if (wantsThinking) {
131
+ return label.includes('thinking') && label.includes('heavy') && !label.includes('pro');
132
+ }
133
+ return false;
134
+ };
135
+ const hasProComposerPill = () => Boolean(
136
+ document.querySelector('button.__composer-pill, button[aria-label="Pro, click to remove"]')
137
+ );
86
138
 
87
139
  const button = document.querySelector(BUTTON_SELECTOR);
88
140
  if (!button) {
@@ -110,13 +162,34 @@ function buildModelSelectionExpression(targetModel, strategy) {
110
162
  };
111
163
 
112
164
  const getButtonLabel = () => (button.textContent ?? '').trim();
165
+ const getComposerModelLabel = () =>
166
+ (document.querySelector(COMPOSER_MODEL_SIGNAL_SELECTOR)?.textContent ?? '').trim();
167
+ const readComposerModelSignal = () => normalizeText(getComposerModelLabel());
168
+ const withProPillSignal = (label) => {
169
+ const resolved = label || '';
170
+ if (!wantsPro || !hasProComposerPill()) return resolved;
171
+ const normalized = normalizeText(resolved);
172
+ if (!normalized || normalized.includes('pro')) return resolved;
173
+ return resolved + ' + Pro';
174
+ };
175
+ const getResolvedLabel = (fallback) =>
176
+ withProPillSignal(getComposerModelLabel() || getButtonLabel() || fallback);
113
177
  if (MODEL_STRATEGY === 'current') {
114
- return { status: 'already-selected', label: getButtonLabel() };
178
+ const currentLabel = getResolvedLabel(PRIMARY_LABEL);
179
+ return {
180
+ status: 'already-selected',
181
+ label: currentLabel,
182
+ };
115
183
  }
116
184
  const buttonMatchesTarget = () => {
117
185
  const normalizedLabel = normalizeText(getButtonLabel());
118
186
  if (!normalizedLabel) return false;
187
+ if (isTargetGpt55VisibleAlias(normalizedLabel)) return true;
188
+ if (wantsPro && normalizedLabel === 'chatgpt' && hasProComposerPill()) {
189
+ return true;
190
+ }
119
191
  if (desiredVersion) {
192
+ if (desiredVersion === '5-5' && !normalizedLabel.includes('5 5')) return false;
120
193
  if (desiredVersion === '5-4' && !normalizedLabel.includes('5 4')) return false;
121
194
  if (desiredVersion === '5-2' && !normalizedLabel.includes('5 2')) return false;
122
195
  if (desiredVersion === '5-1' && !normalizedLabel.includes('5 1')) return false;
@@ -131,9 +204,47 @@ function buildModelSelectionExpression(targetModel, strategy) {
131
204
  if (!wantsThinking && normalizedLabel.includes('thinking')) return false;
132
205
  return true;
133
206
  };
207
+ const buttonHasGenericLabel = () => {
208
+ const normalizedLabel = normalizeText(getButtonLabel());
209
+ return !normalizedLabel || normalizedLabel === 'chatgpt';
210
+ };
211
+ const composerSignalMatchesTarget = () => {
212
+ const signal = readComposerModelSignal();
213
+ if (!signal) {
214
+ return COMPOSER_SIGNAL_ALLOW_BLANK;
215
+ }
216
+ if (COMPOSER_SIGNAL_EXCLUDES.some((token) => token && signal.includes(token))) {
217
+ return false;
218
+ }
219
+ if (COMPOSER_SIGNAL_INCLUDES.length === 0) {
220
+ return true;
221
+ }
222
+ return COMPOSER_SIGNAL_INCLUDES.some((token) => token && signal.includes(token));
223
+ };
224
+ const activeSelectionMatchesTarget = () => {
225
+ if (buttonMatchesTarget()) {
226
+ return true;
227
+ }
228
+ if (!buttonHasGenericLabel()) {
229
+ return false;
230
+ }
231
+ return composerSignalMatchesTarget();
232
+ };
233
+ const selectionStateChanged = (previousButtonLabel, previousComposerSignal) => {
234
+ const currentButtonLabel = normalizeText(getButtonLabel());
235
+ const currentComposerSignal = readComposerModelSignal();
236
+ if (
237
+ currentButtonLabel &&
238
+ currentButtonLabel !== previousButtonLabel &&
239
+ !buttonHasGenericLabel()
240
+ ) {
241
+ return true;
242
+ }
243
+ return currentComposerSignal !== previousComposerSignal;
244
+ };
134
245
 
135
- if (buttonMatchesTarget()) {
136
- return { status: 'already-selected', label: getButtonLabel() };
246
+ if (activeSelectionMatchesTarget()) {
247
+ return { status: 'already-selected', label: getResolvedLabel(PRIMARY_LABEL) };
137
248
  }
138
249
 
139
250
  let lastPointerClick = 0;
@@ -144,6 +255,10 @@ function buildModelSelectionExpression(targetModel, strategy) {
144
255
  };
145
256
 
146
257
  const getOptionLabel = (node) => node?.textContent?.trim() ?? '';
258
+ const isThinkingEffortControl = (node) =>
259
+ node instanceof HTMLElement &&
260
+ (node.getAttribute('data-model-picker-thinking-effort-action') === 'true' ||
261
+ Boolean(node.closest('[data-model-picker-thinking-effort-action="true"]')));
147
262
  const optionIsSelected = (node) => {
148
263
  if (!(node instanceof HTMLElement)) {
149
264
  return false;
@@ -160,7 +275,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
160
275
  if (dataSelected === 'true' || selectedStates.includes(dataState)) {
161
276
  return true;
162
277
  }
163
- if (node.querySelector('[data-testid*="check"], [role="img"][data-icon="check"], svg[data-icon="check"]')) {
278
+ if (node.querySelector('[data-testid*="check"], [role="img"][data-icon="check"], svg[data-icon="check"], .trailing svg')) {
164
279
  return true;
165
280
  }
166
281
  return false;
@@ -182,6 +297,12 @@ function buildModelSelectionExpression(targetModel, strategy) {
182
297
  normalizedTestId.includes('gpt-5-2') ||
183
298
  normalizedTestId.includes('gpt-5.2') ||
184
299
  normalizedTestId.includes('gpt52');
300
+ const has55 =
301
+ normalizedTestId.includes('5-5') ||
302
+ normalizedTestId.includes('5.5') ||
303
+ normalizedTestId.includes('gpt-5-5') ||
304
+ normalizedTestId.includes('gpt-5.5') ||
305
+ normalizedTestId.includes('gpt55');
185
306
  const has54 =
186
307
  normalizedTestId.includes('5-4') ||
187
308
  normalizedTestId.includes('5.4') ||
@@ -200,7 +321,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
200
321
  normalizedTestId.includes('gpt-5-0') ||
201
322
  normalizedTestId.includes('gpt-5.0') ||
202
323
  normalizedTestId.includes('gpt50');
203
- const candidateVersion = has54 ? '5-4' : has52 ? '5-2' : has51 ? '5-1' : has50 ? '5-0' : null;
324
+ const candidateVersion = has55 ? '5-5' : has54 ? '5-4' : has52 ? '5-2' : has51 ? '5-1' : has50 ? '5-0' : null;
204
325
  // If a candidate advertises a different version, ignore it entirely.
205
326
  if (candidateVersion && candidateVersion !== desiredVersion) {
206
327
  return 0;
@@ -227,6 +348,33 @@ function buildModelSelectionExpression(targetModel, strategy) {
227
348
  }
228
349
  }
229
350
  }
351
+ const candidateGpt55VisibleAlias = isTargetGpt55VisibleAlias(normalizedText);
352
+ const candidateHasThinking =
353
+ normalizedText.includes('thinking') || normalizedTestId.includes('thinking');
354
+ const candidateHasPro =
355
+ candidateGpt55VisibleAlias ||
356
+ normalizedText === 'pro' ||
357
+ normalizedText.startsWith('pro ') ||
358
+ normalizedText.includes(' pro ') ||
359
+ normalizedText.endsWith(' pro') ||
360
+ normalizedText.includes('proresearch') ||
361
+ normalizedTestId.includes('pro');
362
+ if (wantsPro && candidateHasThinking) return 0;
363
+ if (wantsPro && !candidateHasPro) return 0;
364
+ if (wantsThinking && candidateHasPro) return 0;
365
+ if (desiredVersion === '5-5' && normalizedText && !candidateGpt55VisibleAlias) {
366
+ const candidateHasVersion =
367
+ normalizedText.includes('5 5') ||
368
+ normalizedText.includes('gpt55') ||
369
+ normalizedText.includes('gpt 5 5');
370
+ const versionLikeLabel = /(?:^|\\s)5\\s+[0-9](?:\\s|$)/.test(normalizedText) || normalizedText.includes('gpt');
371
+ if (versionLikeLabel && !candidateHasVersion) {
372
+ return 0;
373
+ }
374
+ }
375
+ if (candidateGpt55VisibleAlias) {
376
+ score += 900;
377
+ }
230
378
  if (normalizedText && normalizedTarget) {
231
379
  if (normalizedText === normalizedTarget) {
232
380
  score += 500;
@@ -286,6 +434,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
286
434
  for (const menu of menus) {
287
435
  const buttons = Array.from(menu.querySelectorAll(${menuItemLiteral}));
288
436
  for (const option of buttons) {
437
+ if (isThinkingEffortControl(option)) {
438
+ continue;
439
+ }
289
440
  const text = option.textContent ?? '';
290
441
  const normalizedText = normalizeText(text);
291
442
  const testid = option.getAttribute('data-testid') ?? '';
@@ -301,6 +452,25 @@ function buildModelSelectionExpression(targetModel, strategy) {
301
452
  }
302
453
  return bestMatch;
303
454
  };
455
+ const waitForTargetSelection = (previousButtonLabel, previousComposerSignal) => new Promise((resolve) => {
456
+ const waitStart = performance.now();
457
+ const check = () => {
458
+ if (activeSelectionMatchesTarget()) {
459
+ resolve('target');
460
+ return;
461
+ }
462
+ if (selectionStateChanged(previousButtonLabel, previousComposerSignal)) {
463
+ resolve('changed');
464
+ return;
465
+ }
466
+ if (performance.now() - waitStart > SETTLE_WAIT_MS) {
467
+ resolve('timeout');
468
+ return;
469
+ }
470
+ setTimeout(check, 100);
471
+ };
472
+ check();
473
+ });
304
474
 
305
475
  return new Promise((resolve) => {
306
476
  const start = performance.now();
@@ -345,11 +515,13 @@ function buildModelSelectionExpression(targetModel, strategy) {
345
515
  ensureMenuOpen();
346
516
  const match = findBestOption();
347
517
  if (match) {
348
- if (optionIsSelected(match.node)) {
518
+ if (activeSelectionMatchesTarget()) {
349
519
  closeMenu();
350
- resolve({ status: 'already-selected', label: getButtonLabel() || match.label });
520
+ resolve({ status: 'already-selected', label: getResolvedLabel(match.label) });
351
521
  return;
352
522
  }
523
+ const previousButtonLabel = normalizeText(getButtonLabel());
524
+ const previousComposerSignal = readComposerModelSignal();
353
525
  dispatchClickSequence(match.node);
354
526
  // Submenus (e.g. "Legacy models") need a second pass to pick the actual model option.
355
527
  // Keep scanning once the submenu opens instead of treating the submenu click as a final switch.
@@ -358,15 +530,15 @@ function buildModelSelectionExpression(targetModel, strategy) {
358
530
  setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
359
531
  return;
360
532
  }
361
- // Wait for the top bar label to reflect the requested model; otherwise keep scanning.
362
- setTimeout(() => {
363
- if (buttonMatchesTarget()) {
533
+ // Wait for the selected model signal to settle before reopening the picker.
534
+ waitForTargetSelection(previousButtonLabel, previousComposerSignal).then((selectionSettled) => {
535
+ if (selectionSettled === 'target') {
364
536
  closeMenu();
365
- resolve({ status: 'switched', label: getButtonLabel() || match.label });
537
+ resolve({ status: 'switched', label: getResolvedLabel(match.label) });
366
538
  return;
367
539
  }
368
540
  attempt();
369
- }, Math.max(120, INITIAL_WAIT_MS));
541
+ });
370
542
  return;
371
543
  }
372
544
  if (performance.now() - start > MAX_WAIT_MS) {
@@ -385,6 +557,27 @@ function buildModelSelectionExpression(targetModel, strategy) {
385
557
  export function buildModelMatchersLiteralForTest(targetModel) {
386
558
  return buildModelMatchersLiteral(targetModel);
387
559
  }
560
+ function buildComposerSignalMatchers(targetModel) {
561
+ const normalized = targetModel
562
+ .trim()
563
+ .toLowerCase()
564
+ .replace(/[^a-z0-9]+/g, " ")
565
+ .replace(/\s+/g, " ")
566
+ .trim();
567
+ if (normalized.includes("pro")) {
568
+ return { includesAny: ["pro"], excludesAny: ["thinking"], allowBlank: false };
569
+ }
570
+ if (normalized.includes("thinking")) {
571
+ return { includesAny: ["thinking"], excludesAny: ["pro"], allowBlank: false };
572
+ }
573
+ if (normalized.includes("instant")) {
574
+ return { includesAny: [], excludesAny: ["thinking", "pro"], allowBlank: true };
575
+ }
576
+ return { includesAny: [], excludesAny: ["thinking", "pro"], allowBlank: true };
577
+ }
578
+ export function buildComposerSignalMatchersForTest(targetModel) {
579
+ return buildComposerSignalMatchers(targetModel);
580
+ }
388
581
  function buildModelMatchersLiteral(targetModel) {
389
582
  const base = targetModel.trim().toLowerCase();
390
583
  const labelTokens = new Set();
@@ -396,115 +589,145 @@ function buildModelMatchersLiteral(targetModel) {
396
589
  }
397
590
  };
398
591
  push(base, labelTokens);
399
- push(base.replace(/\s+/g, ' '), labelTokens);
400
- const collapsed = base.replace(/\s+/g, '');
592
+ push(base.replace(/\s+/g, " "), labelTokens);
593
+ const collapsed = base.replace(/\s+/g, "");
401
594
  push(collapsed, labelTokens);
402
- const dotless = base.replace(/[.]/g, '');
595
+ const dotless = base.replace(/[.]/g, "");
403
596
  push(dotless, labelTokens);
404
597
  push(`chatgpt ${base}`, labelTokens);
405
598
  push(`chatgpt ${dotless}`, labelTokens);
406
599
  push(`gpt ${base}`, labelTokens);
407
600
  push(`gpt ${dotless}`, labelTokens);
601
+ // Numeric variations (5.5 <-> 55 <-> gpt-5-5)
602
+ if (base.includes("5.5") || base.includes("5-5") || base.includes("55")) {
603
+ push("5.5", labelTokens);
604
+ push("gpt-5.5", labelTokens);
605
+ push("gpt5.5", labelTokens);
606
+ push("gpt-5-5", labelTokens);
607
+ push("gpt5-5", labelTokens);
608
+ push("gpt55", labelTokens);
609
+ push("chatgpt 5.5", labelTokens);
610
+ if (base.includes("thinking")) {
611
+ push("thinking heavy", labelTokens);
612
+ push("heavy thinking", labelTokens);
613
+ testIdTokens.add("model-switcher-gpt-5-5-thinking");
614
+ testIdTokens.add("gpt-5-5-thinking");
615
+ testIdTokens.add("gpt-5.5-thinking");
616
+ }
617
+ if (!base.includes("pro") && !base.includes("thinking")) {
618
+ testIdTokens.add("model-switcher-gpt-5-5");
619
+ }
620
+ testIdTokens.add("gpt-5-5");
621
+ testIdTokens.add("gpt5-5");
622
+ testIdTokens.add("gpt55");
623
+ }
408
624
  // Numeric variations (5.4 ↔ 54 ↔ gpt-5-4)
409
- if (base.includes('5.4') || base.includes('5-4') || base.includes('54')) {
410
- push('5.4', labelTokens);
411
- push('gpt-5.4', labelTokens);
412
- push('gpt5.4', labelTokens);
413
- push('gpt-5-4', labelTokens);
414
- push('gpt5-4', labelTokens);
415
- push('gpt54', labelTokens);
416
- push('chatgpt 5.4', labelTokens);
417
- if (!base.includes('pro')) {
418
- testIdTokens.add('model-switcher-gpt-5-4');
419
- }
420
- testIdTokens.add('gpt-5-4');
421
- testIdTokens.add('gpt5-4');
422
- testIdTokens.add('gpt54');
625
+ if (base.includes("5.4") || base.includes("5-4") || base.includes("54")) {
626
+ push("5.4", labelTokens);
627
+ push("gpt-5.4", labelTokens);
628
+ push("gpt5.4", labelTokens);
629
+ push("gpt-5-4", labelTokens);
630
+ push("gpt5-4", labelTokens);
631
+ push("gpt54", labelTokens);
632
+ push("chatgpt 5.4", labelTokens);
633
+ if (!base.includes("pro")) {
634
+ testIdTokens.add("model-switcher-gpt-5-4");
635
+ }
636
+ testIdTokens.add("gpt-5-4");
637
+ testIdTokens.add("gpt5-4");
638
+ testIdTokens.add("gpt54");
423
639
  }
424
640
  // Numeric variations (5.1 ↔ 51 ↔ gpt-5-1)
425
- if (base.includes('5.1') || base.includes('5-1') || base.includes('51')) {
426
- push('5.1', labelTokens);
427
- push('gpt-5.1', labelTokens);
428
- push('gpt5.1', labelTokens);
429
- push('gpt-5-1', labelTokens);
430
- push('gpt5-1', labelTokens);
431
- push('gpt51', labelTokens);
432
- push('chatgpt 5.1', labelTokens);
433
- testIdTokens.add('gpt-5-1');
434
- testIdTokens.add('gpt5-1');
435
- testIdTokens.add('gpt51');
641
+ if (base.includes("5.1") || base.includes("5-1") || base.includes("51")) {
642
+ push("5.1", labelTokens);
643
+ push("gpt-5.1", labelTokens);
644
+ push("gpt5.1", labelTokens);
645
+ push("gpt-5-1", labelTokens);
646
+ push("gpt5-1", labelTokens);
647
+ push("gpt51", labelTokens);
648
+ push("chatgpt 5.1", labelTokens);
649
+ testIdTokens.add("gpt-5-1");
650
+ testIdTokens.add("gpt5-1");
651
+ testIdTokens.add("gpt51");
436
652
  }
437
653
  // Numeric variations (5.0 ↔ 50 ↔ gpt-5-0)
438
- if (base.includes('5.0') || base.includes('5-0') || base.includes('50')) {
439
- push('5.0', labelTokens);
440
- push('gpt-5.0', labelTokens);
441
- push('gpt5.0', labelTokens);
442
- push('gpt-5-0', labelTokens);
443
- push('gpt5-0', labelTokens);
444
- push('gpt50', labelTokens);
445
- push('chatgpt 5.0', labelTokens);
446
- testIdTokens.add('gpt-5-0');
447
- testIdTokens.add('gpt5-0');
448
- testIdTokens.add('gpt50');
654
+ if (base.includes("5.0") || base.includes("5-0") || base.includes("50")) {
655
+ push("5.0", labelTokens);
656
+ push("gpt-5.0", labelTokens);
657
+ push("gpt5.0", labelTokens);
658
+ push("gpt-5-0", labelTokens);
659
+ push("gpt5-0", labelTokens);
660
+ push("gpt50", labelTokens);
661
+ push("chatgpt 5.0", labelTokens);
662
+ testIdTokens.add("gpt-5-0");
663
+ testIdTokens.add("gpt5-0");
664
+ testIdTokens.add("gpt50");
449
665
  }
450
666
  // Numeric variations (5.2 ↔ 52 ↔ gpt-5-2)
451
- if (base.includes('5.2') || base.includes('5-2') || base.includes('52')) {
452
- push('5.2', labelTokens);
453
- push('gpt-5.2', labelTokens);
454
- push('gpt5.2', labelTokens);
455
- push('gpt-5-2', labelTokens);
456
- push('gpt5-2', labelTokens);
457
- push('gpt52', labelTokens);
458
- push('chatgpt 5.2', labelTokens);
667
+ if (base.includes("5.2") || base.includes("5-2") || base.includes("52")) {
668
+ push("5.2", labelTokens);
669
+ push("gpt-5.2", labelTokens);
670
+ push("gpt5.2", labelTokens);
671
+ push("gpt-5-2", labelTokens);
672
+ push("gpt5-2", labelTokens);
673
+ push("gpt52", labelTokens);
674
+ push("chatgpt 5.2", labelTokens);
459
675
  // Thinking variant: explicit testid for "Thinking" picker option
460
- if (base.includes('thinking')) {
461
- push('thinking', labelTokens);
462
- testIdTokens.add('model-switcher-gpt-5-2-thinking');
463
- testIdTokens.add('gpt-5-2-thinking');
464
- testIdTokens.add('gpt-5.2-thinking');
676
+ if (base.includes("thinking")) {
677
+ push("thinking", labelTokens);
678
+ testIdTokens.add("model-switcher-gpt-5-2-thinking");
679
+ testIdTokens.add("gpt-5-2-thinking");
680
+ testIdTokens.add("gpt-5.2-thinking");
465
681
  }
466
682
  // Instant variant: explicit testid for "Instant" picker option
467
- if (base.includes('instant')) {
468
- push('instant', labelTokens);
469
- testIdTokens.add('model-switcher-gpt-5-2-instant');
470
- testIdTokens.add('gpt-5-2-instant');
471
- testIdTokens.add('gpt-5.2-instant');
683
+ if (base.includes("instant")) {
684
+ push("instant", labelTokens);
685
+ testIdTokens.add("model-switcher-gpt-5-2-instant");
686
+ testIdTokens.add("gpt-5-2-instant");
687
+ testIdTokens.add("gpt-5.2-instant");
472
688
  }
473
689
  // Base 5.2 testids (for "Auto" mode when no suffix specified)
474
- if (!base.includes('thinking') && !base.includes('instant') && !base.includes('pro')) {
475
- testIdTokens.add('model-switcher-gpt-5-2');
690
+ if (!base.includes("thinking") && !base.includes("instant") && !base.includes("pro")) {
691
+ testIdTokens.add("model-switcher-gpt-5-2");
476
692
  }
477
- testIdTokens.add('gpt-5-2');
478
- testIdTokens.add('gpt5-2');
479
- testIdTokens.add('gpt52');
693
+ testIdTokens.add("gpt-5-2");
694
+ testIdTokens.add("gpt5-2");
695
+ testIdTokens.add("gpt52");
480
696
  }
481
697
  // Pro / research variants
482
- if (base.includes('pro')) {
483
- push('proresearch', labelTokens);
484
- push('research grade', labelTokens);
485
- push('advanced reasoning', labelTokens);
486
- if (base.includes('5.4') || base.includes('5-4') || base.includes('54')) {
487
- testIdTokens.add('gpt-5.4-pro');
488
- testIdTokens.add('gpt-5-4-pro');
489
- testIdTokens.add('gpt54pro');
490
- }
491
- if (base.includes('5.1') || base.includes('5-1') || base.includes('51')) {
492
- testIdTokens.add('gpt-5.1-pro');
493
- testIdTokens.add('gpt-5-1-pro');
494
- testIdTokens.add('gpt51pro');
495
- }
496
- if (base.includes('5.0') || base.includes('5-0') || base.includes('50')) {
497
- testIdTokens.add('gpt-5.0-pro');
498
- testIdTokens.add('gpt-5-0-pro');
499
- testIdTokens.add('gpt50pro');
500
- }
501
- if (base.includes('5.2') || base.includes('5-2') || base.includes('52')) {
502
- testIdTokens.add('gpt-5.2-pro');
503
- testIdTokens.add('gpt-5-2-pro');
504
- testIdTokens.add('gpt52pro');
505
- }
506
- testIdTokens.add('pro');
507
- testIdTokens.add('proresearch');
698
+ if (base.includes("pro")) {
699
+ push("proresearch", labelTokens);
700
+ push("research grade", labelTokens);
701
+ push("advanced reasoning", labelTokens);
702
+ if (base.includes("5.5") || base.includes("5-5") || base.includes("55")) {
703
+ push("pro extended", labelTokens);
704
+ push("extended pro", labelTokens);
705
+ testIdTokens.add("gpt-5.5-pro");
706
+ testIdTokens.add("gpt-5-5-pro");
707
+ testIdTokens.add("gpt55pro");
708
+ }
709
+ if (base.includes("5.4") || base.includes("5-4") || base.includes("54")) {
710
+ testIdTokens.add("gpt-5.4-pro");
711
+ testIdTokens.add("gpt-5-4-pro");
712
+ testIdTokens.add("gpt54pro");
713
+ }
714
+ if (base.includes("5.1") || base.includes("5-1") || base.includes("51")) {
715
+ testIdTokens.add("gpt-5.1-pro");
716
+ testIdTokens.add("gpt-5-1-pro");
717
+ testIdTokens.add("gpt51pro");
718
+ }
719
+ if (base.includes("5.0") || base.includes("5-0") || base.includes("50")) {
720
+ testIdTokens.add("gpt-5.0-pro");
721
+ testIdTokens.add("gpt-5-0-pro");
722
+ testIdTokens.add("gpt50pro");
723
+ }
724
+ if (base.includes("5.2") || base.includes("5-2") || base.includes("52")) {
725
+ testIdTokens.add("gpt-5.2-pro");
726
+ testIdTokens.add("gpt-5-2-pro");
727
+ testIdTokens.add("gpt52pro");
728
+ }
729
+ testIdTokens.add("pro");
730
+ testIdTokens.add("proresearch");
508
731
  }
509
732
  base
510
733
  .split(/\s+/)
@@ -513,7 +736,7 @@ function buildModelMatchersLiteral(targetModel) {
513
736
  .forEach((token) => {
514
737
  push(token, labelTokens);
515
738
  });
516
- const hyphenated = base.replace(/\s+/g, '-');
739
+ const hyphenated = base.replace(/\s+/g, "-");
517
740
  push(hyphenated, testIdTokens);
518
741
  push(collapsed, testIdTokens);
519
742
  push(dotless, testIdTokens);
@@ -525,13 +748,13 @@ function buildModelMatchersLiteral(targetModel) {
525
748
  labelTokens.add(base);
526
749
  }
527
750
  if (!testIdTokens.size) {
528
- testIdTokens.add(base.replace(/\s+/g, '-'));
751
+ testIdTokens.add(base.replace(/\s+/g, "-"));
529
752
  }
530
753
  return {
531
754
  labelTokens: Array.from(labelTokens).filter(Boolean),
532
755
  testIdTokens: Array.from(testIdTokens).filter(Boolean),
533
756
  };
534
757
  }
535
- export function buildModelSelectionExpressionForTest(targetModel) {
536
- return buildModelSelectionExpression(targetModel, 'select');
758
+ export function buildModelSelectionExpressionForTest(targetModel, strategy = "select") {
759
+ return buildModelSelectionExpression(targetModel, strategy);
537
760
  }