@steipete/oracle 0.14.0 → 0.15.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.
- package/README.md +2 -2
- package/dist/bin/oracle-cli.js +10 -8
- package/dist/docs-site/browser-mode.html +3 -2
- package/dist/docs-site/cli-reference.html +1 -1
- package/dist/docs-site/mcp.html +1 -1
- package/dist/src/browser/actions/assistantResponse.js +2 -1
- package/dist/src/browser/actions/deepResearch.js +132 -61
- package/dist/src/browser/actions/modelSelection.js +388 -30
- package/dist/src/browser/actions/thinkingTime.js +303 -65
- package/dist/src/browser/artifacts.js +2 -8
- package/dist/src/browser/chatgptFiles.js +198 -49
- package/dist/src/browser/chatgptImages.js +126 -24
- package/dist/src/browser/chromeLifecycle.js +35 -4
- package/dist/src/browser/deepResearchResult.js +23 -0
- package/dist/src/browser/index.js +145 -19
- package/dist/src/browser/profileCopy.js +93 -0
- package/dist/src/browser/projectSourcesRunner.js +2 -1
- package/dist/src/browser/prompt.js +151 -22
- package/dist/src/cli/browserConfig.js +19 -2
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/options.js +8 -0
- package/dist/src/cli/sessionRunner.js +13 -7
- package/dist/src/mcp/tools/chatgptImage.js +8 -3
- package/dist/src/mcp/tools/consult.js +9 -8
- package/dist/src/mcp/types.js +11 -2
- package/dist/src/oracle/thinkingTime.js +40 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +6 -6
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/README.md
CHANGED
|
@@ -302,8 +302,8 @@ Browser automation can open or control Chrome, so dry-runs and live runs print a
|
|
|
302
302
|
| `--browser-research deep` | Activate ChatGPT Deep Research for broad web research and cited reports (browser only). |
|
|
303
303
|
| `--browser-follow-up <prompt>` | Browser-only multi-turn consult: submit an additional prompt in the same ChatGPT conversation after the initial answer. Repeat for challenge/revision/final-decision passes. Not supported with Deep Research mode. |
|
|
304
304
|
| `--browser-archive <auto\|always\|never>` | Archive completed ChatGPT browser conversations after local artifacts are saved. `auto` archives successful one-shot chats only, and skips project, Deep Research, multi-turn, failed, and incomplete sessions. |
|
|
305
|
-
| `--browser-attachments <auto\|never\|always>` | Control browser file delivery: `auto` pastes small text files inline and uploads larger
|
|
306
|
-
| `--browser-bundle-files`, `--browser-bundle-format <text\|zip>`
|
|
305
|
+
| `--browser-attachments <auto\|never\|always>` | Control browser file delivery: `auto` pastes small text files inline and uploads larger or raw files, `never` requires inline-compatible text files, and `always` uploads files as ChatGPT attachments. |
|
|
306
|
+
| `--browser-bundle-files`, `--browser-bundle-format <auto\|text\|zip>` | Bundle browser uploads into one attachment. `auto` uses a text bundle for text-only inputs and a byte-preserving ZIP when bundled inputs include raw files; `text` writes a Markdown-style text bundle; `zip` archives the original file bytes. |
|
|
307
307
|
| `--browser-port <port>` | Pin the Chrome DevTools port (WSL/Windows firewall helper). |
|
|
308
308
|
| `--browser-inline-cookies[(-file)] <payload \| path>` | Supply cookies without Chrome/Keychain (browser). |
|
|
309
309
|
| `--browser-timeout`, `--browser-input-timeout`, `--browser-attachment-timeout` | Control overall/browser input/attachment readiness timeouts (supports h/m/s/ms). |
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { DEFAULT_MODEL, MODEL_CONFIGS } from "../src/oracle/config.js";
|
|
|
18
18
|
import { isKnownModel } from "../src/oracle/modelResolver.js";
|
|
19
19
|
import { CHATGPT_URL } from "../src/browser/constants.js";
|
|
20
20
|
import { applyHelpStyling } from "../src/cli/help.js";
|
|
21
|
-
import { collectPaths, collectModelList, collectTextValues, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
|
|
21
|
+
import { collectPaths, collectModelList, collectTextValues, parseFloatOption, parseIntOption, parseSearchOption, parseThinkingTimeOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
|
|
22
22
|
import { copyToClipboard } from "../src/cli/clipboard.js";
|
|
23
23
|
import { buildMarkdownBundle } from "../src/cli/markdownBundle.js";
|
|
24
24
|
import { shouldDetachSession } from "../src/cli/detach.js";
|
|
@@ -325,12 +325,13 @@ program
|
|
|
325
325
|
.addOption(new Option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.").hideHelp())
|
|
326
326
|
.addOption(new Option("--browser-manual-login", "Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.").hideHelp())
|
|
327
327
|
.addOption(new Option("--browser-manual-login-profile-dir <path>", "Persistent Chrome profile directory for manual-login browser runs.").hideHelp())
|
|
328
|
+
.addOption(new Option("--copy-profile <dir>", 'Copy a signed-in Chrome user-data dir to a throwaway profile and run browser mode against it (login-free; auto-cleanup). e.g. "$HOME/Library/Application Support/Google/Chrome".'))
|
|
328
329
|
.addOption(new Option("--browser-headless", "Launch Chrome in headless mode.").hideHelp())
|
|
329
330
|
.addOption(new Option("--browser-hide-window", "Hide the Chrome window after launch (macOS headful only).").hideHelp())
|
|
330
331
|
.addOption(new Option("--browser-keep-browser", "Keep Chrome running after completion.").hideHelp())
|
|
331
332
|
.addOption(new Option("--browser-model-strategy <mode>", "ChatGPT model picker strategy: select (default) switches to the requested model, current keeps the active model, ignore skips the picker entirely.").choices(["select", "current", "ignore"]))
|
|
332
|
-
.addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.")
|
|
333
|
-
.
|
|
333
|
+
.addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy, or ChatGPT UI aliases.")
|
|
334
|
+
.argParser(parseThinkingTimeOption)
|
|
334
335
|
.hideHelp())
|
|
335
336
|
.addOption(new Option("--browser-research <mode>", "Browser research mode: deep activates ChatGPT Deep Research.").choices(["off", "deep"]))
|
|
336
337
|
.addOption(new Option("--browser-archive <mode>", "Archive completed ChatGPT browser conversations after local artifacts are saved (auto archives successful non-project one-shots only).").choices(["auto", "always", "never"]))
|
|
@@ -338,7 +339,7 @@ program
|
|
|
338
339
|
.argParser(collectTextValues)
|
|
339
340
|
.default([]))
|
|
340
341
|
.addOption(new Option("--browser-allow-cookie-errors", "Continue even if Chrome cookies cannot be copied.").hideHelp())
|
|
341
|
-
.addOption(new Option("--browser-attachments <mode>", "How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never
|
|
342
|
+
.addOption(new Option("--browser-attachments <mode>", "How to deliver --file inputs in browser mode: auto (default) pastes text inline up to ~60k chars then uploads; never requires inline-compatible text files; always uploads.")
|
|
342
343
|
.choices(["auto", "never", "always"])
|
|
343
344
|
.default("auto"))
|
|
344
345
|
.addOption(new Option("--remote-chrome <host:port>", "Connect to remote Chrome DevTools Protocol, or when combined with --browser-attach-running use this host:port as the local attach hint."))
|
|
@@ -347,9 +348,9 @@ program
|
|
|
347
348
|
.addOption(new Option("--remote-token <token>", "Access token for the remote `oracle serve` instance."))
|
|
348
349
|
.addOption(new Option("--browser-inline-files", "Alias for --browser-attachments never (force pasting file contents inline).").default(false))
|
|
349
350
|
.addOption(new Option("--browser-bundle-files", "Bundle all attachments into a single archive before uploading.").default(false))
|
|
350
|
-
.addOption(new Option("--browser-bundle-format <format>", "Bundle format for browser uploads when files are bundled:
|
|
351
|
-
.choices(["text", "zip"])
|
|
352
|
-
.default("
|
|
351
|
+
.addOption(new Option("--browser-bundle-format <format>", "Bundle format for browser uploads when files are bundled: auto (default), text, or zip.")
|
|
352
|
+
.choices(["auto", "text", "zip"])
|
|
353
|
+
.default("auto"))
|
|
353
354
|
.addOption(new Option("--youtube <url>", "YouTube video URL to analyze (Gemini web/cookie mode only; uses your signed-in Chrome cookies for gemini.google.com)."))
|
|
354
355
|
.addOption(new Option("--generate-image <file>", "Generate image and save to file (Gemini browser mode; ChatGPT browser mode saves downloadable image artifacts when present)."))
|
|
355
356
|
.addOption(new Option("--edit-image <file>", "Edit existing image (Gemini browser mode; for ChatGPT attach source images with --file and use --generate-image for output)."))
|
|
@@ -689,7 +690,7 @@ function buildRunOptions(options, overrides = {}) {
|
|
|
689
690
|
"auto",
|
|
690
691
|
browserInlineFiles: overrides.browserInlineFiles ?? options.browserInlineFiles ?? false,
|
|
691
692
|
browserBundleFiles: overrides.browserBundleFiles ?? options.browserBundleFiles ?? false,
|
|
692
|
-
browserBundleFormat: overrides.browserBundleFormat ?? options.browserBundleFormat ?? "
|
|
693
|
+
browserBundleFormat: overrides.browserBundleFormat ?? options.browserBundleFormat ?? "auto",
|
|
693
694
|
generateImage: overrides.generateImage ?? options.generateImage,
|
|
694
695
|
outputPath: overrides.outputPath ?? options.output,
|
|
695
696
|
browserFollowUps: overrides.browserFollowUps ?? options.browserFollowUp ?? [],
|
|
@@ -1301,6 +1302,7 @@ async function runRootCommand(options) {
|
|
|
1301
1302
|
const { buildBrowserConfig, resolveBrowserModelLabel } = await import("../src/cli/browserConfig.js");
|
|
1302
1303
|
const config = await buildBrowserConfig({
|
|
1303
1304
|
...options,
|
|
1305
|
+
remoteHost: remoteHost ?? undefined,
|
|
1304
1306
|
model: activeModel,
|
|
1305
1307
|
browserModelLabel: resolveBrowserModelLabel(cliModelArg, activeModel),
|
|
1306
1308
|
});
|
|
@@ -336,11 +336,12 @@ body:not(.home) .doc>h1:first-child{display:none}
|
|
|
336
336
|
<li><code>--browser-port <port></code> (alias: <code>--browser-debug-port</code>; env: <code>ORACLE_BROWSER_PORT</code>/<code>ORACLE_BROWSER_DEBUG_PORT</code>): pin the DevTools port (handy on WSL/Windows firewalls). When omitted, a random open port is chosen.</li>
|
|
337
337
|
<li><code>ORACLE_CHATGPT_ACCOUNT_EMAIL</code>: exact saved-account email to select if ChatGPT shows its “Welcome back” account picker. Set it on the machine running browser automation. Oracle never logs the address; without it, Oracle selects only a single unambiguous saved account and fails closed when several are present.</li>
|
|
338
338
|
<li><code>--browser-no-cookie-sync</code>, <code>--browser-manual-login</code> (persistent automation profile + user-driven login), <code>--browser-headless</code>, <code>--browser-hide-window</code>, <code>--browser-keep-browser</code>, and the global <code>-v/--verbose</code> flag for detailed automation logs.</li>
|
|
339
|
+
<li><code>--copy-profile <dir></code>: copy a signed-in Chrome user-data directory (e.g. <code>"$HOME/Library/Application Support/Google/Chrome"</code>) to a throwaway profile and run against it, reusing your live ChatGPT session with no manual sign-in. Oracle copies the profile recorded as active in <code>Local State</code>; pass <code>--browser-chrome-profile <name></code> to select another direct child profile. The copy is launched with the real Keychain (not mocked) so its encrypted cookies decrypt, and is always deleted afterward—including setup/launch failures, incomplete captures, Cloudflare challenges, and interrupts. Copied-profile runs cannot be kept or reattached. Not compatible with <code>--browser-keep-browser</code>, <code>--browser-manual-login</code>, <code>--browser-attach-running</code>, <code>--remote-chrome</code>, or <code>--remote-host</code>, and fails fast if the required <code>Local State</code> cannot be copied. macOS/Linux; requires <code>rsync</code>.</li>
|
|
339
340
|
<li><code>--browser-url</code>: override ChatGPT base URL if needed.</li>
|
|
340
|
-
<li><code>--browser-attachments <auto|never|always></code>: control how <code>--file</code> inputs are delivered in browser mode. Default <code>auto</code> pastes
|
|
341
|
+
<li><code>--browser-attachments <auto|never|always></code>: control how <code>--file</code> inputs are delivered in browser mode. Default <code>auto</code> pastes text contents inline up to ~60k characters and uploads larger or raw files. <code>never</code> requires inline-compatible text inputs and rejects raw/binary files.</li>
|
|
341
342
|
<li><code>--browser-inline-files</code>: alias for <code>--browser-attachments never</code> (forces inline paste; never uploads attachments).</li>
|
|
342
343
|
<li><code>--browser-bundle-files</code>: bundle all resolved attachments into a single temp file before uploading (only used when uploads are enabled/selected).</li>
|
|
343
|
-
<li><code>--browser-bundle-format <text|zip></code>: choose the bundle format. <code>text</code> keeps the
|
|
344
|
+
<li><code>--browser-bundle-format <auto|text|zip></code>: choose the bundle format. <code>auto</code> uses a text bundle for text-only inputs and a byte-preserving ZIP when bundled inputs include raw files; <code>text</code> keeps the single Markdown-style text bundle; <code>zip</code> archives the original file bytes. ZIP bundle inputs are capped at 128 MiB because bundle creation is in-memory.</li>
|
|
344
345
|
<li>sqlite bindings: automatic rebuilds now require <code>ORACLE_ALLOW_SQLITE_REBUILD=1</code>. Without it, the CLI logs instructions instead of running <code>pnpm rebuild</code> on your behalf.</li>
|
|
345
346
|
<li><code>--model</code>: the same flag used for API runs is accepted, but the ChatGPT automation path supports GPT-5.5, GPT-5.4, and GPT-5.2 variants. Use <code>gpt-5.5-pro</code>, <code>gpt-5.5</code>, <code>gpt-5.5-instant</code>, <code>gpt-5.4-pro</code>, <code>gpt-5.4</code>, <code>gpt-5.2</code>, <code>gpt-5.2-thinking</code>, <code>gpt-5.2-instant</code>, or <code>gpt-5.2-pro</code>. Legacy Pro aliases still resolve to the latest Pro picker target.</li>
|
|
346
347
|
<li>Cookie sync is mandatory—if we can’t copy cookies from Chrome, the run exits early. By default Oracle copies a small ChatGPT auth/Cloudflare allowlist to avoid oversized request headers; use <code>--browser-cookie-names</code> only when you need to override that set. Use the hidden <code>--browser-allow-cookie-errors</code> flag only when you’re intentionally running logged out (it skips the early exit but still warns).</li>
|
|
@@ -264,7 +264,7 @@ body:not(.home) .doc>h1:first-child{display:none}
|
|
|
264
264
|
<table><thead><tr><th>Flag</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--base-url <url></code></td><td>LiteLLM / Azure / OpenRouter / proxy.</td></tr><tr><td><code>--provider <mode></code></td><td>API route: <code>auto</code>, <code>openai</code>, or <code>azure</code>.</td></tr><tr><td><code>--no-azure</code></td><td>Ignore Azure env/config for this run.</td></tr><tr><td><code>--route</code></td><td>Print redacted API route plan, then exit.</td></tr><tr><td><code>--azure-endpoint</code></td><td>Azure OpenAI endpoint.</td></tr><tr><td><code>--azure-deployment</code></td><td>Azure deployment name.</td></tr><tr><td><code>--azure-api-version</code></td><td>Azure API version.</td></tr></tbody></table>
|
|
265
265
|
<p>See <a href="openai-endpoints.html">OpenAI / Azure / OpenRouter</a> and <a href="openrouter.html">OpenRouter</a>.</p>
|
|
266
266
|
<h2 id="browser-mode"><a class="anchor" href="#browser-mode" aria-label="Anchor link">#</a>Browser mode</h2>
|
|
267
|
-
<table><thead><tr><th>Flag</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--chatgpt-url <url></code></td><td>Target a ChatGPT workspace / project folder.</td></tr><tr><td><code>--browser-model-strategy <select|current|ignore></code></td><td>Control ChatGPT model picker.</td></tr><tr><td><code>--browser-manual-login</code></td><td>Use persistent profile + manual login (no Keychain).</td></tr><tr><td><code>--browser-attach-running</code></td><td>Attach to your already-running Chrome via DevTools.</td></tr><tr><td><code>--browser-tab <ref></code></td><td>Reuse an existing tab (<code>current</code>, id, URL, title substring).</td></tr><tr><td><code>--browser-thinking-time <light|standard|extended|heavy></code></td><td>Pro / Thinking model intensity.</td></tr><tr><td><code>--browser-research deep</code></td><td>Activate Deep Research mode.</td></tr><tr><td><code>--browser-follow-up <prompt></code></td><td>Multi-turn in the same ChatGPT conversation.</td></tr><tr><td><code>--browser-port <port></code></td><td>Pin Chrome DevTools port.</td></tr><tr><td><code>--browser-inline-cookies[(-file)] <…></code></td><td>Supply cookies inline (no Keychain / Chrome).</td></tr><tr><td><code>--browser-timeout</code>, <code>--browser-input-timeout</code>, <code>--browser-attachment-timeout</code></td><td>Overall / input / attachment readiness timeouts (h/m/s/ms).</td></tr><tr><td><code>--browser-recheck-delay</code>, <code>--browser-recheck-timeout</code></td><td>Delayed retry after a timeout.</td></tr><tr><td><code>--browser-auto-reattach-delay/-interval/-timeout</code></td><td>Poll the existing tab when ChatGPT redirects mid-load.</td></tr><tr><td><code>--browser-reuse-wait</code></td><td>Wait for shared Chrome profile before launching.</td></tr><tr><td><code>--browser-profile-lock-timeout</code></td><td>Wait for the manual-login profile lock.</td></tr><tr><td><code>--browser-max-concurrent-tabs</code></td><td>Soft limit for shared-profile parallel runs (default 3).</td></tr><tr><td><code>--browser-keep-browser</code></td><td>Keep the browser open after the run.</td></tr><tr><td><code>--browser-headless</code>, <code>--browser-hide-window</code></td><td>Visibility controls.</td></tr><tr><td><code>--browser-attachments <auto|never|always></code></td><td>Attach files inline vs upload.</td></tr><tr><td><code>--browser-bundle-files</code>, <code>--browser-bundle-format <text|zip></code></td><td>Bundle browser uploads as text or ZIP.</td></tr><tr><td><code>--browser-chrome-path</code>, <code>--browser-cookie-path</code></td><td>Override Chrome / cookie store discovery (Linux / Windows).</td></tr></tbody></table>
|
|
267
|
+
<table><thead><tr><th>Flag</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--chatgpt-url <url></code></td><td>Target a ChatGPT workspace / project folder.</td></tr><tr><td><code>--browser-model-strategy <select|current|ignore></code></td><td>Control ChatGPT model picker.</td></tr><tr><td><code>--browser-manual-login</code></td><td>Use persistent profile + manual login (no Keychain).</td></tr><tr><td><code>--browser-attach-running</code></td><td>Attach to your already-running Chrome via DevTools.</td></tr><tr><td><code>--browser-tab <ref></code></td><td>Reuse an existing tab (<code>current</code>, id, URL, title substring).</td></tr><tr><td><code>--browser-thinking-time <light|standard|extended|heavy></code></td><td>Pro / Thinking model intensity.</td></tr><tr><td><code>--browser-research deep</code></td><td>Activate Deep Research mode.</td></tr><tr><td><code>--browser-follow-up <prompt></code></td><td>Multi-turn in the same ChatGPT conversation.</td></tr><tr><td><code>--browser-port <port></code></td><td>Pin Chrome DevTools port.</td></tr><tr><td><code>--browser-inline-cookies[(-file)] <…></code></td><td>Supply cookies inline (no Keychain / Chrome).</td></tr><tr><td><code>--browser-timeout</code>, <code>--browser-input-timeout</code>, <code>--browser-attachment-timeout</code></td><td>Overall / input / attachment readiness timeouts (h/m/s/ms).</td></tr><tr><td><code>--browser-recheck-delay</code>, <code>--browser-recheck-timeout</code></td><td>Delayed retry after a timeout.</td></tr><tr><td><code>--browser-auto-reattach-delay/-interval/-timeout</code></td><td>Poll the existing tab when ChatGPT redirects mid-load.</td></tr><tr><td><code>--browser-reuse-wait</code></td><td>Wait for shared Chrome profile before launching.</td></tr><tr><td><code>--browser-profile-lock-timeout</code></td><td>Wait for the manual-login profile lock.</td></tr><tr><td><code>--browser-max-concurrent-tabs</code></td><td>Soft limit for shared-profile parallel runs (default 3).</td></tr><tr><td><code>--browser-keep-browser</code></td><td>Keep the browser open after the run.</td></tr><tr><td><code>--browser-headless</code>, <code>--browser-hide-window</code></td><td>Visibility controls.</td></tr><tr><td><code>--browser-attachments <auto|never|always></code></td><td>Attach files inline vs upload.</td></tr><tr><td><code>--browser-bundle-files</code>, <code>--browser-bundle-format <auto|text|zip></code></td><td>Bundle browser uploads as text or byte-preserving ZIP.</td></tr><tr><td><code>--browser-chrome-path</code>, <code>--browser-cookie-path</code></td><td>Override Chrome / cookie store discovery (Linux / Windows).</td></tr></tbody></table>
|
|
268
268
|
<p>See <a href="browser-mode.html">Browser Mode</a> for usage.</p>
|
|
269
269
|
<h2 id="remote-browser"><a class="anchor" href="#remote-browser" aria-label="Anchor link">#</a>Remote browser</h2>
|
|
270
270
|
<table><thead><tr><th>Flag</th><th>Purpose</th></tr></thead><tbody><tr><td><code>--remote-host <host:port></code></td><td>Use a remote <code>oracle serve</code> host.</td></tr><tr><td><code>--remote-token <secret></code></td><td>Auth for the remote host.</td></tr><tr><td><code>--remote-chrome <host:port></code></td><td>Attach to an existing remote Chrome session.</td></tr></tbody></table>
|
package/dist/docs-site/mcp.html
CHANGED
|
@@ -266,7 +266,7 @@ body:not(.home) .doc>h1:first-child{display:none}
|
|
|
266
266
|
<ul>
|
|
267
267
|
<li>Inputs: <code>prompt</code> (required), <code>files?: string[]</code> (globs), <code>model?: string</code> (defaults to CLI), <code>engine?: "api" | "browser"</code> (optional; Oracle follows CLI defaults: <code>ORACLE_ENGINE</code> and the effective config first, then API when <code>OPENAI_API_KEY</code> is set, otherwise browser), <code>slug?: string</code>.</li>
|
|
268
268
|
<li>Presets: <code>preset?: "chatgpt-pro-heavy"</code> applies browser mode + current Pro model alias + extended thinking, unless the request overrides those fields.</li>
|
|
269
|
-
<li>Browser-only extras: <code>browserAttachments?: "auto"|"never"|"always"</code>, <code>browserBundleFiles?: boolean</code>, <code>browserBundleFormat?: "text"|"zip"</code>, <code>browserThinkingTime?: "light"|"standard"|"extended"|"heavy"</code>, <code>browserResearchMode?: "deep"</code>, <code>browserFollowUps?: string[]</code>, <code>browserArchive?: "auto"|"always"|"never"</code>, <code>browserKeepBrowser?: boolean</code>, <code>browserModelLabel?: string</code>, <code>browserModelStrategy?: "select"|"current"|"ignore"</code>, <code>generateImage?: string</code>, <code>outputPath?: string</code>.</li>
|
|
269
|
+
<li>Browser-only extras: <code>browserAttachments?: "auto"|"never"|"always"</code>, <code>browserBundleFiles?: boolean</code>, <code>browserBundleFormat?: "auto"|"text"|"zip"</code>, <code>browserThinkingTime?: "light"|"standard"|"extended"|"heavy"</code>, <code>browserResearchMode?: "deep"</code>, <code>browserFollowUps?: string[]</code>, <code>browserArchive?: "auto"|"always"|"never"</code>, <code>browserKeepBrowser?: boolean</code>, <code>browserModelLabel?: string</code>, <code>browserModelStrategy?: "select"|"current"|"ignore"</code>, <code>generateImage?: string</code>, <code>outputPath?: string</code>.</li>
|
|
270
270
|
<li>Dry runs: set <code>dryRun: true</code> to preview the resolved request without creating a session or touching the browser.</li>
|
|
271
271
|
<li>Behavior: starts a session, runs it with the chosen engine, returns final output + metadata. Background/foreground follows the CLI (e.g., GPT‑5 Pro detaches by default). If API mode fails because <code>OPENAI_API_KEY</code> is missing and you have ChatGPT Pro, retry with <code>engine: "browser"</code> or <code>preset: "chatgpt-pro-heavy"</code> to use your signed-in ChatGPT session instead of an API key.</li>
|
|
272
272
|
<li>Logging: emits MCP logs (<code>info</code> per line, <code>debug</code> for streamed chunks with byte sizes). If browser prerequisites are missing, returns an error payload instead of running.</li>
|
|
@@ -857,7 +857,8 @@ function buildAssistantExtractor(functionName) {
|
|
|
857
857
|
!normalizedText ||
|
|
858
858
|
normalizedText === 'edit' ||
|
|
859
859
|
normalizedText === 'stopped thinking' ||
|
|
860
|
-
normalizedText === 'stopped thinking edit'
|
|
860
|
+
normalizedText === 'stopped thinking edit' ||
|
|
861
|
+
/^thought for \\d+(?:\\.\\d+)?\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\\s+edit$/.test(normalizedText);
|
|
861
862
|
if (generatedImages.length > 0 && imageOnlyChrome) {
|
|
862
863
|
const label = generatedImages.length === 1 ? 'Generated image.' : \`Generated \${generatedImages.length} images.\`;
|
|
863
864
|
return { text: label, html: messageRoot?.innerHTML ?? html, messageId, turnId, turnIndex: index };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEEP_RESEARCH_PLUS_BUTTON, DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, DEEP_RESEARCH_PILL_LABEL, DEEP_RESEARCH_POLL_INTERVAL_MS, DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, FINISHED_ACTIONS_SELECTOR, STOP_BUTTON_SELECTOR, CONVERSATION_TURN_SELECTOR, } from "../constants.js";
|
|
2
2
|
import { delay } from "../utils.js";
|
|
3
|
+
import { isDeepResearchIncompleteText } from "../deepResearchResult.js";
|
|
3
4
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
4
5
|
import { captureAssistantMarkdown, readAssistantSnapshot } from "./assistantResponse.js";
|
|
5
6
|
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
@@ -116,7 +117,10 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
|
|
|
116
117
|
: -1;
|
|
117
118
|
const scopedToNewTurns = minTurnLiteral >= 0;
|
|
118
119
|
const ignoredTargetKeys = new Set(options?.ignoredTargetKeys ?? []);
|
|
119
|
-
const requireScopedTargetOwner = options?.requireScopedTargetOwner === true
|
|
120
|
+
const requireScopedTargetOwner = options?.requireScopedTargetOwner === true ||
|
|
121
|
+
(scopedToNewTurns && options?.targetBaselineCaptured !== true);
|
|
122
|
+
let observedResearchEvidence = false;
|
|
123
|
+
let loggedIncompleteResult = false;
|
|
120
124
|
logger(`Monitoring Deep Research (timeout: ${Math.round(timeoutMs / 60_000)}min)...`);
|
|
121
125
|
while (Date.now() - start < timeoutMs) {
|
|
122
126
|
const { result } = await Runtime.evaluate({
|
|
@@ -127,7 +131,6 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
|
|
|
127
131
|
if (val?.accountBlocked) {
|
|
128
132
|
throw new BrowserAutomationError("ChatGPT account security block detected during Deep Research. Open chatgpt.com in Chrome, secure the account, then rerun Oracle.", { stage: "chatgpt-account-blocked", code: "chatgpt-account-blocked" });
|
|
129
133
|
}
|
|
130
|
-
const activeScopedResearch = Boolean(val?.hasActiveScopedResearch);
|
|
131
134
|
// ChatGPT renders the Deep Research report inside an out-of-process,
|
|
132
135
|
// sandboxed iframe (connector_openai_deep_research.*.oaiusercontent.com),
|
|
133
136
|
// doubly nested and same-origin. That OOPIF does NOT appear in the main
|
|
@@ -136,24 +139,27 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
|
|
|
136
139
|
// (readDeepResearchTargetResult) attaches to the iframe's own CDP target and
|
|
137
140
|
// walks its nested frames, so it CAN read the report. Prefer the target path
|
|
138
141
|
// and fall back to the in-page frame path for legacy/inline rendering.
|
|
139
|
-
const
|
|
142
|
+
const rawTargetResult = client
|
|
140
143
|
? ((await readDeepResearchTargetResult(client, ignoredTargetKeys, requireScopedTargetOwner ? minTurnLiteral : -1).catch(() => null))?.read ?? null)
|
|
141
144
|
: null;
|
|
145
|
+
const targetResult = filterIncompleteDeepResearchRead(rawTargetResult);
|
|
142
146
|
// A completed target read is authoritative. If the target read is missing or
|
|
143
147
|
// only in-progress, still try the in-page frame path so an incomplete target
|
|
144
148
|
// read does not suppress a completed report there (legacy/inline rendering).
|
|
145
|
-
const
|
|
146
|
-
? await readDeepResearchFrameResult(Runtime, Page).catch(() => null)
|
|
149
|
+
const inPageScan = !targetResult?.completed && Page
|
|
150
|
+
? await readDeepResearchFrameResult(Runtime, Page, client, scopedToNewTurns ? minTurnLiteral : -1).catch(() => null)
|
|
147
151
|
: null;
|
|
152
|
+
const rawInPageResult = inPageScan?.read ?? null;
|
|
153
|
+
const inPageResult = filterIncompleteDeepResearchRead(rawInPageResult);
|
|
148
154
|
const read = pickPreferredDeepResearchRead(targetResult, inPageResult);
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// Target keys captured before submission are ignored, so a target result is
|
|
156
|
+
// tied to this run. Main-page iframes are not: old reports can remain in the
|
|
157
|
+
// conversation and must never authorize a new normal-response fallback.
|
|
158
|
+
observedResearchEvidence ||= Boolean(rawTargetResult ||
|
|
159
|
+
(scopedToNewTurns && rawInPageResult) ||
|
|
160
|
+
val?.researchActivity ||
|
|
161
|
+
val?.hasActiveScopedResearch);
|
|
162
|
+
if (read?.completed && read.text) {
|
|
157
163
|
logger(`Deep Research completed (${Math.round((Date.now() - start) / 1000)}s elapsed)`);
|
|
158
164
|
return {
|
|
159
165
|
text: read.text,
|
|
@@ -163,9 +169,18 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
|
|
|
163
169
|
}
|
|
164
170
|
// Completion detected
|
|
165
171
|
if (val?.finished) {
|
|
172
|
+
if (!observedResearchEvidence) {
|
|
173
|
+
throw new BrowserAutomationError("ChatGPT returned a completed response without starting Deep Research. The Deep Research selection may have silently fallen back to a normal response.", { stage: "deep-research-not-started", code: "deep-research-not-started" });
|
|
174
|
+
}
|
|
166
175
|
logger(`Deep Research completed (${Math.round((Date.now() - start) / 1000)}s elapsed)`);
|
|
167
176
|
return await extractDeepResearchResult(Runtime, logger, minTurnIndex ?? undefined);
|
|
168
177
|
}
|
|
178
|
+
const incompleteFrameResult = Boolean((rawTargetResult?.completed && !targetResult?.completed) ||
|
|
179
|
+
(rawInPageResult?.completed && !inPageResult?.completed));
|
|
180
|
+
if ((val?.incompleteResult || incompleteFrameResult) && !loggedIncompleteResult) {
|
|
181
|
+
logger("Deep Research interim status detected; waiting for the final report");
|
|
182
|
+
loggedIncompleteResult = true;
|
|
183
|
+
}
|
|
169
184
|
// Progress logging every 60 seconds
|
|
170
185
|
const now = Date.now();
|
|
171
186
|
if (now - lastLogTime >= 60_000) {
|
|
@@ -204,24 +219,26 @@ export async function extractDeepResearchResult(Runtime, logger, minTurnIndex) {
|
|
|
204
219
|
};
|
|
205
220
|
// Try the copy-button approach first for clean markdown
|
|
206
221
|
const markdown = await captureAssistantMarkdown(Runtime, meta, logger);
|
|
207
|
-
if (markdown && !
|
|
222
|
+
if (markdown && !isDeepResearchIncompleteText(markdown)) {
|
|
208
223
|
return { text: markdown, html: snapshot?.html ?? undefined, meta };
|
|
209
224
|
}
|
|
210
225
|
// Fall back to snapshot text
|
|
211
|
-
if (snapshot?.text && !
|
|
226
|
+
if (snapshot?.text && !isDeepResearchIncompleteText(snapshot.text)) {
|
|
212
227
|
return { text: snapshot.text, html: snapshot.html ?? undefined, meta };
|
|
213
228
|
}
|
|
214
229
|
throw new BrowserAutomationError("Deep Research completed but failed to extract the response text.", { stage: "deep-research-extract", code: "extraction-failed" });
|
|
215
230
|
}
|
|
216
|
-
function isDeepResearchPlaceholderText(text) {
|
|
217
|
-
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
218
|
-
return (normalized === "called tool" ||
|
|
219
|
-
normalized === "used tool" ||
|
|
220
|
-
normalized === "użyto narzędzia" ||
|
|
221
|
-
normalized === "narzędzie wywołane");
|
|
222
|
-
}
|
|
223
231
|
export function isDeepResearchPlaceholderTextForTest(text) {
|
|
224
|
-
return
|
|
232
|
+
return isDeepResearchIncompleteText(text);
|
|
233
|
+
}
|
|
234
|
+
function filterIncompleteDeepResearchRead(result) {
|
|
235
|
+
if (!result?.completed || !result.text || !isDeepResearchIncompleteText(result.text)) {
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
return { ...result, completed: false, inProgress: true };
|
|
239
|
+
}
|
|
240
|
+
export function filterIncompleteDeepResearchReadForTest(result) {
|
|
241
|
+
return filterIncompleteDeepResearchRead(result);
|
|
225
242
|
}
|
|
226
243
|
/**
|
|
227
244
|
* Choose the authoritative Deep Research read between the target-attach result
|
|
@@ -243,37 +260,64 @@ function pickPreferredDeepResearchRead(targetResult, inPageResult) {
|
|
|
243
260
|
export function pickPreferredDeepResearchReadForTest(targetResult, inPageResult) {
|
|
244
261
|
return pickPreferredDeepResearchRead(targetResult, inPageResult);
|
|
245
262
|
}
|
|
246
|
-
async function readDeepResearchFrameResult(Runtime, Page) {
|
|
263
|
+
async function readDeepResearchFrameResult(Runtime, Page, client, minTurnIndex = -1) {
|
|
247
264
|
const pageWithFrames = Page;
|
|
248
265
|
if (typeof pageWithFrames.getFrameTree !== "function" ||
|
|
249
266
|
typeof pageWithFrames.createIsolatedWorld !== "function") {
|
|
250
267
|
return null;
|
|
251
268
|
}
|
|
252
269
|
const frameTree = (await pageWithFrames.getFrameTree())?.frameTree;
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
270
|
+
const frameIds = collectPageDeepResearchFrameIds(frameTree);
|
|
271
|
+
if (frameIds.length === 0) {
|
|
255
272
|
return null;
|
|
256
273
|
}
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (typeof world.executionContextId !== "number") {
|
|
263
|
-
return null;
|
|
274
|
+
const rawClient = client;
|
|
275
|
+
if (minTurnIndex >= 0) {
|
|
276
|
+
if (typeof rawClient?.send !== "function") {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
264
279
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
280
|
+
let best = null;
|
|
281
|
+
for (const frameId of frameIds) {
|
|
282
|
+
let ownerTurnIndex = null;
|
|
283
|
+
if (minTurnIndex >= 0 && rawClient?.send) {
|
|
284
|
+
ownerTurnIndex = await readDeepResearchTargetOwnerTurnIndex(rawClient, frameId, rawClient.oraclePageSessionId);
|
|
285
|
+
if (ownerTurnIndex === null || ownerTurnIndex < minTurnIndex) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const world = await pageWithFrames.createIsolatedWorld({
|
|
290
|
+
frameId,
|
|
291
|
+
worldName: "oracle-deep-research",
|
|
292
|
+
grantUniveralAccess: true,
|
|
293
|
+
});
|
|
294
|
+
if (typeof world.executionContextId !== "number") {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const { result } = await Runtime.evaluate({
|
|
298
|
+
expression: buildDeepResearchFrameStatusExpression(),
|
|
299
|
+
contextId: world.executionContextId,
|
|
300
|
+
returnByValue: true,
|
|
301
|
+
});
|
|
302
|
+
const read = result?.value ?? null;
|
|
303
|
+
if (!read) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
best = { read, ownerTurnIndex };
|
|
307
|
+
if (read.completed) {
|
|
308
|
+
return best;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return best;
|
|
271
312
|
}
|
|
272
313
|
async function readDeepResearchTargetResult(client, ignoredTargetKeys = new Set(), minTurnIndex = -1) {
|
|
273
314
|
const rawClient = client;
|
|
274
315
|
if (typeof rawClient.send !== "function") {
|
|
275
316
|
return null;
|
|
276
317
|
}
|
|
318
|
+
if (typeof client.on !== "function") {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
277
321
|
// On the browser-WSEndpoint path, `client` is a session-bound wrapper whose
|
|
278
322
|
// domain methods target the page session but whose raw `send` is the
|
|
279
323
|
// browser-level send. We must therefore pass the page session id explicitly so
|
|
@@ -299,7 +343,7 @@ async function readDeepResearchTargetResult(client, ignoredTargetKeys = new Set(
|
|
|
299
343
|
ownedSessionIds.add(eventSessionId);
|
|
300
344
|
}
|
|
301
345
|
};
|
|
302
|
-
client.on
|
|
346
|
+
client.on("Target.attachedToTarget", onAttached);
|
|
303
347
|
try {
|
|
304
348
|
// Scope discovery to the current Oracle-controlled page. `client` is
|
|
305
349
|
// connected to the conversation page target, so enabling auto-attach on this
|
|
@@ -311,13 +355,16 @@ async function readDeepResearchTargetResult(client, ignoredTargetKeys = new Set(
|
|
|
311
355
|
// would surface another tab's completed Deep Research report and let it be
|
|
312
356
|
// saved into the current session (cross-tab leak). Only auto-attached,
|
|
313
357
|
// page-scoped sessions are treated as belonging to this run.
|
|
314
|
-
await rawClient
|
|
358
|
+
const autoAttachEnabled = await rawClient
|
|
315
359
|
.send("Target.setAutoAttach", {
|
|
316
360
|
autoAttach: true,
|
|
317
361
|
waitForDebuggerOnStart: false,
|
|
318
362
|
flatten: true,
|
|
319
363
|
}, pageSessionId)
|
|
320
|
-
.
|
|
364
|
+
.then(() => true, () => false);
|
|
365
|
+
if (!autoAttachEnabled) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
321
368
|
await delay(100);
|
|
322
369
|
if (minTurnIndex >= 0) {
|
|
323
370
|
await rawClient.send("DOM.enable", {}, pageSessionId).catch(() => undefined);
|
|
@@ -374,7 +421,11 @@ async function readDeepResearchTargetResult(client, ignoredTargetKeys = new Set(
|
|
|
374
421
|
}
|
|
375
422
|
}
|
|
376
423
|
export async function captureDeepResearchTargetKeys(client) {
|
|
377
|
-
|
|
424
|
+
const scan = await readDeepResearchTargetResult(client);
|
|
425
|
+
if (!scan) {
|
|
426
|
+
throw new Error("Deep Research target baseline capture unavailable");
|
|
427
|
+
}
|
|
428
|
+
return scan.targetKeys;
|
|
378
429
|
}
|
|
379
430
|
async function readDeepResearchTargetOwnerTurnIndex(rawClient, frameId, pageSessionId) {
|
|
380
431
|
const owner = (await rawClient
|
|
@@ -419,7 +470,7 @@ async function readDeepResearchTargetSession(rawClient, sessionId, targetUrl) {
|
|
|
419
470
|
const frameTree = (await rawClient
|
|
420
471
|
.send("Page.getFrameTree", {}, sessionId)
|
|
421
472
|
.catch(() => null));
|
|
422
|
-
const
|
|
473
|
+
const ownerFrameId = frameTree?.frameTree?.frame?.id;
|
|
423
474
|
if (!isConfirmedDeepResearchTarget(targetUrl, frameTree?.frameTree)) {
|
|
424
475
|
return { confirmed: false, read: null };
|
|
425
476
|
}
|
|
@@ -438,7 +489,7 @@ async function readDeepResearchTargetSession(rawClient, sessionId, targetUrl) {
|
|
|
438
489
|
}
|
|
439
490
|
const value = await evaluateDeepResearchFrameStatus(rawClient, sessionId, world.executionContextId);
|
|
440
491
|
if (value?.completed) {
|
|
441
|
-
return { confirmed: true, read: value, frameId };
|
|
492
|
+
return { confirmed: true, read: value, frameId: ownerFrameId };
|
|
442
493
|
}
|
|
443
494
|
if ((value?.textLength ?? 0) > (best?.textLength ?? 0) || value?.inProgress) {
|
|
444
495
|
best = value;
|
|
@@ -446,12 +497,12 @@ async function readDeepResearchTargetSession(rawClient, sessionId, targetUrl) {
|
|
|
446
497
|
}
|
|
447
498
|
const topFrameValue = await evaluateDeepResearchFrameStatus(rawClient, sessionId);
|
|
448
499
|
if (topFrameValue?.completed) {
|
|
449
|
-
return { confirmed: true, read: topFrameValue, frameId };
|
|
500
|
+
return { confirmed: true, read: topFrameValue, frameId: ownerFrameId };
|
|
450
501
|
}
|
|
451
502
|
if ((topFrameValue?.textLength ?? 0) > (best?.textLength ?? 0) || topFrameValue?.inProgress) {
|
|
452
503
|
best = topFrameValue;
|
|
453
504
|
}
|
|
454
|
-
return { confirmed: true, read: best, frameId };
|
|
505
|
+
return { confirmed: true, read: best, frameId: ownerFrameId };
|
|
455
506
|
}
|
|
456
507
|
async function evaluateDeepResearchFrameStatus(rawClient, sessionId, contextId) {
|
|
457
508
|
const response = (await rawClient
|
|
@@ -474,21 +525,20 @@ function isDeepResearchFrameDescriptor(url, name = "") {
|
|
|
474
525
|
return (descriptor.includes("connector_openai_deep_research") || descriptor.includes("deep-research"));
|
|
475
526
|
}
|
|
476
527
|
function findDeepResearchFrameId(tree) {
|
|
528
|
+
return collectPageDeepResearchFrameIds(tree)[0] ?? null;
|
|
529
|
+
}
|
|
530
|
+
function collectPageDeepResearchFrameIds(tree) {
|
|
477
531
|
if (!tree?.frame) {
|
|
478
|
-
return
|
|
532
|
+
return [];
|
|
479
533
|
}
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return tree.frame.id ?? null;
|
|
534
|
+
const ids = [];
|
|
535
|
+
if (tree.frame.id && isDeepResearchFrameDescriptor(tree.frame.url ?? "", tree.frame.name ?? "")) {
|
|
536
|
+
ids.push(tree.frame.id);
|
|
484
537
|
}
|
|
485
538
|
for (const child of tree.childFrames ?? []) {
|
|
486
|
-
|
|
487
|
-
if (match) {
|
|
488
|
-
return match;
|
|
489
|
-
}
|
|
539
|
+
ids.push(...collectPageDeepResearchFrameIds(child));
|
|
490
540
|
}
|
|
491
|
-
return
|
|
541
|
+
return ids;
|
|
492
542
|
}
|
|
493
543
|
function collectDeepResearchFrameIds(tree) {
|
|
494
544
|
if (!tree?.frame) {
|
|
@@ -644,20 +694,41 @@ function buildDeepResearchCompletionPollExpression(minTurnIndex) {
|
|
|
644
694
|
const text = (lastTurn?.textContent || '').trim();
|
|
645
695
|
const normalized = text.toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
646
696
|
const textLength = text.length;
|
|
697
|
+
const lines = text.split(/\\n+/).map(line => line.trim()).filter(Boolean);
|
|
698
|
+
const tailIsPlanningPanel = text.length <= 1500 &&
|
|
699
|
+
lines.length >= 4 &&
|
|
700
|
+
lines.length <= 20 &&
|
|
701
|
+
/^update$/i.test(lines[1] || '') &&
|
|
702
|
+
/^stop research$/i.test(lines[lines.length - 1] || '') &&
|
|
703
|
+
/^determining steps for creating a report(?:\\.\\.\\.)?$/i.test(lines[lines.length - 2] || '');
|
|
647
704
|
const isToolStub = normalized === 'called tool' ||
|
|
648
705
|
normalized === 'used tool' ||
|
|
649
706
|
normalized === 'użyto narzędzia' ||
|
|
650
707
|
normalized === 'narzędzie wywołane';
|
|
708
|
+
const incompleteResult = isToolStub ||
|
|
709
|
+
normalized === 'planning' ||
|
|
710
|
+
normalized === 'researching' ||
|
|
711
|
+
normalized === 'searching the web' ||
|
|
712
|
+
(text.trimStart().startsWith('<system-reminder>') &&
|
|
713
|
+
/<system-reminder>[\\s\\S]*#\\s*plan mode\\b/i.test(text)) ||
|
|
714
|
+
tailIsPlanningPanel;
|
|
651
715
|
const finished = Boolean(lastTurn?.querySelector(${finishedSelector})) &&
|
|
652
716
|
textLength >= 40 &&
|
|
653
|
-
!
|
|
717
|
+
!incompleteResult;
|
|
654
718
|
const hasIframe = Array.from(document.querySelectorAll('iframe')).some(f => {
|
|
655
719
|
const rect = f.getBoundingClientRect();
|
|
656
720
|
return rect.width > 200 && rect.height > 200;
|
|
657
721
|
});
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
722
|
+
const hasScopedDeepResearchIframe = Array.from(lastTurn?.querySelectorAll?.('iframe') || []).some(f => {
|
|
723
|
+
const rect = f.getBoundingClientRect();
|
|
724
|
+
const descriptor = String(f.getAttribute('src') || '') + ' ' + String(f.getAttribute('name') || '');
|
|
725
|
+
return rect.width > 200 && rect.height > 200 &&
|
|
726
|
+
/connector_openai_deep_research|deep-research/i.test(descriptor);
|
|
727
|
+
});
|
|
728
|
+
const hasActiveScopedResearch = scopedToNewTurns && Boolean(lastTurn) &&
|
|
729
|
+
hasScopedDeepResearchIframe &&
|
|
730
|
+
(textLength < 40 || isToolStub || tailIsPlanningPanel || /chatgpt\\s+said:?$/i.test(text));
|
|
731
|
+
return { finished, stopVisible, textLength, hasIframe, isToolStub, incompleteResult, researchActivity: tailIsPlanningPanel || (isToolStub && hasScopedDeepResearchIframe), hasActiveScopedResearch, accountBlocked };
|
|
661
732
|
})()`;
|
|
662
733
|
}
|
|
663
734
|
export function buildDeepResearchStatusExpressionForTest() {
|