@steipete/oracle 0.18.0 → 0.20.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 (169) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +76 -76
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +170 -45
  13. package/dist/src/browser/actions/modelSelection.js +91 -4
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +364 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +156 -16
  19. package/dist/src/browser/actions/webSearch.js +96 -0
  20. package/dist/src/browser/attachRunning.js +46 -2
  21. package/dist/src/browser/attachmentValidation.js +53 -0
  22. package/dist/src/browser/cancellation.js +105 -0
  23. package/dist/src/browser/chatgptImages.js +2 -0
  24. package/dist/src/browser/chatgptThrottle.js +104 -0
  25. package/dist/src/browser/chromeLifecycle.js +272 -47
  26. package/dist/src/browser/config.js +14 -1
  27. package/dist/src/browser/detect.js +1 -1
  28. package/dist/src/browser/index.js +525 -274
  29. package/dist/src/browser/liveTabs.js +3 -4
  30. package/dist/src/browser/modelDisplay.js +9 -0
  31. package/dist/src/browser/pageActions.js +2 -2
  32. package/dist/src/browser/profileState.js +114 -9
  33. package/dist/src/browser/projectSourcesRunner.js +83 -15
  34. package/dist/src/browser/prompt.js +272 -113
  35. package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
  36. package/dist/src/browser/reattach.js +31 -4
  37. package/dist/src/browser/recoveryTarget.js +131 -0
  38. package/dist/src/browser/sessionRunner.js +73 -11
  39. package/dist/src/browser/tabLeaseRegistry.js +434 -38
  40. package/dist/src/browser/targetClaim.js +54 -0
  41. package/dist/src/browser/utils.js +20 -2
  42. package/dist/src/cli/browserConfig.js +36 -4
  43. package/dist/src/cli/browserDefaults.js +10 -0
  44. package/dist/src/cli/browserTabs.js +15 -29
  45. package/dist/src/cli/detachedSession.js +69 -0
  46. package/dist/src/cli/dryRun.js +58 -48
  47. package/dist/src/cli/harvestIntegrity.js +140 -0
  48. package/dist/src/cli/options.js +15 -0
  49. package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
  50. package/dist/src/cli/runOptions.js +19 -4
  51. package/dist/src/cli/serveBrowserConfig.js +24 -0
  52. package/dist/src/cli/sessionDisplay.js +13 -7
  53. package/dist/src/cli/sessionRunner.js +95 -12
  54. package/dist/src/config.js +1 -0
  55. package/dist/src/gemini-web/client.js +67 -21
  56. package/dist/src/gemini-web/executor.js +4 -0
  57. package/dist/src/gemini-web/http.js +15 -0
  58. package/dist/src/gemini-web/models.js +15 -11
  59. package/dist/src/mcp/server.js +11 -14
  60. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  61. package/dist/src/mcp/tools/consult.js +86 -24
  62. package/dist/src/mcp/tools/projectSources.js +4 -6
  63. package/dist/src/mcp/tools/sessionResources.js +1 -1
  64. package/dist/src/mcp/tools/sessions.js +2 -2
  65. package/dist/src/mcp/tools/wait.js +182 -0
  66. package/dist/src/mcp/types.js +8 -1
  67. package/dist/src/oracle/config.js +14 -0
  68. package/dist/src/oracle/errors.js +11 -0
  69. package/dist/src/oracle/geminiModels.js +1 -0
  70. package/dist/src/oracle/providerRoutePlan.js +3 -2
  71. package/dist/src/oracle/run.js +27 -4
  72. package/dist/src/remote/client.js +73 -6
  73. package/dist/src/remote/health.js +6 -2
  74. package/dist/src/remote/runSlots.js +83 -0
  75. package/dist/src/remote/server.js +346 -71
  76. package/dist/src/sessionManager.js +36 -1
  77. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  78. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  79. package/package.json +25 -23
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  81. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  82. package/dist/bin/oracle.js +0 -683
  83. package/dist/docs-site/.nojekyll +0 -0
  84. package/dist/docs-site/CNAME +0 -1
  85. package/dist/docs-site/RELEASING.html +0 -410
  86. package/dist/docs-site/agents.html +0 -374
  87. package/dist/docs-site/anthropic.html +0 -368
  88. package/dist/docs-site/bridge.html +0 -400
  89. package/dist/docs-site/browser-mode.html +0 -588
  90. package/dist/docs-site/chromium-forks.html +0 -347
  91. package/dist/docs-site/cli-reference.html +0 -346
  92. package/dist/docs-site/configuration.html +0 -436
  93. package/dist/docs-site/favicon.svg +0 -14
  94. package/dist/docs-site/followup.html +0 -368
  95. package/dist/docs-site/gemini.html +0 -379
  96. package/dist/docs-site/grok.html +0 -325
  97. package/dist/docs-site/index.html +0 -360
  98. package/dist/docs-site/install.html +0 -335
  99. package/dist/docs-site/linux.html +0 -321
  100. package/dist/docs-site/llms.txt +0 -43
  101. package/dist/docs-site/manual-tests.html +0 -595
  102. package/dist/docs-site/mcp.html +0 -368
  103. package/dist/docs-site/multimodel.html +0 -364
  104. package/dist/docs-site/mythical-pro-agents.html +0 -360
  105. package/dist/docs-site/notifier.html +0 -338
  106. package/dist/docs-site/openai-endpoints.html +0 -387
  107. package/dist/docs-site/openrouter.html +0 -344
  108. package/dist/docs-site/quickstart.html +0 -369
  109. package/dist/docs-site/refactor/ux.html +0 -532
  110. package/dist/docs-site/sessions.html +0 -388
  111. package/dist/docs-site/social-card.png +0 -0
  112. package/dist/docs-site/social-card.svg +0 -79
  113. package/dist/docs-site/spec.html +0 -363
  114. package/dist/docs-site/testing.html +0 -320
  115. package/dist/docs-site/tui-debug.html +0 -326
  116. package/dist/docs-site/windows-work.html +0 -323
  117. package/dist/docs-site/windows.html +0 -320
  118. package/dist/markdansi/types/index.js +0 -4
  119. package/dist/oracle/bin/oracle-cli.js +0 -472
  120. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  121. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  122. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  123. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  124. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  125. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  126. package/dist/oracle/src/browser/config.js +0 -33
  127. package/dist/oracle/src/browser/constants.js +0 -40
  128. package/dist/oracle/src/browser/cookies.js +0 -210
  129. package/dist/oracle/src/browser/domDebug.js +0 -36
  130. package/dist/oracle/src/browser/index.js +0 -331
  131. package/dist/oracle/src/browser/pageActions.js +0 -5
  132. package/dist/oracle/src/browser/prompt.js +0 -88
  133. package/dist/oracle/src/browser/promptSummary.js +0 -20
  134. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  135. package/dist/oracle/src/browser/types.js +0 -1
  136. package/dist/oracle/src/browser/utils.js +0 -62
  137. package/dist/oracle/src/browserMode.js +0 -1
  138. package/dist/oracle/src/cli/browserConfig.js +0 -44
  139. package/dist/oracle/src/cli/dryRun.js +0 -59
  140. package/dist/oracle/src/cli/engine.js +0 -17
  141. package/dist/oracle/src/cli/errorUtils.js +0 -9
  142. package/dist/oracle/src/cli/help.js +0 -70
  143. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  144. package/dist/oracle/src/cli/options.js +0 -103
  145. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  146. package/dist/oracle/src/cli/rootAlias.js +0 -30
  147. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  148. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  149. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  150. package/dist/oracle/src/heartbeat.js +0 -43
  151. package/dist/oracle/src/oracle/client.js +0 -48
  152. package/dist/oracle/src/oracle/config.js +0 -29
  153. package/dist/oracle/src/oracle/errors.js +0 -101
  154. package/dist/oracle/src/oracle/files.js +0 -220
  155. package/dist/oracle/src/oracle/format.js +0 -33
  156. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  157. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  158. package/dist/oracle/src/oracle/request.js +0 -48
  159. package/dist/oracle/src/oracle/run.js +0 -444
  160. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  161. package/dist/oracle/src/oracle/types.js +0 -1
  162. package/dist/oracle/src/oracle.js +0 -9
  163. package/dist/oracle/src/sessionManager.js +0 -205
  164. package/dist/oracle/src/version.js +0 -39
  165. package/dist/scripts/chrome/browser-tools.js +0 -295
  166. package/dist/src/browser/chromeCookies.js +0 -312
  167. package/dist/src/browser/keytarShim.js +0 -56
  168. package/dist/src/browser/profileSync.js +0 -141
  169. package/dist/src/browser/windowsCookies.js +0 -219
@@ -3,7 +3,7 @@ import { buildConversationTurnListExpression } from "../conversationTurns.js";
3
3
  import { delay } from "../utils.js";
4
4
  import { isDeepResearchIncompleteText } from "../deepResearchResult.js";
5
5
  import { buildClickDispatcher } from "./domEvents.js";
6
- import { captureAssistantMarkdown, readAssistantSnapshot } from "./assistantResponse.js";
6
+ import { captureAssistantMarkdown, readAssistantSnapshot, throwIfAssistantUiError, } from "./assistantResponse.js";
7
7
  import { BrowserAutomationError } from "../../oracle/errors.js";
8
8
  /**
9
9
  * Activates Deep Research mode through ChatGPT's composer tools menu and
@@ -79,65 +79,131 @@ async function waitForDeepResearchPill(Runtime, timeoutMs = 5000) {
79
79
  * After prompt submission, waits for the research plan to appear and
80
80
  * auto-confirm (~60s countdown + 10s safety margin).
81
81
  */
82
- export async function waitForResearchPlanAutoConfirm(Runtime, logger, autoConfirmWaitMs = DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS) {
82
+ export async function waitForResearchPlanAutoConfirm(Runtime, logger, autoConfirmWaitMs = DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, options) {
83
+ const ignoredTargetKeys = new Set(options?.ignoredTargetKeys ?? []);
84
+ const minTurnIndex = typeof options?.minTurnIndex === "number" && Number.isFinite(options.minTurnIndex)
85
+ ? Math.floor(options.minTurnIndex)
86
+ : -1;
87
+ let capturedPlan = null;
88
+ let loggedPlan = false;
89
+ const targetOwnerMinTurnIndex = minTurnIndex >= 0 && options?.targetBaselineCaptured !== true ? minTurnIndex : -1;
90
+ const readPlanStatus = async () => {
91
+ const targetRead = options?.client
92
+ ? ((await readDeepResearchTargetResult(options.client, ignoredTargetKeys, targetOwnerMinTurnIndex).catch(() => null))?.read ?? null)
93
+ : null;
94
+ if (targetRead?.planTitle || targetRead?.researchStarted) {
95
+ return targetRead;
96
+ }
97
+ const inPageRead = options?.Page
98
+ ? await readDeepResearchFrameResult(Runtime, options.Page, options.client, minTurnIndex).catch(() => null)
99
+ : null;
100
+ return inPageRead?.read ?? targetRead;
101
+ };
102
+ const capturePlan = async (status) => {
103
+ if (!status.planTitle || !status.planSteps || status.planSteps.length === 0) {
104
+ return capturedPlan;
105
+ }
106
+ const next = {
107
+ title: status.planTitle,
108
+ steps: status.planSteps,
109
+ phase: status.researchStarted ? "researching" : "planning",
110
+ ...(status.planActionText ? { actionText: status.planActionText } : {}),
111
+ capturedAt: capturedPlan?.capturedAt ?? new Date().toISOString(),
112
+ };
113
+ const changed = !capturedPlan ||
114
+ capturedPlan.phase !== next.phase ||
115
+ capturedPlan.title !== next.title ||
116
+ capturedPlan.steps.join("\n") !== next.steps.join("\n") ||
117
+ capturedPlan.actionText !== next.actionText;
118
+ capturedPlan = next;
119
+ if (!loggedPlan) {
120
+ logger(`[browser] Deep Research plan detected:\n${[next.title, ...next.steps.map((step, index) => `${index + 1}. ${step}`)].join("\n")}`);
121
+ loggedPlan = true;
122
+ }
123
+ if (changed) {
124
+ await options?.onPlan?.(next);
125
+ }
126
+ return next;
127
+ };
128
+ const reportResearchStarted = async () => {
129
+ if (capturedPlan?.phase === "planning") {
130
+ capturedPlan = { ...capturedPlan, phase: "researching" };
131
+ await options?.onPlan?.(capturedPlan);
132
+ }
133
+ logger("[browser] Deep Research execution started; plan countdown is complete.");
134
+ return capturedPlan;
135
+ };
83
136
  // Phase A: Detect research plan appearance (up to 60s)
84
137
  const planDeadline = Date.now() + 60_000;
85
138
  let planDetected = false;
86
139
  while (Date.now() < planDeadline) {
140
+ const frameStatus = await readPlanStatus();
141
+ if (frameStatus) {
142
+ await capturePlan(frameStatus);
143
+ if (frameStatus.researchStarted) {
144
+ return reportResearchStarted();
145
+ }
146
+ if (capturedPlan) {
147
+ planDetected = true;
148
+ break;
149
+ }
150
+ }
151
+ // Legacy/inline fallback. Do not treat an arbitrary large iframe as a plan:
152
+ // ChatGPT projects, attachments, and other tools also render large iframes.
87
153
  const { result } = await Runtime.evaluate({
88
154
  expression: `(() => {
89
- const iframes = document.querySelectorAll('iframe');
90
- const hasResearchIframe = Array.from(iframes).some(f => {
91
- const rect = f.getBoundingClientRect();
92
- return rect.width > 200 && rect.height > 200;
93
- });
94
- const assistantText = (document.querySelector('[data-message-author-role="assistant"]')?.textContent || '').toLowerCase();
155
+ const turns = Array.from(document.querySelectorAll('[data-message-author-role="assistant"]'));
156
+ const assistantText = String(turns.at(-1)?.textContent || '').toLowerCase();
95
157
  const hasResearchText = assistantText.includes('researching') ||
96
158
  assistantText.includes('research plan') ||
97
- assistantText.includes('survey') ||
98
- assistantText.includes('analyze');
99
- return { hasResearchIframe, hasResearchText };
159
+ assistantText.includes('正在研究') ||
160
+ assistantText.includes('研究计划');
161
+ return { hasResearchText };
100
162
  })()`,
101
163
  returnByValue: true,
102
164
  });
103
165
  const val = result?.value;
104
- if (val?.hasResearchIframe || val?.hasResearchText) {
166
+ if (val?.hasResearchText) {
105
167
  planDetected = true;
106
- logger("Research plan detected, waiting for auto-confirm countdown...");
168
+ logger("[browser] Deep Research activity detected; waiting for plan auto-confirm...");
107
169
  break;
108
170
  }
109
171
  await delay(2_000);
110
172
  }
111
173
  if (!planDetected) {
112
174
  logger("Warning: Research plan not detected within 60s; continuing (may have auto-confirmed already)");
113
- return;
175
+ return capturedPlan;
114
176
  }
115
- // Phase B: Wait for auto-confirm countdown
177
+ // Phase B: Wait for the OOPIF's real execution state instead of sleeping for
178
+ // the full countdown. The main page cannot see this sandboxed iframe's text.
116
179
  const confirmStart = Date.now();
117
180
  while (Date.now() - confirmStart < autoConfirmWaitMs) {
181
+ const frameStatus = await readPlanStatus();
182
+ if (frameStatus) {
183
+ await capturePlan(frameStatus);
184
+ if (frameStatus.researchStarted) {
185
+ return reportResearchStarted();
186
+ }
187
+ }
118
188
  const { result } = await Runtime.evaluate({
119
189
  expression: `(() => {
120
- const iframes = document.querySelectorAll('iframe');
121
- const hasLargeIframe = Array.from(iframes).some(f => {
122
- const rect = f.getBoundingClientRect();
123
- return rect.width > 200 && rect.height > 200;
124
- });
125
190
  const text = (document.body?.innerText || '').toLowerCase();
126
191
  const isResearching = text.includes('researching...') ||
127
192
  text.includes('reading sources') ||
128
- text.includes('considering');
129
- return { hasLargeIframe, isResearching };
193
+ text.includes('正在研究') ||
194
+ text.includes('正在阅读来源');
195
+ return { isResearching };
130
196
  })()`,
131
197
  returnByValue: true,
132
198
  });
133
199
  const val = result?.value;
134
200
  if (val?.isResearching) {
135
- logger("Research plan confirmed, execution started");
136
- return;
201
+ return reportResearchStarted();
137
202
  }
138
- await delay(5_000);
203
+ await delay(2_000);
139
204
  }
140
- logger("Auto-confirm wait complete, proceeding to monitor research progress");
205
+ logger("[browser] Deep Research plan wait elapsed; proceeding to monitor research progress.");
206
+ return capturedPlan;
141
207
  }
142
208
  /**
143
209
  * Polls for Deep Research completion over 5-30+ minutes.
@@ -248,6 +314,7 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
248
314
  */
249
315
  export async function extractDeepResearchResult(Runtime, logger, minTurnIndex) {
250
316
  const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
317
+ throwIfAssistantUiError(snapshot);
251
318
  const meta = {
252
319
  turnId: snapshot?.turnId ?? null,
253
320
  messageId: snapshot?.messageId ?? null,
@@ -599,6 +666,44 @@ function buildDeepResearchFrameStatusExpression() {
599
666
  return `(() => {
600
667
  const rawText = document.body?.innerText || '';
601
668
  const html = document.body?.innerHTML || '';
669
+ const cleanText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
670
+ const sections = typeof document.querySelectorAll === 'function'
671
+ ? Array.from(document.querySelectorAll('section'))
672
+ : [];
673
+ const planSection = sections.find((section) => {
674
+ if (typeof section.querySelector !== 'function' ||
675
+ typeof section.querySelectorAll !== 'function' ||
676
+ !cleanText(section.querySelector('h2')?.textContent) ||
677
+ section.querySelectorAll('ul li').length === 0) {
678
+ return false;
679
+ }
680
+ const buttons = Array.from(section.querySelectorAll('button'));
681
+ const hasPlanAction = buttons.some((button) =>
682
+ /^(edit|update|编辑|更新)$/i.test(cleanText(button.textContent))
683
+ );
684
+ const hasResearchStatus = Boolean(section.querySelector('p.loading-shimmer')) ||
685
+ buttons.some((button) =>
686
+ /stop research|停止研究/i.test(cleanText(button.getAttribute?.('aria-label')))
687
+ );
688
+ return hasPlanAction || hasResearchStatus;
689
+ });
690
+ const planTitle = cleanText(planSection?.querySelector?.('h2')?.textContent);
691
+ const planSteps = planSection && typeof planSection.querySelectorAll === 'function'
692
+ ? Array.from(planSection.querySelectorAll('ul li'))
693
+ .map((item) => cleanText(item.textContent))
694
+ .filter(Boolean)
695
+ : [];
696
+ const planActionText = planSection && typeof planSection.querySelectorAll === 'function'
697
+ ? Array.from(planSection.querySelectorAll('button'))
698
+ .map((button) => cleanText(button.textContent))
699
+ .find((text) => /^(edit|update|编辑|更新)$/i.test(text)) || ''
700
+ : '';
701
+ const hasResearchShimmer = Boolean(planSection?.querySelector?.('p.loading-shimmer'));
702
+ const hasStopResearchControl = typeof planSection?.querySelectorAll === 'function' &&
703
+ Array.from(planSection.querySelectorAll('button')).some((button) =>
704
+ /stop research|停止研究/i.test(cleanText(button.getAttribute?.('aria-label')))
705
+ );
706
+ const researchStarted = planSteps.length > 0 && (hasResearchShimmer || hasStopResearchControl);
602
707
  const isPlaceholder = (line) => /^(called tool|used tool|użyto narzędzia|narzędzie wywołane)$/i.test(line);
603
708
  const isCompletionLine = (line) =>
604
709
  /^(research completed|badanie ukończone)\\b/i.test(line);
@@ -640,6 +745,10 @@ function buildDeepResearchFrameStatusExpression() {
640
745
  return {
641
746
  completed,
642
747
  inProgress,
748
+ researchStarted,
749
+ planTitle: planTitle || undefined,
750
+ planSteps: planSteps.length > 0 ? planSteps : undefined,
751
+ planActionText: planActionText || undefined,
643
752
  textLength: reportText.length || rawText.trim().length,
644
753
  text: completed ? reportText : undefined,
645
754
  html: completed ? html : undefined,
@@ -772,9 +881,9 @@ export function buildDeepResearchCompletionPollExpressionForTest(minTurnIndex =
772
881
  return buildDeepResearchCompletionPollExpression(minTurnIndex);
773
882
  }
774
883
  function buildFindDeepResearchPillExpression(functionName = "findDeepResearchPill") {
775
- const pillLabel = JSON.stringify(DEEP_RESEARCH_PILL_LABEL);
884
+ const pillLabels = JSON.stringify([DEEP_RESEARCH_PILL_LABEL, "深度研究"]);
776
885
  return `const ${functionName} = () => {
777
- const label = ${pillLabel}.toLowerCase();
886
+ const labels = ${pillLabels}.map(label => label.toLowerCase());
778
887
  const selectors = [
779
888
  '.__composer-pill-composite',
780
889
  '.__composer-pill',
@@ -797,7 +906,7 @@ function buildFindDeepResearchPillExpression(functionName = "findDeepResearchPil
797
906
  pill.querySelector('button')?.getAttribute('aria-label') ||
798
907
  ''
799
908
  ).toLowerCase();
800
- if (text.includes(label) || aria.includes(label)) {
909
+ if (labels.some(label => text.includes(label) || aria.includes(label))) {
801
910
  return pill;
802
911
  }
803
912
  }
@@ -818,6 +927,18 @@ function buildWaitForDeepResearchPillExpression(timeoutMs) {
818
927
  function buildActivateDeepResearchExpression() {
819
928
  const plusBtnSelector = JSON.stringify(DEEP_RESEARCH_PLUS_BUTTON);
820
929
  const targetText = JSON.stringify(DEEP_RESEARCH_DROPDOWN_ITEM_TEXT);
930
+ const targetLabels = JSON.stringify([DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, "深度研究"]);
931
+ const descriptionLabels = JSON.stringify(["Get a detailed report", "获取详细报告"]);
932
+ const addFilesLabels = JSON.stringify(["add files", "添加文件"]);
933
+ const dropdownReadyLabels = JSON.stringify([
934
+ "add photos",
935
+ "create image",
936
+ "web search",
937
+ "deep research",
938
+ "get a detailed report",
939
+ "深度研究",
940
+ "获取详细报告",
941
+ ]);
821
942
  return `(async () => {
822
943
  ${buildClickDispatcher()}
823
944
  ${buildFindDeepResearchPillExpression()}
@@ -863,8 +984,12 @@ function buildActivateDeepResearchExpression() {
863
984
  '[data-radix-popper-content-wrapper]',
864
985
  '[data-floating-ui-portal]',
865
986
  ].join(',');
866
- const target = ${targetText}.toLowerCase();
867
987
  const normalizeText = (value) => String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
988
+ const compactText = (value) => normalizeText(value).replace(/\\s+/g, '');
989
+ const targetLabels = ${targetLabels}.map(normalizeText);
990
+ const descriptionLabels = ${descriptionLabels}.map(normalizeText);
991
+ const addFilesLabels = ${addFilesLabels}.map(normalizeText);
992
+ const dropdownReadyLabels = ${dropdownReadyLabels}.map(normalizeText);
868
993
  const getText = (item) => normalizeText(item.textContent || item.getAttribute?.('aria-label') || '');
869
994
  const isInPopover = (item) => Boolean(item.closest?.(popoverSelector));
870
995
  const isVisible = (item) => {
@@ -890,14 +1015,16 @@ function buildActivateDeepResearchExpression() {
890
1015
  input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
891
1016
  input.dispatchEvent(new Event('change', { bubbles: true }));
892
1017
  };
893
- const isDeepResearchText = (text) => (
894
- text === target ||
895
- text.startsWith(target + ' ') ||
896
- text === 'get a detailed report' ||
897
- text.startsWith('get a detailed report ') ||
898
- (text.includes(target) && text.includes('detailed report')) ||
899
- text.replace(/\\s+/g, '').startsWith('deepresearch')
900
- );
1018
+ const isDeepResearchText = (text) => {
1019
+ const compact = compactText(text);
1020
+ const exactLabel = targetLabels.some(label => compact === compactText(label) || text.startsWith(label + ' '));
1021
+ const exactDescription = descriptionLabels.some(
1022
+ label => text === label || text.startsWith(label + ' ')
1023
+ );
1024
+ const combinedLabel = targetLabels.some(label => compact.startsWith(compactText(label))) &&
1025
+ descriptionLabels.some(label => compact.includes(compactText(label)));
1026
+ return exactLabel || exactDescription || combinedLabel;
1027
+ };
901
1028
  const getClickableItem = (item) => item.closest?.(
902
1029
  '[data-radix-collection-item], [role="option"], [cmdk-item], button, [role="menuitem"], [role="menuitemradio"], .__menu-item, [class*="__menu-item"], [class*="menu-item"]'
903
1030
  ) || item;
@@ -914,7 +1041,7 @@ function buildActivateDeepResearchExpression() {
914
1041
  .map(item => {
915
1042
  const text = getText(item);
916
1043
  const clickable = getClickableItem(item);
917
- const exact = text === target ? 0 : 1;
1044
+ const exact = targetLabels.includes(text) ? 0 : 1;
918
1045
  const menuRow = /(^|\\s)__menu-item(\\s|$)/.test(clickable.className || '') ? 0 : 1;
919
1046
  return { item: clickable, score: exact + menuRow, textLength: text.length };
920
1047
  })
@@ -945,7 +1072,9 @@ function buildActivateDeepResearchExpression() {
945
1072
  // mutate the main composer and can be submitted as normal prompt text.
946
1073
  const plusBtn = document.querySelector(${plusBtnSelector}) ||
947
1074
  Array.from(document.querySelectorAll('button')).find(
948
- b => (b.getAttribute('aria-label') || '').toLowerCase().includes('add files')
1075
+ b => addFilesLabels.some(label =>
1076
+ normalizeText(b.getAttribute('aria-label') || '').includes(label)
1077
+ )
949
1078
  );
950
1079
  if (!plusBtn) return { status: 'plus-button-missing' };
951
1080
  dispatchClickSequence(plusBtn);
@@ -957,11 +1086,7 @@ function buildActivateDeepResearchExpression() {
957
1086
  const items = collectAvailableItems({ requirePopover: true });
958
1087
  if (findDeepResearchItem({ requirePopover: true }) || items.some(text => {
959
1088
  const normalized = normalizeText(text);
960
- return normalized.includes('add photos') ||
961
- normalized.includes('create image') ||
962
- normalized.includes('web search') ||
963
- normalized.includes('deep research') ||
964
- normalized.includes('get a detailed report');
1089
+ return dropdownReadyLabels.some(label => normalized.includes(label));
965
1090
  })) { resolve(items); return; }
966
1091
  elapsed += 150;
967
1092
  if (elapsed > 3000) { resolve(items.length ? items : null); return; }
@@ -1,6 +1,7 @@
1
1
  import { COMPOSER_MODEL_SIGNAL_SELECTOR, MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from "../constants.js";
2
2
  import { logDomFailure } from "../domDebug.js";
3
3
  import { buildClickDispatcher } from "./domEvents.js";
4
+ import { throwIfThrottled } from "../chatgptThrottle.js";
4
5
  import { delay } from "../utils.js";
5
6
  const LEGACY_PRO_VERSION_WORD_TOKENS = ["5 4", "5 2", "5 1", "5 0", "gpt 5 pro"];
6
7
  const LEGACY_PRO_VERSION_COMPACT_TOKENS = ["gpt54", "gpt52", "gpt51", "gpt50"];
@@ -55,6 +56,11 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
55
56
  }
56
57
  case "option-not-found": {
57
58
  await logDomFailure(Runtime, logger, "model-switcher-option");
59
+ // Check for a rate-limit notice before blaming the model name. The notice
60
+ // is a plain dialog, so its "Got it" button reads as an available option to
61
+ // the menu scrape — which turns "wait a few minutes" into "your model does
62
+ // not exist", and sends the reader after the wrong bug.
63
+ await throwIfThrottled(Runtime, { stage: "model-selection" }, logger);
58
64
  const isTemporary = result.hint?.temporaryChat ?? false;
59
65
  const available = (result.hint?.availableOptions ?? []).filter(Boolean);
60
66
  const availableHint = available.length > 0 ? ` Available: ${available.join(", ")}.` : "";
@@ -65,6 +71,7 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
65
71
  }
66
72
  default: {
67
73
  await logDomFailure(Runtime, logger, "model-switcher-button");
74
+ await throwIfThrottled(Runtime, { stage: "model-selection" }, logger);
68
75
  throw new Error("Unable to locate the ChatGPT model selector button. If the desired model is already selected in the browser, retry with --browser-model-strategy current; otherwise retry with --browser-model-strategy ignore to skip model selection.");
69
76
  }
70
77
  }
@@ -74,6 +81,15 @@ function assertResolvedModelSelection(desiredModel, resolvedLabel) {
74
81
  const resolved = resolvedLabel.toLowerCase();
75
82
  const normalizedDesired = normalizeResolvedModelLabel(desired);
76
83
  const normalizedResolved = normalizeResolvedModelLabel(resolved);
84
+ if (desired === "latest") {
85
+ // The advanced radio is localized, but only the documented exact labels are
86
+ // evidence of GPT-6 Astra. Do not let a generic picker result verify Latest.
87
+ if (resolvedLabel.normalize("NFC").trim() === "Latest" ||
88
+ resolvedLabel.normalize("NFC").trim() === "最新") {
89
+ return;
90
+ }
91
+ throw new Error(`Model picker selected "${resolvedLabel}" while "${desiredModel}" requires GPT-6 Astra (Latest).`);
92
+ }
77
93
  const wantsGpt56Sol = /(?:^| )5 6(?: |$)/.test(normalizedDesired) && normalizedDesired.split(" ").includes("sol");
78
94
  if (wantsGpt56Sol) {
79
95
  const resolvedTokens = normalizedResolved.split(" ");
@@ -162,6 +178,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
162
178
  const hasToken = (value, token) => normalizeText(value).split(' ').includes(token);
163
179
  // Normalize every candidate token to keep fuzzy matching deterministic.
164
180
  const normalizedTarget = normalizeText(PRIMARY_LABEL);
181
+ // "Latest" (GPT-6 since 2026-09) is a radio in the advanced view whose composer pill reads
182
+ // "6 Pro" / "6 High"…, while GPT-5.6 Sol's reads "5.6 Pro". Declared up front: getResolvedLabel
183
+ // runs on the picker-less path before the selection helpers below are initialized.
184
+ const targetIsLatest = normalizedTarget === 'latest';
185
+ // ChatGPT localizes the Latest radio itself (for example, Japanese "最新") but
186
+ // keeps the GPT-6 composer pill numeric. Keep this allow-list exact so GPT-5.6
187
+ // Sol or an arbitrary localized menu row can never satisfy a Latest request.
188
+ const isLatestModelLabel = (value) => {
189
+ const label = String(value ?? '').normalize('NFC').trim();
190
+ return label === 'Latest' || label === '最新';
191
+ };
165
192
  const normalizedTokens = Array.from(new Set([normalizedTarget, ...LABEL_TOKENS]))
166
193
  .map((token) => normalizeText(token))
167
194
  .filter(Boolean);
@@ -303,6 +330,14 @@ function buildModelSelectionExpression(targetModel, strategy) {
303
330
  };
304
331
 
305
332
  const getButtonLabel = () => (findModelButton()?.textContent ?? '').trim();
333
+ // With the picker closed the only evidence for "Latest" is the composer pill, so a version-less
334
+ // "latest" target must be decided on it: the blank composer signal would otherwise pass as
335
+ // "already selected" while GPT-5.6 Sol is active. Defined here, before getResolvedLabel, because
336
+ // the "current" strategy resolves the label before the selection helpers further down exist.
337
+ const latestButtonSelected = () => {
338
+ const label = normalizeText(getButtonLabel());
339
+ return /^(chatgpt |gpt )?6(?![0-9 .]*[0-9])/.test(label) && !/(^| )5 6/.test(label);
340
+ };
306
341
  const getComposerModelLabel = () =>
307
342
  (document.querySelector(COMPOSER_MODEL_SIGNAL_SELECTOR)?.textContent ?? '').trim();
308
343
  const readComposerModelSignal = () => normalizeText(getComposerModelLabel());
@@ -406,10 +441,10 @@ function buildModelSelectionExpression(targetModel, strategy) {
406
441
  const ADVANCED_VIEW_SELECTOR = '[data-testid="composer-model-picker-slider-advanced-view"]';
407
442
  const INTELLIGENCE_PICKER_SELECTOR = '[data-testid="composer-intelligence-picker-content"]';
408
443
  const SUBMENU_OPENER_SELECTOR = '[role="menuitem"][aria-haspopup="menu"]';
409
- const ADVANCED_WORDS = ['advanced', 'erweitert', '高级', 'avanzado', 'avancado', 'avance'];
410
- const MODEL_WORDS = ['model', 'modell', '模型', 'modelo', 'modello', 'modele'];
444
+ const ADVANCED_WORDS = ['advanced', 'erweitert', '高级', '고급', 'avanzado', 'avancado', 'avance'];
445
+ const MODEL_WORDS = ['model', 'modell', '模型', '모델', 'modelo', 'modello', 'modele'];
411
446
  const EFFORT_WORDS = [
412
- 'effort', 'aufwand', '强度', '努力',
447
+ 'effort', 'aufwand', '强度', '努力', '추론 수준',
413
448
  'esfuerzo', 'esforco', 'sforzo', 'inspanning', 'wysilek',
414
449
  ];
415
450
  const pickerNodeLabel = (node) => {
@@ -420,6 +455,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
420
455
  .toLowerCase()
421
456
  .normalize('NFD')
422
457
  .replace(/[\u0300-\u036f]/g, '')
458
+ .normalize('NFC')
423
459
  .replace(/\\s+/g, ' ')
424
460
  .trim();
425
461
  } catch {
@@ -473,15 +509,37 @@ function buildModelSelectionExpression(targetModel, strategy) {
473
509
  if (wantsInstant) return label.includes('instant');
474
510
  if (wantsThinking) return Boolean(desiredVersion) && !labelHasProWord(label);
475
511
  if (desiredVersion) return true;
512
+ // A version-less target ("Latest") must match the radio that is actually checked in the
513
+ // advanced view: the opener's text lists every radio label, so a substring test would
514
+ // report "Latest" as selected while GPT-5.6 Sol is the checked model.
515
+ const checkedAdvancedRadio = findCheckedAdvancedModelRadio(parentMenu);
516
+ if (checkedAdvancedRadio) {
517
+ const checkedLabel = checkedAdvancedRadio.textContent ?? '';
518
+ return targetIsLatest
519
+ ? isLatestModelLabel(checkedLabel)
520
+ : normalizedTokens.some((token) => token && normalizeText(checkedLabel) === token);
521
+ }
476
522
  return normalizedTokens.some((token) => token && label.includes(token));
477
523
  };
524
+ const findCheckedAdvancedModelRadio = (menu = null) => {
525
+ const scope = menu || findUnifiedPickerMenu() || document;
526
+ return (
527
+ scope?.querySelector?.(
528
+ '[data-testid="composer-model-picker-slider-advanced-view"] [role="menuitemradio"][aria-checked="true"]',
529
+ ) ?? null
530
+ );
531
+ };
478
532
  const getAdvancedModelLabel = () => {
479
533
  const opener = findModelSubmenuOpener(findUnifiedPickerMenu());
480
534
  if (!opener) return '';
481
535
  const raw = (opener.textContent ?? '').trim();
482
536
  const normalized = normalizeText(pickerNodeLabel(opener));
483
537
  const version = versionFromLabel(normalized);
484
- if (!version) return raw;
538
+ if (!version) {
539
+ const checkedAdvancedRadio = findCheckedAdvancedModelRadio(findUnifiedPickerMenu());
540
+ const checkedLabel = (checkedAdvancedRadio?.textContent ?? '').trim();
541
+ return checkedLabel || raw;
542
+ }
485
543
  const [major, minor] = version.split('-');
486
544
  const suffix = normalized.split(' ').includes('sol') ? ' Sol' : '';
487
545
  return 'GPT-' + major + '.' + minor + suffix;
@@ -495,6 +553,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
495
553
  );
496
554
  };
497
555
  const getResolvedLabel = (observedOptionLabel = '') => {
556
+ if (targetIsLatest) {
557
+ const checkedAdvancedRadio = findCheckedAdvancedModelRadio();
558
+ if (checkedAdvancedRadio) return (checkedAdvancedRadio.textContent ?? '').trim();
559
+ // Picker closed: the pill ("6 Pro") is the evidence; report the radio's name so callers
560
+ // can compare against the requested target instead of the tier-suffixed pill text.
561
+ if (latestButtonSelected()) return 'Latest';
562
+ const currentButtonLabel = getButtonLabel();
563
+ if (currentButtonLabel) return currentButtonLabel;
564
+ // No picker button at all (e.g. the "current" strategy on a page that hides it): fall back
565
+ // to the generic composer/observed label resolution below.
566
+ }
498
567
  if (configuredSelectionMatchesTarget()) {
499
568
  const variant = getConfiguredVariantLabel();
500
569
  const version = formatModelOptionLabel(getConfiguredVersionLabel());
@@ -636,6 +705,13 @@ function buildModelSelectionExpression(targetModel, strategy) {
636
705
  return COMPOSER_SIGNAL_INCLUDES.some((token) => token && signal.includes(token));
637
706
  };
638
707
  const activeSelectionMatchesTarget = () => {
708
+ if (targetIsLatest) {
709
+ const checkedAdvancedRadio = findCheckedAdvancedModelRadio();
710
+ if (checkedAdvancedRadio) {
711
+ return isLatestModelLabel(checkedAdvancedRadio.textContent ?? '');
712
+ }
713
+ return latestButtonSelected();
714
+ }
639
715
  if (advancedModelSignalMatchesTarget()) {
640
716
  return true;
641
717
  }
@@ -686,6 +762,8 @@ function buildModelSelectionExpression(targetModel, strategy) {
686
762
  node.getAttribute('data-composer-intelligence-pro-effort-action') === 'true' ||
687
763
  Boolean(node.closest('[data-model-picker-thinking-effort-action="true"]')) ||
688
764
  Boolean(node.closest('[data-composer-intelligence-pro-effort-action="true"]')) ||
765
+ (isUnifiedPickerMenu(menu) && isSubmenuOpener(node) &&
766
+ containsPickerWord(pickerNodeLabel(node), EFFORT_WORDS)) ||
689
767
  isDetachedProEffortMenu(menu));
690
768
  const optionIsSelected = (node) => {
691
769
  if (!(node instanceof HTMLElement)) {
@@ -708,6 +786,11 @@ function buildModelSelectionExpression(targetModel, strategy) {
708
786
 
709
787
  const scoreOption = (normalizedText, testid, node) => {
710
788
  // Assign a score to every node so we can pick the most likely match without brittle equality checks.
789
+ // Latest is localized in the advanced radio list. Match the documented labels
790
+ // exactly instead of falling through to generic scoring, which could select Sol.
791
+ if (targetIsLatest) {
792
+ return isLatestModelLabel(node?.textContent ?? '') ? 2000 : 0;
793
+ }
711
794
  if (!normalizedText && !testid) {
712
795
  return 0;
713
796
  }
@@ -1358,6 +1441,10 @@ function buildModelMatchersLiteral(targetModel) {
1358
1441
  testIdTokens.add("gpt5-6");
1359
1442
  testIdTokens.add("gpt56");
1360
1443
  }
1444
+ if (base === "latest") {
1445
+ // ChatGPT's Japanese advanced-model radio is named exactly "最新".
1446
+ push("最新", labelTokens);
1447
+ }
1361
1448
  // Numeric variations (5.5 <-> 55 <-> gpt-5-5)
1362
1449
  if (base.includes("5.5") || base.includes("5-5") || base.includes("55")) {
1363
1450
  push("5.5", labelTokens);
@@ -483,7 +483,7 @@ export async function ensurePromptReady(Runtime, timeoutMs, logger) {
483
483
  throw new Error("Prompt textarea did not appear before timeout");
484
484
  }
485
485
  }
486
- function conversationIdFromUrl(value) {
486
+ export function conversationIdFromUrl(value) {
487
487
  if (!value)
488
488
  return null;
489
489
  try {