@steipete/oracle 0.20.2 → 0.21.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 (67) hide show
  1. package/dist/bin/oracle-cli.js +14 -42
  2. package/dist/docs-site/.nojekyll +0 -0
  3. package/dist/docs-site/CNAME +1 -0
  4. package/dist/docs-site/RELEASING.html +403 -0
  5. package/dist/docs-site/advisor.html +317 -0
  6. package/dist/docs-site/agents.html +375 -0
  7. package/dist/docs-site/anthropic.html +368 -0
  8. package/dist/docs-site/bridge.html +421 -0
  9. package/dist/docs-site/browser-mode.html +636 -0
  10. package/dist/docs-site/chromium-forks.html +355 -0
  11. package/dist/docs-site/cli-reference.html +347 -0
  12. package/dist/docs-site/configuration.html +464 -0
  13. package/dist/docs-site/favicon.svg +14 -0
  14. package/dist/docs-site/followup.html +375 -0
  15. package/dist/docs-site/gemini.html +387 -0
  16. package/dist/docs-site/grok.html +325 -0
  17. package/dist/docs-site/index.html +360 -0
  18. package/dist/docs-site/install.html +335 -0
  19. package/dist/docs-site/linux.html +321 -0
  20. package/dist/docs-site/llms.txt +44 -0
  21. package/dist/docs-site/manual-tests.html +621 -0
  22. package/dist/docs-site/mcp.html +412 -0
  23. package/dist/docs-site/multimodel.html +364 -0
  24. package/dist/docs-site/mythical-pro-agents.html +360 -0
  25. package/dist/docs-site/notifier.html +338 -0
  26. package/dist/docs-site/openai-endpoints.html +417 -0
  27. package/dist/docs-site/openrouter.html +344 -0
  28. package/dist/docs-site/quickstart.html +369 -0
  29. package/dist/docs-site/refactor/ux.html +321 -0
  30. package/dist/docs-site/sessions.html +397 -0
  31. package/dist/docs-site/social-card.png +0 -0
  32. package/dist/docs-site/social-card.svg +79 -0
  33. package/dist/docs-site/spec.html +363 -0
  34. package/dist/docs-site/testing.html +323 -0
  35. package/dist/docs-site/tui-debug.html +326 -0
  36. package/dist/docs-site/windows-work.html +348 -0
  37. package/dist/docs-site/windows.html +320 -0
  38. package/dist/src/browser/actions/promptComposer.js +0 -1
  39. package/dist/src/browser/chatgptConversation.js +309 -0
  40. package/dist/src/browser/config.js +2 -0
  41. package/dist/src/browser/configLogging.js +8 -0
  42. package/dist/src/browser/executor.js +19 -0
  43. package/dist/src/browser/index.js +80 -241
  44. package/dist/src/browser/provider.js +11 -0
  45. package/dist/src/browser/reattach.js +0 -1
  46. package/dist/src/browser/reattachHelpers.js +14 -15
  47. package/dist/src/browser/sessionRunner.js +7 -4
  48. package/dist/src/browser/uiWarnings.js +202 -0
  49. package/dist/src/cli/bridge/claudeConfig.js +0 -5
  50. package/dist/src/cli/browserConfig.js +1 -0
  51. package/dist/src/cli/browserDefaults.js +4 -0
  52. package/dist/src/cli/runOptions.js +2 -2
  53. package/dist/src/cli/sessionDisplay.js +0 -1
  54. package/dist/src/cli/sessionRunner.js +1 -0
  55. package/dist/src/config.js +1 -0
  56. package/dist/src/gemini-web/executor.js +11 -7
  57. package/dist/src/mcp/tools/consult.js +1 -1
  58. package/dist/src/oracle/background.js +0 -2
  59. package/dist/src/remote/client.js +8 -0
  60. package/dist/src/remote/server.js +13 -0
  61. package/dist/src/version.js +0 -1
  62. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  63. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  64. package/package.json +7 -10
  65. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  66. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  67. package/dist/scripts/check.js +0 -21
@@ -58,6 +58,7 @@ export const DEFAULT_BROWSER_CONFIG = {
58
58
  researchMode: "off",
59
59
  archiveConversations: "auto",
60
60
  resumeConversationUrl: null,
61
+ captureProviderNative: false,
61
62
  };
62
63
  export function resolveBrowserConfig(config) {
63
64
  const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
@@ -117,6 +118,7 @@ export function resolveBrowserConfig(config) {
117
118
  researchMode,
118
119
  archiveConversations,
119
120
  resumeConversationUrl: config?.resumeConversationUrl ?? DEFAULT_BROWSER_CONFIG.resumeConversationUrl,
121
+ captureProviderNative: config?.captureProviderNative ?? DEFAULT_BROWSER_CONFIG.captureProviderNative,
120
122
  manualLogin,
121
123
  manualLoginProfileDir: manualLogin ? resolvedProfileDir : null,
122
124
  manualLoginCookieSync: config?.manualLoginCookieSync ?? DEFAULT_BROWSER_CONFIG.manualLoginCookieSync,
@@ -0,0 +1,8 @@
1
+ export function redactBrowserConfigForDebugLog(config) {
2
+ const redacted = { ...config };
3
+ if (Array.isArray(config.inlineCookies)) {
4
+ redacted.inlineCookies = `[redacted:${config.inlineCookies.length} cookies]`;
5
+ redacted.inlineCookieCount = config.inlineCookies.length;
6
+ }
7
+ return redacted;
8
+ }
@@ -0,0 +1,19 @@
1
+ import { resolveBrowserProvider } from "./provider.js";
2
+ export async function resolveLocalBrowserExecutor(options) {
3
+ const provider = resolveBrowserProvider(options.model);
4
+ if (provider === "gemini") {
5
+ const { createGeminiWebExecutor } = await import("../gemini-web/index.js");
6
+ return createGeminiWebExecutor({
7
+ youtube: options.youtube,
8
+ generateImage: options.generateImage,
9
+ editImage: options.editImage,
10
+ outputPath: options.outputPath,
11
+ aspectRatio: options.aspectRatio,
12
+ showThoughts: options.geminiShowThoughts,
13
+ allowModelFallback: options.geminiAllowModelFallback,
14
+ });
15
+ }
16
+ if (provider === "chatgpt")
17
+ return (await import("../browserMode.js")).runBrowserMode;
18
+ throw new Error(`Unsupported browser model: ${options.model}. Use a GPT or Gemini model.`);
19
+ }
@@ -6,6 +6,7 @@ import net from "node:net";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { claimBrowserTarget } from "./targetClaim.js";
8
8
  import { resolveBrowserConfig } from "./config.js";
9
+ import { redactBrowserConfigForDebugLog } from "./configLogging.js";
9
10
  import { copyChromeProfile } from "./profileCopy.js";
10
11
  import { BrowserCancellation, withoutBrowserCancellation } from "./cancellation.js";
11
12
  import { launchChrome, registerTerminationHooks, positionChromeWindowOffscreen, positionChromeWindowOnscreen, connectToRemoteChrome, connectWithNewTab, closeTab, createChromePageTarget, ensureChromePageTargetAfterClose, closeBlankChromeTabs, } from "./chromeLifecycle.js";
@@ -15,7 +16,9 @@ import { INPUT_SELECTORS } from "./constants.js";
15
16
  import { uploadAttachmentViaDataTransfer } from "./actions/remoteFileTransfer.js";
16
17
  import { ensureThinkingTime } from "./actions/thinkingTime.js";
17
18
  import { throwIfAssistantUiError } from "./actions/assistantResponse.js";
19
+ import { finalizeProviderNativeCapture, } from "./chatgptConversation.js";
18
20
  import { startThinkingStatusMonitor } from "./actions/thinkingStatus.js";
21
+ import { classifyChatGptUiWarningText, collectChatGptUiWarnings, createAssistantTimeoutError, throwChatGptUiWarningIfPresent, } from "./uiWarnings.js";
19
22
  import { activateDeepResearch, captureDeepResearchTargetKeys, waitForDeepResearchCompletion, waitForResearchPlanAutoConfirm, } from "./actions/deepResearch.js";
20
23
  import { estimateTokenCount, withRetries, delay } from "./utils.js";
21
24
  import { formatElapsed } from "../oracle/format.js";
@@ -44,17 +47,6 @@ import { extractStableConversationIdFromUrl as extractConversationIdFromUrl, isS
44
47
  export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.js";
45
48
  export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from "./utils.js";
46
49
  export { formatThinkingLog, formatThinkingWaitingLog, buildThinkingStatusExpressionForTest, readThinkingStatusForTest, sanitizeThinkingText, startThinkingStatusMonitorForTest, } from "./actions/thinkingStatus.js";
47
- function redactBrowserConfigForDebugLog(config) {
48
- const redacted = { ...config };
49
- if (Array.isArray(config.inlineCookies)) {
50
- redacted.inlineCookies = `[redacted:${config.inlineCookies.length} cookies]`;
51
- redacted.inlineCookieCount = config.inlineCookies.length;
52
- }
53
- return redacted;
54
- }
55
- export function redactBrowserConfigForDebugLogForTest(config) {
56
- return redactBrowserConfigForDebugLog(config);
57
- }
58
50
  function isCloudflareChallengeError(error) {
59
51
  if (!(error instanceof BrowserAutomationError))
60
52
  return false;
@@ -97,207 +89,6 @@ function shouldApplyThinkingTimeSelection(config) {
97
89
  // suppress an explicitly configured thinking-time selection.
98
90
  return config.thinkingTime !== undefined;
99
91
  }
100
- const MAX_CHATGPT_UI_WARNING_CHARS = 300;
101
- const MAX_CHATGPT_UI_WARNINGS = 3;
102
- function classifyChatGptUiWarningText(text) {
103
- const normalized = text.toLowerCase();
104
- if (/\btoo many requests\b/.test(normalized) ||
105
- /\bsending too many requests\b/.test(normalized) ||
106
- /\btoo quickly\b/.test(normalized) ||
107
- /\btemporarily limited access\b/.test(normalized) ||
108
- /\bplease wait a few minutes\b/.test(normalized) ||
109
- /\brate limit(?:ed)?\b/.test(normalized) ||
110
- /\bslow down\b/.test(normalized)) {
111
- return "rate_limit";
112
- }
113
- if (/\btemporarily unavailable\b/.test(normalized) ||
114
- /\bsomething went wrong\b/.test(normalized) ||
115
- /\bfailed to generate\b/.test(normalized) ||
116
- /\btry again later\b/.test(normalized)) {
117
- return "temporary_unavailable";
118
- }
119
- if (/\bverify you are human\b/.test(normalized) ||
120
- /\bunusual activity\b/.test(normalized) ||
121
- /\bcloudflare\b/.test(normalized) ||
122
- /\bchallenge\b/.test(normalized) ||
123
- /\blogin required\b/.test(normalized) ||
124
- /\bsign in\b/.test(normalized)) {
125
- return "auth_or_challenge";
126
- }
127
- return null;
128
- }
129
- function sanitizeChatGptUiWarningText(text) {
130
- return text
131
- .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[redacted-email]")
132
- .replace(/\b((?:access|auth|session)[-_ ]?token|token)\s*[:=]\s*["']?[^\s"',;]+/gi, "$1=[redacted]")
133
- .replace(/\b(?:sk-(?:ant-|or-)?|xai-)[A-Za-z0-9_-]{8,}\b/g, "[redacted-token]");
134
- }
135
- function normalizeUiWarningCandidate(value) {
136
- if (!value || typeof value !== "object")
137
- return null;
138
- const candidate = value;
139
- const text = typeof candidate.text === "string"
140
- ? sanitizeChatGptUiWarningText(candidate.text.replace(/\s+/g, " ").trim())
141
- : "";
142
- if (!text)
143
- return null;
144
- return {
145
- text: text.slice(0, MAX_CHATGPT_UI_WARNING_CHARS),
146
- source: typeof candidate.source === "string" ? candidate.source : null,
147
- role: typeof candidate.role === "string" ? candidate.role : null,
148
- ariaLive: typeof candidate.ariaLive === "string" ? candidate.ariaLive : null,
149
- selector: typeof candidate.selector === "string" ? candidate.selector : null,
150
- };
151
- }
152
- async function collectChatGptUiWarnings(Runtime) {
153
- try {
154
- const { result } = await Runtime.evaluate({
155
- awaitPromise: true,
156
- returnByValue: true,
157
- expression: `(() => {
158
- const warningPattern = /too many requests|sending too many requests|too quickly|temporarily limited access|please wait a few minutes|rate limit|rate limited|slow down|try again later|temporarily unavailable|something went wrong|failed to generate|verify you are human|unusual activity|cloudflare|challenge|login required|sign in/i;
159
- const selectors = [
160
- '[role="alert"]',
161
- '[role="status"]',
162
- '[role="dialog"]',
163
- '[aria-live]',
164
- '[data-testid*="toast" i]',
165
- '[data-testid*="banner" i]',
166
- '[data-testid*="error" i]',
167
- '[class*="toast" i]',
168
- '[class*="banner" i]'
169
- ];
170
- const isVisible = (element) => {
171
- if (!(element instanceof HTMLElement)) return false;
172
- let current = element;
173
- while (current) {
174
- const currentStyle = window.getComputedStyle(current);
175
- if (
176
- !currentStyle ||
177
- currentStyle.display === 'none' ||
178
- currentStyle.visibility === 'hidden' ||
179
- currentStyle.visibility === 'collapse' ||
180
- Number.parseFloat(currentStyle.opacity || '1') === 0
181
- ) {
182
- return false;
183
- }
184
- current = current.parentElement;
185
- }
186
- const rect = element.getBoundingClientRect();
187
- return rect.width > 0 && rect.height > 0;
188
- };
189
- const describe = (element, source, selector = null) => ({
190
- text: (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 1000),
191
- source,
192
- selector,
193
- role: element.getAttribute('role'),
194
- ariaLive: element.getAttribute('aria-live')
195
- });
196
- const out = [];
197
- const seen = new Set();
198
- const warningContainers = [];
199
- const overlapsWarningContainer = (element) => warningContainers.some((container) => (
200
- container !== element && (container.contains(element) || element.contains(container))
201
- ));
202
- const add = (element, entry) => {
203
- if (!entry.text || !warningPattern.test(entry.text)) return;
204
- const key = entry.text + '|' + (entry.role || '') + '|' + (entry.ariaLive || '');
205
- if (seen.has(key)) return;
206
- seen.add(key);
207
- warningContainers.push(element);
208
- out.push(entry);
209
- };
210
- for (const selector of selectors) {
211
- if (out.length >= 5) break;
212
- let elements = [];
213
- try {
214
- elements = Array.from(document.querySelectorAll(selector));
215
- } catch {
216
- elements = [];
217
- }
218
- for (const element of elements) {
219
- if (out.length >= 5) break;
220
- if (overlapsWarningContainer(element)) continue;
221
- if (isVisible(element)) add(element, describe(element, 'selector', selector));
222
- }
223
- }
224
- return out.slice(0, 5);
225
- })()`,
226
- });
227
- const rawWarnings = Array.isArray(result?.value) ? result.value : [];
228
- const warnings = [];
229
- const seen = new Set();
230
- for (const raw of rawWarnings) {
231
- const candidate = normalizeUiWarningCandidate(raw);
232
- if (!candidate)
233
- continue;
234
- const type = classifyChatGptUiWarningText(candidate.text);
235
- if (!type)
236
- continue;
237
- const key = `${type}:${candidate.text}`;
238
- if (seen.has(key))
239
- continue;
240
- seen.add(key);
241
- warnings.push({
242
- type,
243
- message: candidate.text,
244
- source: candidate.source,
245
- role: candidate.role,
246
- ariaLive: candidate.ariaLive,
247
- selector: candidate.selector,
248
- });
249
- if (warnings.length >= MAX_CHATGPT_UI_WARNINGS)
250
- break;
251
- }
252
- return warnings;
253
- }
254
- catch {
255
- return [];
256
- }
257
- }
258
- function formatChatGptUiWarningType(type) {
259
- switch (type) {
260
- case "rate_limit":
261
- return "rate-limit";
262
- case "temporary_unavailable":
263
- return "temporary-unavailable";
264
- case "auth_or_challenge":
265
- return "authentication/challenge";
266
- }
267
- }
268
- async function createChatGptUiWarningError(params) {
269
- const [uiWarning] = await collectChatGptUiWarnings(params.Runtime);
270
- if (!uiWarning)
271
- return null;
272
- params.logger(`[browser] ChatGPT UI warning detected (${uiWarning.type}): ${uiWarning.message}`);
273
- return new BrowserAutomationError(`ChatGPT displayed a ${formatChatGptUiWarningType(uiWarning.type)} warning while waiting for ${params.waitTarget}: ${uiWarning.message}`, {
274
- stage: params.stage,
275
- code: "chatgpt-ui-warning",
276
- uiWarning,
277
- runtime: params.runtime,
278
- diagnostics: params.diagnostics,
279
- }, params.cause);
280
- }
281
- async function throwChatGptUiWarningIfPresent(params) {
282
- const error = await createChatGptUiWarningError(params);
283
- if (error)
284
- throw error;
285
- }
286
- async function createAssistantTimeoutError(params) {
287
- const warningError = await createChatGptUiWarningError({
288
- Runtime: params.Runtime,
289
- logger: params.logger,
290
- runtime: params.runtime,
291
- stage: "assistant-timeout",
292
- waitTarget: "the assistant",
293
- diagnostics: params.diagnostics,
294
- cause: params.cause,
295
- });
296
- if (!warningError) {
297
- return new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime: params.runtime, diagnostics: params.diagnostics }, params.cause);
298
- }
299
- return warningError;
300
- }
301
92
  /**
302
93
  * Make the page behave like a focused foreground tab.
303
94
  *
@@ -653,6 +444,31 @@ function formatBrowserLeaseDiagnostics(options) {
653
444
  `launch=${options.launchDisposition ?? "unknown"}`,
654
445
  ].join("; ");
655
446
  }
447
+ /**
448
+ * Provider-native capture, gated on explicit opt-in.
449
+ *
450
+ * Off by default because it costs two extra authenticated requests per run and
451
+ * only matters when a caller intends to treat the transcript as evidence rather
452
+ * than as an answer. When it is on and it fails, the run is unaffected: the
453
+ * summary records why, and nothing throws.
454
+ */
455
+ async function runProviderNativeCapture(params) {
456
+ if (!params.config.captureProviderNative) {
457
+ return { artifacts: [] };
458
+ }
459
+ const conversationId = params.conversationUrl
460
+ ? extractConversationIdFromUrl(params.conversationUrl)
461
+ : undefined;
462
+ return finalizeProviderNativeCapture({
463
+ Runtime: params.Runtime,
464
+ conversationId,
465
+ conversationUrl: params.conversationUrl,
466
+ sessionId: params.sessionId,
467
+ answerMarkdown: params.answerMarkdown,
468
+ answerMessageId: params.answerMessageId,
469
+ logger: params.logger,
470
+ });
471
+ }
656
472
  function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
657
473
  return {
658
474
  requestedModel: desiredModel ?? null,
@@ -892,6 +708,7 @@ async function runBrowserModeInternal(options, cancellation) {
892
708
  const startedAt = Date.now();
893
709
  let answerText = "";
894
710
  let answerMarkdown = "";
711
+ let answerMessageId;
895
712
  let answerHtml = "";
896
713
  let runStatus = "attempted";
897
714
  let connectionClosedUnexpectedly = false;
@@ -1437,15 +1254,23 @@ async function runBrowserModeInternal(options, cancellation) {
1437
1254
  conversationUrl: lastUrl,
1438
1255
  logger,
1439
1256
  }), logger);
1257
+ const providerCapture = await runProviderNativeCapture({
1258
+ Runtime,
1259
+ config,
1260
+ conversationUrl: lastUrl,
1261
+ sessionId: options.sessionId,
1262
+ answerMarkdown: researchResult.text,
1263
+ logger,
1264
+ });
1440
1265
  const transcriptArtifact = await saveOptionalArtifact(() => saveBrowserTranscriptArtifact({
1441
1266
  sessionId: options.sessionId,
1442
1267
  prompt: promptText,
1443
1268
  answerMarkdown: researchResult.text,
1444
1269
  conversationUrl: lastUrl,
1445
- artifacts: appendArtifacts(undefined, [reportArtifact]),
1270
+ artifacts: appendArtifacts(appendArtifacts(undefined, [reportArtifact]), providerCapture.artifacts),
1446
1271
  logger,
1447
1272
  }), logger);
1448
- const savedArtifacts = appendArtifacts(undefined, [reportArtifact, transcriptArtifact]);
1273
+ const savedArtifacts = appendArtifacts(appendArtifacts(undefined, [reportArtifact, transcriptArtifact]), providerCapture.artifacts);
1449
1274
  const archive = await maybeArchiveCompletedConversation({
1450
1275
  Runtime,
1451
1276
  logger,
@@ -1459,6 +1284,7 @@ async function runBrowserModeInternal(options, cancellation) {
1459
1284
  answerMarkdown: researchResult.text,
1460
1285
  answerHtml: researchResult.html,
1461
1286
  artifacts: savedArtifacts,
1287
+ providerNativeCapture: providerCapture.summary,
1462
1288
  archive,
1463
1289
  modelSelection: modelSelectionEvidence,
1464
1290
  thinkingSelection: thinkingSelectionEvidence,
@@ -1771,6 +1597,7 @@ async function runBrowserModeInternal(options, cancellation) {
1771
1597
  turnAnswerMarkdown = bestText;
1772
1598
  }
1773
1599
  }
1600
+ answerMessageId = turnAnswer.meta.messageId ?? undefined;
1774
1601
  return {
1775
1602
  label,
1776
1603
  answerText: turnAnswerText,
@@ -1874,15 +1701,25 @@ async function runBrowserModeInternal(options, cancellation) {
1874
1701
  });
1875
1702
  const savedImageArtifacts = appendArtifacts(undefined, imageArtifacts.savedImages);
1876
1703
  const savedBrowserArtifacts = appendArtifacts(savedImageArtifacts, fileArtifacts.savedFiles);
1704
+ const providerCapture = await runProviderNativeCapture({
1705
+ Runtime,
1706
+ config,
1707
+ conversationUrl: lastUrl,
1708
+ sessionId: options.sessionId,
1709
+ answerMarkdown,
1710
+ answerMessageId,
1711
+ logger,
1712
+ });
1713
+ const browserArtifactsWithCapture = appendArtifacts(savedBrowserArtifacts, providerCapture.artifacts);
1877
1714
  const transcriptArtifact = await saveOptionalArtifact(() => saveBrowserTranscriptArtifact({
1878
1715
  sessionId: options.sessionId,
1879
1716
  prompt: promptText,
1880
1717
  answerMarkdown,
1881
1718
  conversationUrl: lastUrl,
1882
- artifacts: savedBrowserArtifacts,
1719
+ artifacts: browserArtifactsWithCapture,
1883
1720
  logger,
1884
1721
  }), logger);
1885
- const savedArtifacts = appendArtifacts(savedBrowserArtifacts, [transcriptArtifact]);
1722
+ const savedArtifacts = appendArtifacts(browserArtifactsWithCapture, [transcriptArtifact]);
1886
1723
  const archive = await maybeArchiveCompletedConversation({
1887
1724
  Runtime,
1888
1725
  logger,
@@ -1902,6 +1739,7 @@ async function runBrowserModeInternal(options, cancellation) {
1902
1739
  answerMarkdown,
1903
1740
  answerHtml: answerHtml.length > 0 ? answerHtml : undefined,
1904
1741
  artifacts: savedArtifacts,
1742
+ providerNativeCapture: providerCapture.summary,
1905
1743
  generatedImages: imageArtifacts.generatedImages,
1906
1744
  savedImages: imageArtifacts.savedImages,
1907
1745
  downloadableFiles: fileArtifacts.files,
@@ -2277,26 +2115,6 @@ async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answe
2277
2115
  }
2278
2116
  return { answerText, answerMarkdown };
2279
2117
  }
2280
- async function _assertNavigatedToHttp(runtime, _logger, timeoutMs = 10_000) {
2281
- const deadline = Date.now() + timeoutMs;
2282
- let lastUrl = "";
2283
- while (Date.now() < deadline) {
2284
- const { result } = await runtime.evaluate({
2285
- expression: 'typeof location === "object" && location.href ? location.href : ""',
2286
- returnByValue: true,
2287
- });
2288
- const url = typeof result?.value === "string" ? result.value : "";
2289
- lastUrl = url;
2290
- if (/^https?:\/\//i.test(url)) {
2291
- return url;
2292
- }
2293
- await delay(250);
2294
- }
2295
- throw new BrowserAutomationError("ChatGPT session not detected; page never left new tab.", {
2296
- stage: "execute-browser",
2297
- details: { url: lastUrl || "(empty)" },
2298
- });
2299
- }
2300
2118
  function detachKeptChromeProcess(chrome) {
2301
2119
  try {
2302
2120
  chrome.process?.unref();
@@ -2463,6 +2281,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2463
2281
  const startedAt = Date.now();
2464
2282
  let answerText = "";
2465
2283
  let answerMarkdown = "";
2284
+ let answerMessageId;
2466
2285
  let answerHtml = "";
2467
2286
  let connectionClosedUnexpectedly = false;
2468
2287
  let runStatus = "attempted";
@@ -2782,15 +2601,23 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2782
2601
  conversationUrl: lastUrl,
2783
2602
  logger,
2784
2603
  }), logger);
2604
+ const providerCapture = await runProviderNativeCapture({
2605
+ Runtime,
2606
+ config,
2607
+ conversationUrl: lastUrl,
2608
+ sessionId: options.sessionId,
2609
+ answerMarkdown: researchResult.text,
2610
+ logger,
2611
+ });
2785
2612
  const transcriptArtifact = await saveOptionalArtifact(() => saveBrowserTranscriptArtifact({
2786
2613
  sessionId: options.sessionId,
2787
2614
  prompt: promptText,
2788
2615
  answerMarkdown: researchResult.text,
2789
2616
  conversationUrl: lastUrl,
2790
- artifacts: appendArtifacts(undefined, [reportArtifact]),
2617
+ artifacts: appendArtifacts(appendArtifacts(undefined, [reportArtifact]), providerCapture.artifacts),
2791
2618
  logger,
2792
2619
  }), logger);
2793
- const savedArtifacts = appendArtifacts(undefined, [reportArtifact, transcriptArtifact]);
2620
+ const savedArtifacts = appendArtifacts(appendArtifacts(undefined, [reportArtifact, transcriptArtifact]), providerCapture.artifacts);
2794
2621
  const archive = await maybeArchiveCompletedConversation({
2795
2622
  Runtime,
2796
2623
  logger,
@@ -2805,6 +2632,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2805
2632
  answerMarkdown: researchResult.text,
2806
2633
  answerHtml: researchResult.html,
2807
2634
  artifacts: savedArtifacts,
2635
+ providerNativeCapture: providerCapture.summary,
2808
2636
  archive,
2809
2637
  modelSelection: modelSelectionEvidence,
2810
2638
  thinkingSelection: thinkingSelectionEvidence,
@@ -3082,6 +2910,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3082
2910
  turnAnswerMarkdown = bestText;
3083
2911
  }
3084
2912
  }
2913
+ answerMessageId = turnAnswer.meta.messageId ?? undefined;
3085
2914
  return {
3086
2915
  label,
3087
2916
  answerText: turnAnswerText,
@@ -3177,15 +3006,25 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3177
3006
  });
3178
3007
  const savedImageArtifacts = appendArtifacts(undefined, imageArtifacts.savedImages);
3179
3008
  const savedBrowserArtifacts = appendArtifacts(savedImageArtifacts, fileArtifacts.savedFiles);
3009
+ const providerCapture = await runProviderNativeCapture({
3010
+ Runtime,
3011
+ config,
3012
+ conversationUrl: lastUrl,
3013
+ sessionId: options.sessionId,
3014
+ answerMarkdown,
3015
+ answerMessageId,
3016
+ logger,
3017
+ });
3018
+ const browserArtifactsWithCapture = appendArtifacts(savedBrowserArtifacts, providerCapture.artifacts);
3180
3019
  const transcriptArtifact = await saveOptionalArtifact(() => saveBrowserTranscriptArtifact({
3181
3020
  sessionId: options.sessionId,
3182
3021
  prompt: promptText,
3183
3022
  answerMarkdown,
3184
3023
  conversationUrl: lastUrl,
3185
- artifacts: savedBrowserArtifacts,
3024
+ artifacts: browserArtifactsWithCapture,
3186
3025
  logger,
3187
3026
  }), logger);
3188
- const savedArtifacts = appendArtifacts(savedBrowserArtifacts, [transcriptArtifact]);
3027
+ const savedArtifacts = appendArtifacts(browserArtifactsWithCapture, [transcriptArtifact]);
3189
3028
  const archive = await maybeArchiveCompletedConversation({
3190
3029
  Runtime,
3191
3030
  logger,
@@ -3221,6 +3060,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3221
3060
  submittedPromptHash,
3222
3061
  ownedRecoveryTarget,
3223
3062
  artifacts: savedArtifacts,
3063
+ providerNativeCapture: providerCapture.summary,
3224
3064
  generatedImages: imageArtifacts.generatedImages,
3225
3065
  savedImages: imageArtifacts.savedImages,
3226
3066
  downloadableFiles: fileArtifacts.files,
@@ -3333,7 +3173,6 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
3333
3173
  }
3334
3174
  export { estimateTokenCount } from "./utils.js";
3335
3175
  export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from "./config.js";
3336
- // biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
3337
3176
  export const __test__ = {
3338
3177
  assertManualLoginProfileReadyForRun,
3339
3178
  closeRemoteConnectionAfterRun,
@@ -0,0 +1,11 @@
1
+ export function resolveBrowserProvider(model) {
2
+ if (typeof model !== "string")
3
+ return undefined;
4
+ const normalized = model.trim().toLowerCase();
5
+ if (normalized.startsWith("gemini"))
6
+ return "gemini";
7
+ if (normalized.startsWith("gpt-"))
8
+ return "chatgpt";
9
+ return undefined;
10
+ }
11
+ export const REMOTE_GEMINI_UNSUPPORTED_MESSAGE = "Gemini browser runs are supported locally; remote browser services support ChatGPT only.";
@@ -405,7 +405,6 @@ async function readPromptPreviewTurnIndex(Runtime, promptPreview) {
405
405
  });
406
406
  return typeof result?.value === "number" ? result.value : null;
407
407
  }
408
- // biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
409
408
  export const __test__ = {
410
409
  pickTarget,
411
410
  extractConversationIdFromUrl,
@@ -69,11 +69,9 @@ export async function openConversationFromSidebar(Runtime, options, attempt = 0)
69
69
  expression: `(() => {
70
70
  const conversationId = ${JSON.stringify(options.conversationId ?? null)};
71
71
  const preferProjects = ${JSON.stringify(Boolean(options.preferProjects))};
72
- const promptPreview = ${JSON.stringify(options.promptPreview ?? null)};
73
72
  const attemptIndex = ${Math.max(0, attempt)};
74
- const promptNeedleFull = promptPreview ? promptPreview.trim().toLowerCase().slice(0, 100) : '';
75
- const promptNeedleShort = promptNeedleFull.replace(/\\s*\\d{4,}\\s*$/, '').trim();
76
- const promptNeedles = Array.from(new Set([promptNeedleFull, promptNeedleShort].filter(Boolean)));
73
+ const promptNeedles = ${JSON.stringify(buildPromptPreviewNeedles(options.promptPreview, 100))};
74
+ const normalizeText = ${normalizeForComparison.toString()};
77
75
  const nav = document.querySelector('nav') || document.querySelector('aside') || document.body;
78
76
  if (preferProjects) {
79
77
  const projectLink = Array.from(nav.querySelectorAll('a,button'))
@@ -135,7 +133,7 @@ export async function openConversationFromSidebar(Runtime, options, attempt = 0)
135
133
  target = pick(mainCandidates.filter(byId)) || pick(navCandidates.filter(byId));
136
134
  }
137
135
  if (!target && promptNeedles.length > 0) {
138
- const byPrompt = (item) => promptNeedles.some((needle) => item.text && item.text.toLowerCase().includes(needle));
136
+ const byPrompt = (item) => promptNeedles.some((needle) => item.text && normalizeText(item.text).includes(needle));
139
137
  const sortBySpecificity = (items) =>
140
138
  items
141
139
  .filter(byPrompt)
@@ -200,14 +198,13 @@ export async function openConversationFromSidebarWithRetry(Runtime, options, tim
200
198
  return false;
201
199
  }
202
200
  export async function waitForPromptPreview(Runtime, promptPreview, timeoutMs) {
203
- const needleFull = promptPreview.trim().toLowerCase().slice(0, 120);
204
- const needleShort = needleFull.replace(/\\s*\\d{4,}\\s*$/, "").trim();
205
- const needles = Array.from(new Set([needleFull, needleShort].filter(Boolean)));
201
+ const needles = buildPromptPreviewNeedles(promptPreview, 120);
206
202
  if (needles.length === 0)
207
203
  return false;
208
204
  const selectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
209
205
  const expression = `(() => {
210
206
  const needles = ${JSON.stringify(needles)};
207
+ const normalizeText = ${normalizeForComparison.toString()};
211
208
  const root =
212
209
  document.querySelector('section[data-testid="screen-threadFlyOut"]') ||
213
210
  document.querySelector('[data-testid="chat-thread"]') ||
@@ -215,20 +212,17 @@ export async function waitForPromptPreview(Runtime, promptPreview, timeoutMs) {
215
212
  document.querySelector('[role="main"]');
216
213
  if (!root) return false;
217
214
  const userTurns = Array.from(root.querySelectorAll('[data-message-author-role="user"], [data-turn="user"]'));
218
- const collectText = (nodes) =>
215
+ const collectText = (nodes) => normalizeText(
219
216
  nodes
220
217
  .map((node) => (node.innerText || node.textContent || ''))
221
- .join(' ')
222
- .toLowerCase();
218
+ .join(' '));
223
219
  let text = collectText(userTurns);
224
- let hasTurns = userTurns.length > 0;
225
220
  if (!text) {
226
221
  const turns = Array.from(root.querySelectorAll(${selectorLiteral}));
227
- hasTurns = hasTurns || turns.length > 0;
228
222
  text = collectText(turns);
229
223
  }
230
224
  if (!text) {
231
- text = (root.innerText || root.textContent || '').toLowerCase();
225
+ text = normalizeText(root.innerText || root.textContent || '');
232
226
  }
233
227
  return needles.some((needle) => text.includes(needle));
234
228
  })()`;
@@ -282,9 +276,14 @@ export async function readConversationTurnIndex(Runtime, logger) {
282
276
  function normalizeForComparison(text) {
283
277
  return String(text || "")
284
278
  .toLowerCase()
285
- .replace(/\\s+/g, " ")
279
+ .replace(/\s+/g, " ")
286
280
  .trim();
287
281
  }
282
+ function buildPromptPreviewNeedles(promptPreview, limit) {
283
+ const full = normalizeForComparison(promptPreview ?? "").slice(0, limit);
284
+ const withoutCounter = full.replace(/\s*\d{4,}\s*$/, "").trim();
285
+ return [...new Set([full, withoutCounter].filter(Boolean))];
286
+ }
288
287
  export function buildPromptEchoMatcher(promptPreview) {
289
288
  const normalizedPrompt = normalizeForComparison(promptPreview ?? "");
290
289
  if (!normalizedPrompt) {