@steipete/oracle 0.9.0 → 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 (177) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +61 -48
  3. package/dist/bin/oracle-cli.js +455 -402
  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 +275 -117
  22. package/dist/src/browser/actions/navigation.js +161 -137
  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 +390 -295
  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 +1 -1
  40. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  42. package/dist/src/browser/providers/index.js +2 -2
  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 +62 -48
  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 +2 -2
  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 +3 -3
  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 +7 -7
  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 +127 -106
  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 +32 -28
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +95 -81
  77. package/dist/src/cli/sessionLineage.js +6 -2
  78. package/dist/src/cli/sessionRunner.js +103 -93
  79. package/dist/src/cli/sessionTable.js +26 -23
  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 +139 -128
  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 +19 -15
  86. package/dist/src/gemini-web/client.js +76 -70
  87. package/dist/src/gemini-web/executionMode.js +6 -8
  88. package/dist/src/gemini-web/executor.js +98 -93
  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 +51 -47
  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 +50 -41
  99. package/dist/src/oracle/config.js +96 -66
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +55 -46
  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 -33
  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 +16 -13
  112. package/dist/src/oracle/run.js +156 -134
  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 +77 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/README.md +2 -0
  127. package/package.json +66 -62
  128. package/vendor/oracle-notifier/README.md +2 -0
  129. package/dist/markdansi/types/index.js +0 -4
  130. package/dist/oracle/bin/oracle-cli.js +0 -472
  131. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  132. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  133. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  134. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  135. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  136. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  137. package/dist/oracle/src/browser/config.js +0 -33
  138. package/dist/oracle/src/browser/constants.js +0 -40
  139. package/dist/oracle/src/browser/cookies.js +0 -210
  140. package/dist/oracle/src/browser/domDebug.js +0 -36
  141. package/dist/oracle/src/browser/index.js +0 -331
  142. package/dist/oracle/src/browser/pageActions.js +0 -5
  143. package/dist/oracle/src/browser/prompt.js +0 -88
  144. package/dist/oracle/src/browser/promptSummary.js +0 -20
  145. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  146. package/dist/oracle/src/browser/types.js +0 -1
  147. package/dist/oracle/src/browser/utils.js +0 -62
  148. package/dist/oracle/src/browserMode.js +0 -1
  149. package/dist/oracle/src/cli/browserConfig.js +0 -44
  150. package/dist/oracle/src/cli/dryRun.js +0 -59
  151. package/dist/oracle/src/cli/engine.js +0 -17
  152. package/dist/oracle/src/cli/errorUtils.js +0 -9
  153. package/dist/oracle/src/cli/help.js +0 -70
  154. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  155. package/dist/oracle/src/cli/options.js +0 -103
  156. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  157. package/dist/oracle/src/cli/rootAlias.js +0 -30
  158. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  159. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  160. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  161. package/dist/oracle/src/heartbeat.js +0 -43
  162. package/dist/oracle/src/oracle/client.js +0 -48
  163. package/dist/oracle/src/oracle/config.js +0 -29
  164. package/dist/oracle/src/oracle/errors.js +0 -101
  165. package/dist/oracle/src/oracle/files.js +0 -220
  166. package/dist/oracle/src/oracle/format.js +0 -33
  167. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  168. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  169. package/dist/oracle/src/oracle/request.js +0 -48
  170. package/dist/oracle/src/oracle/run.js +0 -444
  171. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  172. package/dist/oracle/src/oracle/types.js +0 -1
  173. package/dist/oracle/src/oracle.js +0 -9
  174. package/dist/oracle/src/sessionManager.js +0 -205
  175. package/dist/oracle/src/version.js +0 -39
  176. package/dist/scripts/chrome/browser-tools.js +0 -295
  177. package/dist/src/browser/profileSync.js +0 -141
@@ -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 '';
@@ -73,7 +83,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
73
83
  const targetWords = normalizedTarget.split(' ').filter(Boolean);
74
84
  const desiredVersion = normalizedTarget.includes('5 4')
75
85
  ? '5-4'
76
- : normalizedTarget.includes('5 2')
86
+ : normalizedTarget.includes('5 5')
87
+ ? '5-5'
88
+ : normalizedTarget.includes('5 2')
77
89
  ? '5-2'
78
90
  : normalizedTarget.includes('5 1')
79
91
  ? '5-1'
@@ -83,6 +95,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
83
95
  const wantsPro = normalizedTarget.includes(' pro') || normalizedTarget.endsWith(' pro') || normalizedTokens.includes('pro');
84
96
  const wantsInstant = normalizedTarget.includes('instant');
85
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
+ };
86
109
 
87
110
  const button = document.querySelector(BUTTON_SELECTOR);
88
111
  if (!button) {
@@ -110,13 +133,19 @@ function buildModelSelectionExpression(targetModel, strategy) {
110
133
  };
111
134
 
112
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;
113
140
  if (MODEL_STRATEGY === 'current') {
114
- return { status: 'already-selected', label: getButtonLabel() };
141
+ return { status: 'already-selected', label: getResolvedLabel(PRIMARY_LABEL) };
115
142
  }
116
143
  const buttonMatchesTarget = () => {
117
144
  const normalizedLabel = normalizeText(getButtonLabel());
118
145
  if (!normalizedLabel) return false;
146
+ if (isTargetGpt55VisibleAlias(normalizedLabel)) return true;
119
147
  if (desiredVersion) {
148
+ if (desiredVersion === '5-5' && !normalizedLabel.includes('5 5')) return false;
120
149
  if (desiredVersion === '5-4' && !normalizedLabel.includes('5 4')) return false;
121
150
  if (desiredVersion === '5-2' && !normalizedLabel.includes('5 2')) return false;
122
151
  if (desiredVersion === '5-1' && !normalizedLabel.includes('5 1')) return false;
@@ -131,9 +160,47 @@ function buildModelSelectionExpression(targetModel, strategy) {
131
160
  if (!wantsThinking && normalizedLabel.includes('thinking')) return false;
132
161
  return true;
133
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
+ };
134
201
 
135
- if (buttonMatchesTarget()) {
136
- return { status: 'already-selected', label: getButtonLabel() };
202
+ if (activeSelectionMatchesTarget()) {
203
+ return { status: 'already-selected', label: getResolvedLabel(PRIMARY_LABEL) };
137
204
  }
138
205
 
139
206
  let lastPointerClick = 0;
@@ -160,7 +227,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
160
227
  if (dataSelected === 'true' || selectedStates.includes(dataState)) {
161
228
  return true;
162
229
  }
163
- 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')) {
164
231
  return true;
165
232
  }
166
233
  return false;
@@ -182,6 +249,12 @@ function buildModelSelectionExpression(targetModel, strategy) {
182
249
  normalizedTestId.includes('gpt-5-2') ||
183
250
  normalizedTestId.includes('gpt-5.2') ||
184
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');
185
258
  const has54 =
186
259
  normalizedTestId.includes('5-4') ||
187
260
  normalizedTestId.includes('5.4') ||
@@ -200,7 +273,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
200
273
  normalizedTestId.includes('gpt-5-0') ||
201
274
  normalizedTestId.includes('gpt-5.0') ||
202
275
  normalizedTestId.includes('gpt50');
203
- const candidateVersion = has54 ? '5-4' : 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;
204
277
  // If a candidate advertises a different version, ignore it entirely.
205
278
  if (candidateVersion && candidateVersion !== desiredVersion) {
206
279
  return 0;
@@ -227,6 +300,20 @@ function buildModelSelectionExpression(targetModel, strategy) {
227
300
  }
228
301
  }
229
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
+ }
230
317
  if (normalizedText && normalizedTarget) {
231
318
  if (normalizedText === normalizedTarget) {
232
319
  score += 500;
@@ -301,6 +388,24 @@ function buildModelSelectionExpression(targetModel, strategy) {
301
388
  }
302
389
  return bestMatch;
303
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
+ });
304
409
 
305
410
  return new Promise((resolve) => {
306
411
  const start = performance.now();
@@ -345,11 +450,13 @@ function buildModelSelectionExpression(targetModel, strategy) {
345
450
  ensureMenuOpen();
346
451
  const match = findBestOption();
347
452
  if (match) {
348
- if (optionIsSelected(match.node)) {
453
+ if (optionIsSelected(match.node) || activeSelectionMatchesTarget()) {
349
454
  closeMenu();
350
- resolve({ status: 'already-selected', label: getButtonLabel() || match.label });
455
+ resolve({ status: 'already-selected', label: getResolvedLabel(match.label) });
351
456
  return;
352
457
  }
458
+ const previousButtonLabel = normalizeText(getButtonLabel());
459
+ const previousComposerSignal = readComposerModelSignal();
353
460
  dispatchClickSequence(match.node);
354
461
  // Submenus (e.g. "Legacy models") need a second pass to pick the actual model option.
355
462
  // Keep scanning once the submenu opens instead of treating the submenu click as a final switch.
@@ -358,15 +465,15 @@ function buildModelSelectionExpression(targetModel, strategy) {
358
465
  setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
359
466
  return;
360
467
  }
361
- // Wait for the top bar label to reflect the requested model; otherwise keep scanning.
362
- setTimeout(() => {
363
- if (buttonMatchesTarget()) {
468
+ // Wait for the selected model signal to settle before reopening the picker.
469
+ waitForTargetSelection(previousButtonLabel, previousComposerSignal).then((selectionSettled) => {
470
+ if (selectionSettled) {
364
471
  closeMenu();
365
- resolve({ status: 'switched', label: getButtonLabel() || match.label });
472
+ resolve({ status: 'switched', label: getResolvedLabel(match.label) });
366
473
  return;
367
474
  }
368
475
  attempt();
369
- }, Math.max(120, INITIAL_WAIT_MS));
476
+ });
370
477
  return;
371
478
  }
372
479
  if (performance.now() - start > MAX_WAIT_MS) {
@@ -385,6 +492,27 @@ function buildModelSelectionExpression(targetModel, strategy) {
385
492
  export function buildModelMatchersLiteralForTest(targetModel) {
386
493
  return buildModelMatchersLiteral(targetModel);
387
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
+ }
388
516
  function buildModelMatchersLiteral(targetModel) {
389
517
  const base = targetModel.trim().toLowerCase();
390
518
  const labelTokens = new Set();
@@ -396,115 +524,145 @@ function buildModelMatchersLiteral(targetModel) {
396
524
  }
397
525
  };
398
526
  push(base, labelTokens);
399
- push(base.replace(/\s+/g, ' '), labelTokens);
400
- const collapsed = base.replace(/\s+/g, '');
527
+ push(base.replace(/\s+/g, " "), labelTokens);
528
+ const collapsed = base.replace(/\s+/g, "");
401
529
  push(collapsed, labelTokens);
402
- const dotless = base.replace(/[.]/g, '');
530
+ const dotless = base.replace(/[.]/g, "");
403
531
  push(dotless, labelTokens);
404
532
  push(`chatgpt ${base}`, labelTokens);
405
533
  push(`chatgpt ${dotless}`, labelTokens);
406
534
  push(`gpt ${base}`, labelTokens);
407
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
+ }
408
559
  // 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');
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");
423
574
  }
424
575
  // 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');
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");
436
587
  }
437
588
  // 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');
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");
449
600
  }
450
601
  // 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);
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);
459
610
  // 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');
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");
465
616
  }
466
617
  // 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');
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");
472
623
  }
473
624
  // 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');
625
+ if (!base.includes("thinking") && !base.includes("instant") && !base.includes("pro")) {
626
+ testIdTokens.add("model-switcher-gpt-5-2");
476
627
  }
477
- testIdTokens.add('gpt-5-2');
478
- testIdTokens.add('gpt5-2');
479
- testIdTokens.add('gpt52');
628
+ testIdTokens.add("gpt-5-2");
629
+ testIdTokens.add("gpt5-2");
630
+ testIdTokens.add("gpt52");
480
631
  }
481
632
  // 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');
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");
508
666
  }
509
667
  base
510
668
  .split(/\s+/)
@@ -513,7 +671,7 @@ function buildModelMatchersLiteral(targetModel) {
513
671
  .forEach((token) => {
514
672
  push(token, labelTokens);
515
673
  });
516
- const hyphenated = base.replace(/\s+/g, '-');
674
+ const hyphenated = base.replace(/\s+/g, "-");
517
675
  push(hyphenated, testIdTokens);
518
676
  push(collapsed, testIdTokens);
519
677
  push(dotless, testIdTokens);
@@ -525,7 +683,7 @@ function buildModelMatchersLiteral(targetModel) {
525
683
  labelTokens.add(base);
526
684
  }
527
685
  if (!testIdTokens.size) {
528
- testIdTokens.add(base.replace(/\s+/g, '-'));
686
+ testIdTokens.add(base.replace(/\s+/g, "-"));
529
687
  }
530
688
  return {
531
689
  labelTokens: Array.from(labelTokens).filter(Boolean),
@@ -533,5 +691,5 @@ function buildModelMatchersLiteral(targetModel) {
533
691
  };
534
692
  }
535
693
  export function buildModelSelectionExpressionForTest(targetModel) {
536
- return buildModelSelectionExpression(targetModel, 'select');
694
+ return buildModelSelectionExpression(targetModel, "select");
537
695
  }