@steipete/oracle 0.8.6 → 0.10.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 (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +130 -45
  3. package/dist/bin/oracle-cli.js +613 -379
  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/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +314 -104
  22. package/dist/src/browser/actions/navigation.js +161 -136
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +452 -303
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +17 -0
  40. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
  42. package/dist/src/browser/providers/index.js +2 -0
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +65 -45
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +7 -4
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +11 -0
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +12 -8
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +145 -87
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +37 -25
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +182 -79
  77. package/dist/src/cli/sessionLineage.js +60 -0
  78. package/dist/src/cli/sessionRunner.js +118 -90
  79. package/dist/src/cli/sessionTable.js +28 -24
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +140 -127
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +80 -0
  86. package/dist/src/gemini-web/client.js +81 -64
  87. package/dist/src/gemini-web/executionMode.js +16 -0
  88. package/dist/src/gemini-web/executor.js +327 -169
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +81 -64
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +84 -46
  99. package/dist/src/oracle/config.js +124 -58
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +69 -45
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -30
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +23 -15
  112. package/dist/src/oracle/run.js +172 -140
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +81 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  127. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  128. package/dist/vendor/oracle-notifier/README.md +2 -0
  129. package/package.json +69 -65
  130. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  131. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  132. package/vendor/oracle-notifier/README.md +2 -0
  133. package/dist/markdansi/types/index.js +0 -4
  134. package/dist/oracle/bin/oracle-cli.js +0 -472
  135. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  136. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  137. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  138. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  139. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  140. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  141. package/dist/oracle/src/browser/config.js +0 -33
  142. package/dist/oracle/src/browser/constants.js +0 -40
  143. package/dist/oracle/src/browser/cookies.js +0 -210
  144. package/dist/oracle/src/browser/domDebug.js +0 -36
  145. package/dist/oracle/src/browser/index.js +0 -331
  146. package/dist/oracle/src/browser/pageActions.js +0 -5
  147. package/dist/oracle/src/browser/prompt.js +0 -88
  148. package/dist/oracle/src/browser/promptSummary.js +0 -20
  149. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  150. package/dist/oracle/src/browser/utils.js +0 -62
  151. package/dist/oracle/src/browserMode.js +0 -1
  152. package/dist/oracle/src/cli/browserConfig.js +0 -44
  153. package/dist/oracle/src/cli/dryRun.js +0 -59
  154. package/dist/oracle/src/cli/engine.js +0 -17
  155. package/dist/oracle/src/cli/errorUtils.js +0 -9
  156. package/dist/oracle/src/cli/help.js +0 -70
  157. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  158. package/dist/oracle/src/cli/options.js +0 -103
  159. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  160. package/dist/oracle/src/cli/rootAlias.js +0 -30
  161. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  162. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  163. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  164. package/dist/oracle/src/heartbeat.js +0 -43
  165. package/dist/oracle/src/oracle/client.js +0 -48
  166. package/dist/oracle/src/oracle/config.js +0 -29
  167. package/dist/oracle/src/oracle/errors.js +0 -101
  168. package/dist/oracle/src/oracle/files.js +0 -220
  169. package/dist/oracle/src/oracle/format.js +0 -33
  170. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  171. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  172. package/dist/oracle/src/oracle/request.js +0 -48
  173. package/dist/oracle/src/oracle/run.js +0 -444
  174. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  175. package/dist/oracle/src/oracle/types.js +0 -1
  176. package/dist/oracle/src/oracle.js +0 -9
  177. package/dist/oracle/src/sessionManager.js +0 -205
  178. package/dist/oracle/src/version.js +0 -39
  179. package/dist/scripts/chrome/browser-tools.js +0 -295
  180. package/dist/src/browser/profileSync.js +0 -141
  181. /package/dist/{oracle/src/browser/types.js → src/gemini-web/executionClients.js} +0 -0
@@ -1,5 +1,5 @@
1
- const CLICK_TYPES = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'];
2
- export function buildClickDispatcher(functionName = 'dispatchClickSequence') {
1
+ const CLICK_TYPES = ["pointerdown", "mousedown", "pointerup", "mouseup", "click"];
2
+ export function buildClickDispatcher(functionName = "dispatchClickSequence") {
3
3
  const typesLiteral = JSON.stringify(CLICK_TYPES);
4
4
  return `function ${functionName}(target){
5
5
  if(!target || !(target instanceof EventTarget)) return false;
@@ -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,26 +9,26 @@ 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
16
  logger(`Model picker: ${label}`);
17
17
  return;
18
18
  }
19
- case 'option-not-found': {
20
- await logDomFailure(Runtime, logger, 'model-switcher-option');
19
+ case "option-not-found": {
20
+ await logDomFailure(Runtime, logger, "model-switcher-option");
21
21
  const isTemporary = result.hint?.temporaryChat ?? false;
22
22
  const available = (result.hint?.availableOptions ?? []).filter(Boolean);
23
- const availableHint = available.length > 0 ? ` Available: ${available.join(', ')}.` : '';
23
+ const availableHint = available.length > 0 ? ` Available: ${available.join(", ")}.` : "";
24
24
  const tempHint = isTemporary && /\bpro\b/i.test(desiredModel)
25
25
  ? ' 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
- : '';
26
+ : "";
27
27
  throw new Error(`Unable to find model option matching "${desiredModel}" in the model switcher.${availableHint}${tempHint}`);
28
28
  }
29
29
  default: {
30
- await logDomFailure(Runtime, logger, 'model-switcher-button');
31
- throw new Error('Unable to locate the ChatGPT model selector button.');
30
+ await logDomFailure(Runtime, logger, "model-switcher-button");
31
+ throw new Error("Unable to locate the ChatGPT model selector button.");
32
32
  }
33
33
  }
34
34
  }
@@ -38,23 +38,33 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
38
38
  */
39
39
  function buildModelSelectionExpression(targetModel, strategy) {
40
40
  const matchers = buildModelMatchersLiteral(targetModel);
41
+ const composerSignalMatchers = buildComposerSignalMatchers(targetModel);
41
42
  const labelLiteral = JSON.stringify(matchers.labelTokens);
42
43
  const idLiteral = JSON.stringify(matchers.testIdTokens);
43
44
  const primaryLabelLiteral = JSON.stringify(targetModel);
44
45
  const strategyLiteral = JSON.stringify(strategy);
46
+ const composerSignalSelectorLiteral = JSON.stringify(COMPOSER_MODEL_SIGNAL_SELECTOR);
47
+ const composerIncludesLiteral = JSON.stringify(composerSignalMatchers.includesAny);
48
+ const composerExcludesLiteral = JSON.stringify(composerSignalMatchers.excludesAny);
49
+ const composerAllowBlankLiteral = JSON.stringify(composerSignalMatchers.allowBlank);
45
50
  const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
46
51
  const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
47
52
  return `(() => {
48
53
  ${buildClickDispatcher()}
49
54
  // Capture the selectors and matcher literals up front so the browser expression stays pure.
50
55
  const BUTTON_SELECTOR = '${MODEL_BUTTON_SELECTOR}';
56
+ const COMPOSER_MODEL_SIGNAL_SELECTOR = ${composerSignalSelectorLiteral};
51
57
  const LABEL_TOKENS = ${labelLiteral};
52
58
  const TEST_IDS = ${idLiteral};
53
59
  const PRIMARY_LABEL = ${primaryLabelLiteral};
54
60
  const MODEL_STRATEGY = ${strategyLiteral};
61
+ const COMPOSER_SIGNAL_INCLUDES = ${composerIncludesLiteral};
62
+ const COMPOSER_SIGNAL_EXCLUDES = ${composerExcludesLiteral};
63
+ const COMPOSER_SIGNAL_ALLOW_BLANK = ${composerAllowBlankLiteral};
55
64
  const INITIAL_WAIT_MS = 150;
56
65
  const REOPEN_INTERVAL_MS = 400;
57
66
  const MAX_WAIT_MS = 20000;
67
+ const SETTLE_WAIT_MS = 1500;
58
68
  const normalizeText = (value) => {
59
69
  if (!value) {
60
70
  return '';
@@ -71,30 +81,72 @@ function buildModelSelectionExpression(targetModel, strategy) {
71
81
  .map((token) => normalizeText(token))
72
82
  .filter(Boolean);
73
83
  const targetWords = normalizedTarget.split(' ').filter(Boolean);
74
- const desiredVersion = normalizedTarget.includes('5 2')
75
- ? '5-2'
76
- : normalizedTarget.includes('5 1')
77
- ? '5-1'
78
- : normalizedTarget.includes('5 0')
79
- ? '5-0'
80
- : null;
84
+ const desiredVersion = normalizedTarget.includes('5 4')
85
+ ? '5-4'
86
+ : normalizedTarget.includes('5 5')
87
+ ? '5-5'
88
+ : normalizedTarget.includes('5 2')
89
+ ? '5-2'
90
+ : normalizedTarget.includes('5 1')
91
+ ? '5-1'
92
+ : normalizedTarget.includes('5 0')
93
+ ? '5-0'
94
+ : null;
81
95
  const wantsPro = normalizedTarget.includes(' pro') || normalizedTarget.endsWith(' pro') || normalizedTokens.includes('pro');
82
96
  const wantsInstant = normalizedTarget.includes('instant');
83
97
  const wantsThinking = normalizedTarget.includes('thinking');
98
+ const isTargetGpt55VisibleAlias = (value) => {
99
+ if (desiredVersion !== '5-5') return false;
100
+ const label = normalizeText(value);
101
+ if (wantsPro) {
102
+ return label.includes('pro') && label.includes('extended') && !label.includes('thinking');
103
+ }
104
+ if (wantsThinking) {
105
+ return label.includes('thinking') && label.includes('heavy') && !label.includes('pro');
106
+ }
107
+ return false;
108
+ };
84
109
 
85
110
  const button = document.querySelector(BUTTON_SELECTOR);
86
111
  if (!button) {
87
112
  return { status: 'button-missing' };
88
113
  }
89
114
 
115
+ const closeMenu = () => {
116
+ try {
117
+ if (dispatchClickSequence(button)) {
118
+ lastPointerClick = performance.now();
119
+ return;
120
+ }
121
+ } catch {}
122
+ try {
123
+ document.dispatchEvent(
124
+ new KeyboardEvent('keydown', {
125
+ key: 'Escape',
126
+ code: 'Escape',
127
+ keyCode: 27,
128
+ which: 27,
129
+ bubbles: true,
130
+ }),
131
+ );
132
+ } catch {}
133
+ };
134
+
90
135
  const getButtonLabel = () => (button.textContent ?? '').trim();
136
+ const getComposerModelLabel = () =>
137
+ (document.querySelector(COMPOSER_MODEL_SIGNAL_SELECTOR)?.textContent ?? '').trim();
138
+ const readComposerModelSignal = () => normalizeText(getComposerModelLabel());
139
+ const getResolvedLabel = (fallback) => getComposerModelLabel() || getButtonLabel() || fallback;
91
140
  if (MODEL_STRATEGY === 'current') {
92
- return { status: 'already-selected', label: getButtonLabel() };
141
+ return { status: 'already-selected', label: getResolvedLabel(PRIMARY_LABEL) };
93
142
  }
94
143
  const buttonMatchesTarget = () => {
95
144
  const normalizedLabel = normalizeText(getButtonLabel());
96
145
  if (!normalizedLabel) return false;
146
+ if (isTargetGpt55VisibleAlias(normalizedLabel)) return true;
97
147
  if (desiredVersion) {
148
+ if (desiredVersion === '5-5' && !normalizedLabel.includes('5 5')) return false;
149
+ if (desiredVersion === '5-4' && !normalizedLabel.includes('5 4')) return false;
98
150
  if (desiredVersion === '5-2' && !normalizedLabel.includes('5 2')) return false;
99
151
  if (desiredVersion === '5-1' && !normalizedLabel.includes('5 1')) return false;
100
152
  if (desiredVersion === '5-0' && !normalizedLabel.includes('5 0')) return false;
@@ -108,9 +160,47 @@ function buildModelSelectionExpression(targetModel, strategy) {
108
160
  if (!wantsThinking && normalizedLabel.includes('thinking')) return false;
109
161
  return true;
110
162
  };
163
+ const buttonHasGenericLabel = () => {
164
+ const normalizedLabel = normalizeText(getButtonLabel());
165
+ return !normalizedLabel || normalizedLabel === 'chatgpt';
166
+ };
167
+ const composerSignalMatchesTarget = () => {
168
+ const signal = readComposerModelSignal();
169
+ if (!signal) {
170
+ return COMPOSER_SIGNAL_ALLOW_BLANK;
171
+ }
172
+ if (COMPOSER_SIGNAL_EXCLUDES.some((token) => token && signal.includes(token))) {
173
+ return false;
174
+ }
175
+ if (COMPOSER_SIGNAL_INCLUDES.length === 0) {
176
+ return true;
177
+ }
178
+ return COMPOSER_SIGNAL_INCLUDES.some((token) => token && signal.includes(token));
179
+ };
180
+ const activeSelectionMatchesTarget = () => {
181
+ if (buttonMatchesTarget()) {
182
+ return true;
183
+ }
184
+ if (!buttonHasGenericLabel()) {
185
+ return false;
186
+ }
187
+ return composerSignalMatchesTarget();
188
+ };
189
+ const selectionStateChanged = (previousButtonLabel, previousComposerSignal) => {
190
+ const currentButtonLabel = normalizeText(getButtonLabel());
191
+ const currentComposerSignal = readComposerModelSignal();
192
+ if (
193
+ currentButtonLabel &&
194
+ currentButtonLabel !== previousButtonLabel &&
195
+ !buttonHasGenericLabel()
196
+ ) {
197
+ return true;
198
+ }
199
+ return currentComposerSignal !== previousComposerSignal;
200
+ };
111
201
 
112
- if (buttonMatchesTarget()) {
113
- return { status: 'already-selected', label: getButtonLabel() };
202
+ if (activeSelectionMatchesTarget()) {
203
+ return { status: 'already-selected', label: getResolvedLabel(PRIMARY_LABEL) };
114
204
  }
115
205
 
116
206
  let lastPointerClick = 0;
@@ -137,7 +227,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
137
227
  if (dataSelected === 'true' || selectedStates.includes(dataState)) {
138
228
  return true;
139
229
  }
140
- if (node.querySelector('[data-testid*="check"], [role="img"][data-icon="check"], svg[data-icon="check"]')) {
230
+ if (node.querySelector('[data-testid*="check"], [role="img"][data-icon="check"], svg[data-icon="check"], .trailing svg')) {
141
231
  return true;
142
232
  }
143
233
  return false;
@@ -159,6 +249,18 @@ function buildModelSelectionExpression(targetModel, strategy) {
159
249
  normalizedTestId.includes('gpt-5-2') ||
160
250
  normalizedTestId.includes('gpt-5.2') ||
161
251
  normalizedTestId.includes('gpt52');
252
+ const has55 =
253
+ normalizedTestId.includes('5-5') ||
254
+ normalizedTestId.includes('5.5') ||
255
+ normalizedTestId.includes('gpt-5-5') ||
256
+ normalizedTestId.includes('gpt-5.5') ||
257
+ normalizedTestId.includes('gpt55');
258
+ const has54 =
259
+ normalizedTestId.includes('5-4') ||
260
+ normalizedTestId.includes('5.4') ||
261
+ normalizedTestId.includes('gpt-5-4') ||
262
+ normalizedTestId.includes('gpt-5.4') ||
263
+ normalizedTestId.includes('gpt54');
162
264
  const has51 =
163
265
  normalizedTestId.includes('5-1') ||
164
266
  normalizedTestId.includes('5.1') ||
@@ -171,7 +273,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
171
273
  normalizedTestId.includes('gpt-5-0') ||
172
274
  normalizedTestId.includes('gpt-5.0') ||
173
275
  normalizedTestId.includes('gpt50');
174
- const candidateVersion = has52 ? '5-2' : has51 ? '5-1' : has50 ? '5-0' : null;
276
+ const candidateVersion = has55 ? '5-5' : has54 ? '5-4' : has52 ? '5-2' : has51 ? '5-1' : has50 ? '5-0' : null;
175
277
  // If a candidate advertises a different version, ignore it entirely.
176
278
  if (candidateVersion && candidateVersion !== desiredVersion) {
177
279
  return 0;
@@ -198,6 +300,20 @@ function buildModelSelectionExpression(targetModel, strategy) {
198
300
  }
199
301
  }
200
302
  }
303
+ const candidateGpt55VisibleAlias = isTargetGpt55VisibleAlias(normalizedText);
304
+ if (desiredVersion === '5-5' && normalizedText && !candidateGpt55VisibleAlias) {
305
+ const candidateHasVersion =
306
+ normalizedText.includes('5 5') ||
307
+ normalizedText.includes('gpt55') ||
308
+ normalizedText.includes('gpt 5 5');
309
+ const versionLikeLabel = /(?:^|\\s)5\\s+[0-9](?:\\s|$)/.test(normalizedText) || normalizedText.includes('gpt');
310
+ if (versionLikeLabel && !candidateHasVersion) {
311
+ return 0;
312
+ }
313
+ }
314
+ if (candidateGpt55VisibleAlias) {
315
+ score += 900;
316
+ }
201
317
  if (normalizedText && normalizedTarget) {
202
318
  if (normalizedText === normalizedTarget) {
203
319
  score += 500;
@@ -272,6 +388,24 @@ function buildModelSelectionExpression(targetModel, strategy) {
272
388
  }
273
389
  return bestMatch;
274
390
  };
391
+ const waitForTargetSelection = (previousButtonLabel, previousComposerSignal) => new Promise((resolve) => {
392
+ const waitStart = performance.now();
393
+ const check = () => {
394
+ if (
395
+ activeSelectionMatchesTarget() ||
396
+ selectionStateChanged(previousButtonLabel, previousComposerSignal)
397
+ ) {
398
+ resolve(true);
399
+ return;
400
+ }
401
+ if (performance.now() - waitStart > SETTLE_WAIT_MS) {
402
+ resolve(false);
403
+ return;
404
+ }
405
+ setTimeout(check, 100);
406
+ };
407
+ check();
408
+ });
275
409
 
276
410
  return new Promise((resolve) => {
277
411
  const start = performance.now();
@@ -316,10 +450,13 @@ function buildModelSelectionExpression(targetModel, strategy) {
316
450
  ensureMenuOpen();
317
451
  const match = findBestOption();
318
452
  if (match) {
319
- if (optionIsSelected(match.node)) {
320
- resolve({ status: 'already-selected', label: getButtonLabel() || match.label });
453
+ if (optionIsSelected(match.node) || activeSelectionMatchesTarget()) {
454
+ closeMenu();
455
+ resolve({ status: 'already-selected', label: getResolvedLabel(match.label) });
321
456
  return;
322
457
  }
458
+ const previousButtonLabel = normalizeText(getButtonLabel());
459
+ const previousComposerSignal = readComposerModelSignal();
323
460
  dispatchClickSequence(match.node);
324
461
  // Submenus (e.g. "Legacy models") need a second pass to pick the actual model option.
325
462
  // Keep scanning once the submenu opens instead of treating the submenu click as a final switch.
@@ -328,14 +465,15 @@ function buildModelSelectionExpression(targetModel, strategy) {
328
465
  setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
329
466
  return;
330
467
  }
331
- // Wait for the top bar label to reflect the requested model; otherwise keep scanning.
332
- setTimeout(() => {
333
- if (buttonMatchesTarget()) {
334
- resolve({ status: 'switched', label: getButtonLabel() || match.label });
468
+ // Wait for the selected model signal to settle before reopening the picker.
469
+ waitForTargetSelection(previousButtonLabel, previousComposerSignal).then((selectionSettled) => {
470
+ if (selectionSettled) {
471
+ closeMenu();
472
+ resolve({ status: 'switched', label: getResolvedLabel(match.label) });
335
473
  return;
336
474
  }
337
475
  attempt();
338
- }, Math.max(120, INITIAL_WAIT_MS));
476
+ });
339
477
  return;
340
478
  }
341
479
  if (performance.now() - start > MAX_WAIT_MS) {
@@ -354,6 +492,27 @@ function buildModelSelectionExpression(targetModel, strategy) {
354
492
  export function buildModelMatchersLiteralForTest(targetModel) {
355
493
  return buildModelMatchersLiteral(targetModel);
356
494
  }
495
+ function buildComposerSignalMatchers(targetModel) {
496
+ const normalized = targetModel
497
+ .trim()
498
+ .toLowerCase()
499
+ .replace(/[^a-z0-9]+/g, " ")
500
+ .replace(/\s+/g, " ")
501
+ .trim();
502
+ if (normalized.includes("pro")) {
503
+ return { includesAny: ["pro"], excludesAny: ["thinking"], allowBlank: false };
504
+ }
505
+ if (normalized.includes("thinking")) {
506
+ return { includesAny: ["thinking"], excludesAny: ["pro"], allowBlank: false };
507
+ }
508
+ if (normalized.includes("instant")) {
509
+ return { includesAny: [], excludesAny: ["thinking", "pro"], allowBlank: true };
510
+ }
511
+ return { includesAny: [], excludesAny: ["thinking", "pro"], allowBlank: true };
512
+ }
513
+ export function buildComposerSignalMatchersForTest(targetModel) {
514
+ return buildComposerSignalMatchers(targetModel);
515
+ }
357
516
  function buildModelMatchersLiteral(targetModel) {
358
517
  const base = targetModel.trim().toLowerCase();
359
518
  const labelTokens = new Set();
@@ -365,94 +524,145 @@ function buildModelMatchersLiteral(targetModel) {
365
524
  }
366
525
  };
367
526
  push(base, labelTokens);
368
- push(base.replace(/\s+/g, ' '), labelTokens);
369
- const collapsed = base.replace(/\s+/g, '');
527
+ push(base.replace(/\s+/g, " "), labelTokens);
528
+ const collapsed = base.replace(/\s+/g, "");
370
529
  push(collapsed, labelTokens);
371
- const dotless = base.replace(/[.]/g, '');
530
+ const dotless = base.replace(/[.]/g, "");
372
531
  push(dotless, labelTokens);
373
532
  push(`chatgpt ${base}`, labelTokens);
374
533
  push(`chatgpt ${dotless}`, labelTokens);
375
534
  push(`gpt ${base}`, labelTokens);
376
535
  push(`gpt ${dotless}`, labelTokens);
536
+ // Numeric variations (5.5 <-> 55 <-> gpt-5-5)
537
+ if (base.includes("5.5") || base.includes("5-5") || base.includes("55")) {
538
+ push("5.5", labelTokens);
539
+ push("gpt-5.5", labelTokens);
540
+ push("gpt5.5", labelTokens);
541
+ push("gpt-5-5", labelTokens);
542
+ push("gpt5-5", labelTokens);
543
+ push("gpt55", labelTokens);
544
+ push("chatgpt 5.5", labelTokens);
545
+ if (base.includes("thinking")) {
546
+ push("thinking heavy", labelTokens);
547
+ push("heavy thinking", labelTokens);
548
+ testIdTokens.add("model-switcher-gpt-5-5-thinking");
549
+ testIdTokens.add("gpt-5-5-thinking");
550
+ testIdTokens.add("gpt-5.5-thinking");
551
+ }
552
+ if (!base.includes("pro") && !base.includes("thinking")) {
553
+ testIdTokens.add("model-switcher-gpt-5-5");
554
+ }
555
+ testIdTokens.add("gpt-5-5");
556
+ testIdTokens.add("gpt5-5");
557
+ testIdTokens.add("gpt55");
558
+ }
559
+ // Numeric variations (5.4 ↔ 54 ↔ gpt-5-4)
560
+ if (base.includes("5.4") || base.includes("5-4") || base.includes("54")) {
561
+ push("5.4", labelTokens);
562
+ push("gpt-5.4", labelTokens);
563
+ push("gpt5.4", labelTokens);
564
+ push("gpt-5-4", labelTokens);
565
+ push("gpt5-4", labelTokens);
566
+ push("gpt54", labelTokens);
567
+ push("chatgpt 5.4", labelTokens);
568
+ if (!base.includes("pro")) {
569
+ testIdTokens.add("model-switcher-gpt-5-4");
570
+ }
571
+ testIdTokens.add("gpt-5-4");
572
+ testIdTokens.add("gpt5-4");
573
+ testIdTokens.add("gpt54");
574
+ }
377
575
  // Numeric variations (5.1 ↔ 51 ↔ gpt-5-1)
378
- if (base.includes('5.1') || base.includes('5-1') || base.includes('51')) {
379
- push('5.1', labelTokens);
380
- push('gpt-5.1', labelTokens);
381
- push('gpt5.1', labelTokens);
382
- push('gpt-5-1', labelTokens);
383
- push('gpt5-1', labelTokens);
384
- push('gpt51', labelTokens);
385
- push('chatgpt 5.1', labelTokens);
386
- testIdTokens.add('gpt-5-1');
387
- testIdTokens.add('gpt5-1');
388
- testIdTokens.add('gpt51');
576
+ if (base.includes("5.1") || base.includes("5-1") || base.includes("51")) {
577
+ push("5.1", labelTokens);
578
+ push("gpt-5.1", labelTokens);
579
+ push("gpt5.1", labelTokens);
580
+ push("gpt-5-1", labelTokens);
581
+ push("gpt5-1", labelTokens);
582
+ push("gpt51", labelTokens);
583
+ push("chatgpt 5.1", labelTokens);
584
+ testIdTokens.add("gpt-5-1");
585
+ testIdTokens.add("gpt5-1");
586
+ testIdTokens.add("gpt51");
389
587
  }
390
588
  // Numeric variations (5.0 ↔ 50 ↔ gpt-5-0)
391
- if (base.includes('5.0') || base.includes('5-0') || base.includes('50')) {
392
- push('5.0', labelTokens);
393
- push('gpt-5.0', labelTokens);
394
- push('gpt5.0', labelTokens);
395
- push('gpt-5-0', labelTokens);
396
- push('gpt5-0', labelTokens);
397
- push('gpt50', labelTokens);
398
- push('chatgpt 5.0', labelTokens);
399
- testIdTokens.add('gpt-5-0');
400
- testIdTokens.add('gpt5-0');
401
- testIdTokens.add('gpt50');
589
+ if (base.includes("5.0") || base.includes("5-0") || base.includes("50")) {
590
+ push("5.0", labelTokens);
591
+ push("gpt-5.0", labelTokens);
592
+ push("gpt5.0", labelTokens);
593
+ push("gpt-5-0", labelTokens);
594
+ push("gpt5-0", labelTokens);
595
+ push("gpt50", labelTokens);
596
+ push("chatgpt 5.0", labelTokens);
597
+ testIdTokens.add("gpt-5-0");
598
+ testIdTokens.add("gpt5-0");
599
+ testIdTokens.add("gpt50");
402
600
  }
403
601
  // Numeric variations (5.2 ↔ 52 ↔ gpt-5-2)
404
- if (base.includes('5.2') || base.includes('5-2') || base.includes('52')) {
405
- push('5.2', labelTokens);
406
- push('gpt-5.2', labelTokens);
407
- push('gpt5.2', labelTokens);
408
- push('gpt-5-2', labelTokens);
409
- push('gpt5-2', labelTokens);
410
- push('gpt52', labelTokens);
411
- push('chatgpt 5.2', labelTokens);
602
+ if (base.includes("5.2") || base.includes("5-2") || base.includes("52")) {
603
+ push("5.2", labelTokens);
604
+ push("gpt-5.2", labelTokens);
605
+ push("gpt5.2", labelTokens);
606
+ push("gpt-5-2", labelTokens);
607
+ push("gpt5-2", labelTokens);
608
+ push("gpt52", labelTokens);
609
+ push("chatgpt 5.2", labelTokens);
412
610
  // Thinking variant: explicit testid for "Thinking" picker option
413
- if (base.includes('thinking')) {
414
- push('thinking', labelTokens);
415
- testIdTokens.add('model-switcher-gpt-5-2-thinking');
416
- testIdTokens.add('gpt-5-2-thinking');
417
- testIdTokens.add('gpt-5.2-thinking');
611
+ if (base.includes("thinking")) {
612
+ push("thinking", labelTokens);
613
+ testIdTokens.add("model-switcher-gpt-5-2-thinking");
614
+ testIdTokens.add("gpt-5-2-thinking");
615
+ testIdTokens.add("gpt-5.2-thinking");
418
616
  }
419
617
  // Instant variant: explicit testid for "Instant" picker option
420
- if (base.includes('instant')) {
421
- push('instant', labelTokens);
422
- testIdTokens.add('model-switcher-gpt-5-2-instant');
423
- testIdTokens.add('gpt-5-2-instant');
424
- testIdTokens.add('gpt-5.2-instant');
618
+ if (base.includes("instant")) {
619
+ push("instant", labelTokens);
620
+ testIdTokens.add("model-switcher-gpt-5-2-instant");
621
+ testIdTokens.add("gpt-5-2-instant");
622
+ testIdTokens.add("gpt-5.2-instant");
425
623
  }
426
624
  // Base 5.2 testids (for "Auto" mode when no suffix specified)
427
- if (!base.includes('thinking') && !base.includes('instant') && !base.includes('pro')) {
428
- testIdTokens.add('model-switcher-gpt-5-2');
625
+ if (!base.includes("thinking") && !base.includes("instant") && !base.includes("pro")) {
626
+ testIdTokens.add("model-switcher-gpt-5-2");
429
627
  }
430
- testIdTokens.add('gpt-5-2');
431
- testIdTokens.add('gpt5-2');
432
- testIdTokens.add('gpt52');
628
+ testIdTokens.add("gpt-5-2");
629
+ testIdTokens.add("gpt5-2");
630
+ testIdTokens.add("gpt52");
433
631
  }
434
632
  // Pro / research variants
435
- if (base.includes('pro')) {
436
- push('proresearch', labelTokens);
437
- push('research grade', labelTokens);
438
- push('advanced reasoning', labelTokens);
439
- if (base.includes('5.1') || base.includes('5-1') || base.includes('51')) {
440
- testIdTokens.add('gpt-5.1-pro');
441
- testIdTokens.add('gpt-5-1-pro');
442
- testIdTokens.add('gpt51pro');
443
- }
444
- if (base.includes('5.0') || base.includes('5-0') || base.includes('50')) {
445
- testIdTokens.add('gpt-5.0-pro');
446
- testIdTokens.add('gpt-5-0-pro');
447
- testIdTokens.add('gpt50pro');
448
- }
449
- if (base.includes('5.2') || base.includes('5-2') || base.includes('52')) {
450
- testIdTokens.add('gpt-5.2-pro');
451
- testIdTokens.add('gpt-5-2-pro');
452
- testIdTokens.add('gpt52pro');
453
- }
454
- testIdTokens.add('pro');
455
- testIdTokens.add('proresearch');
633
+ if (base.includes("pro")) {
634
+ push("proresearch", labelTokens);
635
+ push("research grade", labelTokens);
636
+ push("advanced reasoning", labelTokens);
637
+ if (base.includes("5.5") || base.includes("5-5") || base.includes("55")) {
638
+ push("pro extended", labelTokens);
639
+ push("extended pro", labelTokens);
640
+ testIdTokens.add("gpt-5.5-pro");
641
+ testIdTokens.add("gpt-5-5-pro");
642
+ testIdTokens.add("gpt55pro");
643
+ }
644
+ if (base.includes("5.4") || base.includes("5-4") || base.includes("54")) {
645
+ testIdTokens.add("gpt-5.4-pro");
646
+ testIdTokens.add("gpt-5-4-pro");
647
+ testIdTokens.add("gpt54pro");
648
+ }
649
+ if (base.includes("5.1") || base.includes("5-1") || base.includes("51")) {
650
+ testIdTokens.add("gpt-5.1-pro");
651
+ testIdTokens.add("gpt-5-1-pro");
652
+ testIdTokens.add("gpt51pro");
653
+ }
654
+ if (base.includes("5.0") || base.includes("5-0") || base.includes("50")) {
655
+ testIdTokens.add("gpt-5.0-pro");
656
+ testIdTokens.add("gpt-5-0-pro");
657
+ testIdTokens.add("gpt50pro");
658
+ }
659
+ if (base.includes("5.2") || base.includes("5-2") || base.includes("52")) {
660
+ testIdTokens.add("gpt-5.2-pro");
661
+ testIdTokens.add("gpt-5-2-pro");
662
+ testIdTokens.add("gpt52pro");
663
+ }
664
+ testIdTokens.add("pro");
665
+ testIdTokens.add("proresearch");
456
666
  }
457
667
  base
458
668
  .split(/\s+/)
@@ -461,7 +671,7 @@ function buildModelMatchersLiteral(targetModel) {
461
671
  .forEach((token) => {
462
672
  push(token, labelTokens);
463
673
  });
464
- const hyphenated = base.replace(/\s+/g, '-');
674
+ const hyphenated = base.replace(/\s+/g, "-");
465
675
  push(hyphenated, testIdTokens);
466
676
  push(collapsed, testIdTokens);
467
677
  push(dotless, testIdTokens);
@@ -473,7 +683,7 @@ function buildModelMatchersLiteral(targetModel) {
473
683
  labelTokens.add(base);
474
684
  }
475
685
  if (!testIdTokens.size) {
476
- testIdTokens.add(base.replace(/\s+/g, '-'));
686
+ testIdTokens.add(base.replace(/\s+/g, "-"));
477
687
  }
478
688
  return {
479
689
  labelTokens: Array.from(labelTokens).filter(Boolean),
@@ -481,5 +691,5 @@ function buildModelMatchersLiteral(targetModel) {
481
691
  };
482
692
  }
483
693
  export function buildModelSelectionExpressionForTest(targetModel) {
484
- return buildModelSelectionExpression(targetModel, 'select');
694
+ return buildModelSelectionExpression(targetModel, "select");
485
695
  }