@youdie006/prodex 0.25.0 → 0.26.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/dist/chatgpt-browser.js +154 -19
- package/dist/cli-pro.js +23 -7
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -363,6 +363,48 @@ export async function ensureVirtualDisplay(options = {}) {
|
|
|
363
363
|
}
|
|
364
364
|
throw new Error(`No free X display between :${first} and :${first + 9}. Set PRODEX_VIRTUAL_DISPLAY_NUM to a free number.`);
|
|
365
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* How should the dedicated browser be opened?
|
|
368
|
+
*
|
|
369
|
+
* An explicit flag or environment variable wins; otherwise reopen it the way it
|
|
370
|
+
* was last opened. Without the saved fallback, `pro browser login` - the exact
|
|
371
|
+
* command every `browser_unreachable` blocker tells people to run - put a
|
|
372
|
+
* VISIBLE window back on the desktop of someone who had set up a virtual
|
|
373
|
+
* display, which is the surprise window they went to that trouble to avoid.
|
|
374
|
+
*
|
|
375
|
+
* Choosing a mode explicitly replaces the saved one wholesale rather than
|
|
376
|
+
* merging with it: asking for headless must not also rejoin an old virtual
|
|
377
|
+
* display.
|
|
378
|
+
*/
|
|
379
|
+
export function resolveBrowserWindowMode(args) {
|
|
380
|
+
const env = args.env ?? process.env;
|
|
381
|
+
const flags = args.flags ?? {};
|
|
382
|
+
const fromEnv = (name) => {
|
|
383
|
+
const raw = (env[name] ?? "").trim().toLowerCase();
|
|
384
|
+
if (raw === "")
|
|
385
|
+
return undefined;
|
|
386
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
387
|
+
};
|
|
388
|
+
const explicit = {
|
|
389
|
+
headless: flags.headless ?? fromEnv("PRODEX_HEADLESS"),
|
|
390
|
+
virtualDisplay: flags.virtualDisplay ?? fromEnv("PRODEX_VIRTUAL_DISPLAY"),
|
|
391
|
+
minimized: flags.minimized ?? fromEnv("PRODEX_MINIMIZE_WINDOW")
|
|
392
|
+
};
|
|
393
|
+
const chosen = Object.values(explicit).some((value) => value === true);
|
|
394
|
+
if (chosen) {
|
|
395
|
+
return {
|
|
396
|
+
headless: explicit.headless === true,
|
|
397
|
+
virtualDisplay: explicit.virtualDisplay === true,
|
|
398
|
+
minimized: explicit.minimized === true
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const saved = args.lastLogin;
|
|
402
|
+
return {
|
|
403
|
+
headless: saved?.headless === true,
|
|
404
|
+
virtualDisplay: saved?.virtual_display !== undefined,
|
|
405
|
+
minimized: saved?.minimized === true
|
|
406
|
+
};
|
|
407
|
+
}
|
|
366
408
|
export function resolveHeadlessPreference(explicit, env = process.env) {
|
|
367
409
|
if (typeof explicit === "boolean")
|
|
368
410
|
return explicit;
|
|
@@ -416,6 +458,11 @@ export function isUsableChatGptAnswer(answer) {
|
|
|
416
458
|
if (toolPanelOnly)
|
|
417
459
|
return false;
|
|
418
460
|
}
|
|
461
|
+
// ChatGPT's own interruption notice, which is what a thread shows after the
|
|
462
|
+
// browser dies mid-stream. Measured: recover saved "Pro thinking / Connection
|
|
463
|
+
// interrupted. Waiting for the complete answer" as a recovered answer.
|
|
464
|
+
if (/connection interrupted|연결이\s*중단/i.test(normalized) && normalized.length < 200)
|
|
465
|
+
return false;
|
|
419
466
|
return true;
|
|
420
467
|
}
|
|
421
468
|
/**
|
|
@@ -439,6 +486,26 @@ export function classifyTranscriptRead(state, sentPrompt) {
|
|
|
439
486
|
? "pending"
|
|
440
487
|
: "unavailable";
|
|
441
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Should prodex drag the tab back to the thread it pinned?
|
|
491
|
+
*
|
|
492
|
+
* Only when the pin is a real conversation, and only when the page is the only
|
|
493
|
+
* way left to read the answer. Pinning a project or new-chat page (the url a
|
|
494
|
+
* send starts on, before ChatGPT rewrites it to /c/<id>) made prodex treat its
|
|
495
|
+
* OWN conversation as a stray tab and navigate away from the answer it was
|
|
496
|
+
* waiting for. And while the transcript can answer, moving someone else's tab
|
|
497
|
+
* back buys nothing.
|
|
498
|
+
*/
|
|
499
|
+
export function shouldRecoverThreadNavigation(args) {
|
|
500
|
+
const { pinnedThreadUrl, currentUrl, lastTranscriptClassification } = args;
|
|
501
|
+
if (!pinnedThreadUrl || !currentUrl)
|
|
502
|
+
return false;
|
|
503
|
+
if (!conversationIdFromThreadUrl(pinnedThreadUrl))
|
|
504
|
+
return false;
|
|
505
|
+
if (lastTranscriptClassification === "pending" || lastTranscriptClassification === "answer")
|
|
506
|
+
return false;
|
|
507
|
+
return !chatGptUrlsReferToSameTarget(currentUrl, pinnedThreadUrl);
|
|
508
|
+
}
|
|
442
509
|
export function hasFreshChatGptAnswer(previousAssistantMessageCount, state) {
|
|
443
510
|
return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer) && !state.generating;
|
|
444
511
|
}
|
|
@@ -1725,6 +1792,26 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
1725
1792
|
warnings: []
|
|
1726
1793
|
};
|
|
1727
1794
|
}
|
|
1795
|
+
// Not a research thread: read the ordinary answer from the transcript
|
|
1796
|
+
// too. Recover used to be page-only, so it inherited everything the
|
|
1797
|
+
// page loses - flattened markdown, dropped citation urls - and it once
|
|
1798
|
+
// saved ChatGPT's "Connection interrupted" notice as the answer.
|
|
1799
|
+
const transcript = await evaluateOnPage(page.page, transcriptAnswerExpression(conversationId), {
|
|
1800
|
+
timeoutMs: 60_000
|
|
1801
|
+
});
|
|
1802
|
+
if (transcript.ok && transcript.text.trim().length > 0) {
|
|
1803
|
+
const recovered = resolveTranscriptCitations(transcript.text, transcript.references).trim();
|
|
1804
|
+
if (recovered.length > 0) {
|
|
1805
|
+
return {
|
|
1806
|
+
url,
|
|
1807
|
+
title: "",
|
|
1808
|
+
answer: recovered,
|
|
1809
|
+
modelHints: [],
|
|
1810
|
+
...(transcript.modelSlug ? { modelSlug: transcript.modelSlug } : {}),
|
|
1811
|
+
warnings: []
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1728
1815
|
}
|
|
1729
1816
|
catch {
|
|
1730
1817
|
// Not a research thread, or the transcript API is unavailable: fall
|
|
@@ -2108,12 +2195,21 @@ export async function sendChatGptPrompt(options) {
|
|
|
2108
2195
|
if (!conversationId)
|
|
2109
2196
|
throw new ChatGptBrowserBlockerError(deepResearchUnreadableBlocker(pinnedThreadUrl ?? "https://chatgpt.com/"));
|
|
2110
2197
|
let lastState;
|
|
2198
|
+
let consecutiveResearchReadFailures = 0;
|
|
2111
2199
|
while (Date.now() - started < timeoutMs) {
|
|
2112
2200
|
try {
|
|
2113
2201
|
lastState = await evaluateOnPage(page, deepResearchReportExpression(conversationId), { timeoutMs: 60_000 });
|
|
2202
|
+
consecutiveResearchReadFailures = 0;
|
|
2114
2203
|
}
|
|
2115
2204
|
catch {
|
|
2116
|
-
//
|
|
2205
|
+
// A few failures in a row mean the browser is gone, not busy. Waiting
|
|
2206
|
+
// out a 30-minute budget on a dead browser helps nobody: the research
|
|
2207
|
+
// finishes on ChatGPT's side anyway, so hand back the thread and let
|
|
2208
|
+
// recover collect the report.
|
|
2209
|
+
consecutiveResearchReadFailures += 1;
|
|
2210
|
+
if (consecutiveResearchReadFailures >= CONSECUTIVE_READ_FAILURES_BEFORE_GIVING_UP) {
|
|
2211
|
+
throw new ChatGptBrowserBlockerError(browserLostMidWaitBlocker(pinnedThreadUrl));
|
|
2212
|
+
}
|
|
2117
2213
|
await sleep(5_000);
|
|
2118
2214
|
continue;
|
|
2119
2215
|
}
|
|
@@ -2143,11 +2239,14 @@ export async function sendChatGptPrompt(options) {
|
|
|
2143
2239
|
});
|
|
2144
2240
|
}
|
|
2145
2241
|
let recoveredNavigations = 0;
|
|
2242
|
+
let lastTranscriptClassification;
|
|
2243
|
+
let consecutiveReadFailures = 0;
|
|
2146
2244
|
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
2147
2245
|
while (Date.now() - started < timeoutMs) {
|
|
2148
2246
|
await sleep(1000);
|
|
2149
2247
|
try {
|
|
2150
2248
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
2249
|
+
consecutiveReadFailures = 0;
|
|
2151
2250
|
// First conversation id wins. Re-deriving it every poll would let a tab
|
|
2152
2251
|
// that wandered to another thread redirect the read to a stranger's
|
|
2153
2252
|
// conversation - and the prompt check below is the second line of defence,
|
|
@@ -2159,12 +2258,11 @@ export async function sendChatGptPrompt(options) {
|
|
|
2159
2258
|
// instead of caret heuristics, and the model that actually answered.
|
|
2160
2259
|
if (transcriptConversationId && !finalState.generating) {
|
|
2161
2260
|
const transcript = await readTranscriptAnswer(page, transcriptConversationId, options.prompt);
|
|
2261
|
+
lastTranscriptClassification = transcript.classification;
|
|
2162
2262
|
if (transcript.answer)
|
|
2163
2263
|
return transcriptResult(transcript.answer);
|
|
2164
2264
|
}
|
|
2165
|
-
|
|
2166
|
-
// conversation id is known, a wandering tab is harmless.
|
|
2167
|
-
if (!transcriptConversationId && pinnedThreadUrl && finalState?.url && !chatGptUrlsReferToSameTarget(finalState.url, pinnedThreadUrl)) {
|
|
2265
|
+
if (shouldRecoverThreadNavigation({ pinnedThreadUrl, currentUrl: finalState?.url, lastTranscriptClassification })) {
|
|
2168
2266
|
if (recoveredNavigations >= 2) {
|
|
2169
2267
|
throw new ChatGptBrowserBlockerError({
|
|
2170
2268
|
code: "thread_navigated_away",
|
|
@@ -2186,7 +2284,13 @@ export async function sendChatGptPrompt(options) {
|
|
|
2186
2284
|
throw error;
|
|
2187
2285
|
// Transient CDP failure while the answer is streaming: retry. A throw here
|
|
2188
2286
|
// would discard an already-streamed partial answer and skip the salvage
|
|
2189
|
-
// path below, so keep the last good state and poll again
|
|
2287
|
+
// path below, so keep the last good state and poll again. But a run of
|
|
2288
|
+
// failures is a browser that went away rather than a busy one, and
|
|
2289
|
+
// sitting out the whole budget on it only delays the recovery.
|
|
2290
|
+
consecutiveReadFailures += 1;
|
|
2291
|
+
if (consecutiveReadFailures >= CONSECUTIVE_READ_FAILURES_BEFORE_GIVING_UP) {
|
|
2292
|
+
throw new ChatGptBrowserBlockerError(browserLostMidWaitBlocker(finalState?.url ?? pinnedThreadUrl));
|
|
2293
|
+
}
|
|
2190
2294
|
continue;
|
|
2191
2295
|
}
|
|
2192
2296
|
const runtimeBlocker = chatGptBlockerFromAnswerState(finalState);
|
|
@@ -2205,6 +2309,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
2205
2309
|
// code that innerText flattens) and the model that actually answered.
|
|
2206
2310
|
if (transcriptConversationId) {
|
|
2207
2311
|
const transcript = await readTranscriptAnswer(page, transcriptConversationId, options.prompt);
|
|
2312
|
+
lastTranscriptClassification = transcript.classification;
|
|
2208
2313
|
if (transcript.answer)
|
|
2209
2314
|
return transcriptResult(transcript.answer);
|
|
2210
2315
|
// The transcript can read this conversation and says it is not done:
|
|
@@ -3052,6 +3157,32 @@ export function chunkComposerText(text, size = COMPOSER_INSERT_CHUNK_CHARS) {
|
|
|
3052
3157
|
* so hand the run back rather than waiting on a page that never renders it -
|
|
3053
3158
|
* deep research draws into a widget iframe, leaving the thread DOM empty.
|
|
3054
3159
|
*/
|
|
3160
|
+
/**
|
|
3161
|
+
* The dedicated browser died while a send was waiting for its answer.
|
|
3162
|
+
*
|
|
3163
|
+
* Observed live: Chrome went away mid deep-research run, every poll threw, the
|
|
3164
|
+
* loop swallowed each failure, and the send sat silent for the rest of a
|
|
3165
|
+
* 30-minute budget - while the research finished on ChatGPT's side. Nothing is
|
|
3166
|
+
* lost except the ability to read it, so fail fast and hand back the thread.
|
|
3167
|
+
*/
|
|
3168
|
+
// Reading the page can fail for a moment (a navigation, a busy renderer). Five
|
|
3169
|
+
// failures in a row is not a moment - it is a browser that went away.
|
|
3170
|
+
const CONSECUTIVE_READ_FAILURES_BEFORE_GIVING_UP = 5;
|
|
3171
|
+
export function browserLostMidWaitBlocker(threadUrl) {
|
|
3172
|
+
// Killing the browser aborts a streaming answer too, so promise nothing
|
|
3173
|
+
// about the answer itself - only say where to look. Deep research is the one
|
|
3174
|
+
// case that genuinely keeps going without us.
|
|
3175
|
+
const where = threadUrl ? ` The consult landed in ${threadUrl}` : "";
|
|
3176
|
+
return {
|
|
3177
|
+
code: "browser_unreachable",
|
|
3178
|
+
message: `The dedicated ChatGPT browser stopped responding while this consult was waiting for its answer.${where}`,
|
|
3179
|
+
retryable: true,
|
|
3180
|
+
next_step: threadUrl
|
|
3181
|
+
? `Run \`prodex pro browser login\` to reopen the browser, then collect the answer with \`prodex pro browser recover --target-url ${threadUrl}\` (MCP: pro_recover with thread ${threadUrl}).`
|
|
3182
|
+
: "Run `prodex pro browser login` to reopen the browser, then retry.",
|
|
3183
|
+
...(threadUrl ? { thread: threadUrl } : {})
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3055
3186
|
export function deepResearchUnreadableBlocker(threadUrl) {
|
|
3056
3187
|
return {
|
|
3057
3188
|
code: "deep_research_not_readable",
|
|
@@ -3135,7 +3266,14 @@ const NORMALIZED_PROMPT_MATCH_CHARS = 120;
|
|
|
3135
3266
|
* tool prefixes it ("@Deep research ...") and attachments append to it.
|
|
3136
3267
|
*/
|
|
3137
3268
|
export function transcriptMatchesSentPrompt(userText, sentPrompt) {
|
|
3138
|
-
|
|
3269
|
+
// The composer escapes markdown when it stores what was typed ("## File" is
|
|
3270
|
+
// kept as "\\## File", fences as escaped backticks), so undo that before
|
|
3271
|
+
// comparing - otherwise every prompt carrying markdown, which is every
|
|
3272
|
+
// --file send, looks like a different conversation.
|
|
3273
|
+
const normalize = (value) => value
|
|
3274
|
+
.replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
|
|
3275
|
+
.replace(/\s+/g, " ")
|
|
3276
|
+
.trim();
|
|
3139
3277
|
const seen = normalize(userText);
|
|
3140
3278
|
const sent = normalize(sentPrompt);
|
|
3141
3279
|
if (seen.length === 0 || sent.length === 0)
|
|
@@ -3370,17 +3508,11 @@ export function answerExpression() {
|
|
|
3370
3508
|
});
|
|
3371
3509
|
const assistantMessages = messages.filter((message) => message.role === "assistant");
|
|
3372
3510
|
const userMessages = messages.filter((message) => message.role === "user");
|
|
3373
|
-
//
|
|
3374
|
-
//
|
|
3375
|
-
//
|
|
3376
|
-
//
|
|
3377
|
-
|
|
3378
|
-
const turnAnswers = assistantMessages.length > 0 ? [] : [...document.querySelectorAll('[data-testid^="conversation-turn"]')]
|
|
3379
|
-
.filter((turn) => !turn.querySelector('[data-message-author-role="user"]'))
|
|
3380
|
-
.map((turn) => ({ role: "assistant", text: (turn.innerText || "").trim(), modelSlug: undefined }))
|
|
3381
|
-
.filter((turn) => turn.text.length > 0);
|
|
3382
|
-
const effectiveAssistants = assistantMessages.length > 0 ? assistantMessages : turnAnswers;
|
|
3383
|
-
const assistant = effectiveAssistants.at(-1);
|
|
3511
|
+
// A turn without an assistant message is NOT an answer. Treating one as an
|
|
3512
|
+
// answer (a 0.21.3 fallback for deep research, which is read from the
|
|
3513
|
+
// transcript now) turned a tool's progress panel into a 28-character
|
|
3514
|
+
// "answer" that a consult returned as its result.
|
|
3515
|
+
const assistant = assistantMessages.at(-1);
|
|
3384
3516
|
const buttons = [...document.querySelectorAll('button,[role="button"]')]
|
|
3385
3517
|
.filter((node) => !!(node.offsetWidth || node.offsetHeight || node.getClientRects().length))
|
|
3386
3518
|
.filter((node) => !node.closest(excludedTextSelector))
|
|
@@ -3393,13 +3525,16 @@ export function answerExpression() {
|
|
|
3393
3525
|
return {
|
|
3394
3526
|
title: document.title,
|
|
3395
3527
|
url: location.href,
|
|
3396
|
-
|
|
3528
|
+
// No assistant message means no answer. This used to hand back the last
|
|
3529
|
+
// 4000 characters of the page, which reads as sidebar and navigation text
|
|
3530
|
+
// dressed up as a reply.
|
|
3531
|
+
answer,
|
|
3397
3532
|
textSample: text.slice(0, 12000),
|
|
3398
3533
|
blockerTextSample: visibleTextOutsideMessages(excludedTextSelector).slice(0, 12000),
|
|
3399
3534
|
blockerScanTextSample: visibleTextOutsideMessages(blockerScanExcludedSelector).slice(0, 12000),
|
|
3400
3535
|
visibleButtonLabels: buttons,
|
|
3401
3536
|
generating: placeholder || Boolean(document.querySelector(${streamingSelector})) || buttons.some((label) => generatingControlPattern.test(label)),
|
|
3402
|
-
assistantMessageCount:
|
|
3537
|
+
assistantMessageCount: assistantMessages.length,
|
|
3403
3538
|
userMessageCount: userMessages.length,
|
|
3404
3539
|
// ChatGPT tags each assistant message with the model that produced it -
|
|
3405
3540
|
// the only ground truth for "did the Pro selection actually take".
|
package/dist/cli-pro.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildDryRunBundle } from "./bundle.js";
|
|
5
|
-
import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
5
|
+
import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveBrowserWindowMode, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
6
6
|
import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, formatCliCommand, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readNonNegativeIntegerFlag, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
|
|
7
7
|
import { printProBrowserHelp, printProHelp } from "./cli-help.js";
|
|
8
8
|
import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
|
|
@@ -222,14 +222,30 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
222
222
|
// again: Chrome's singleton would just open ANOTHER window (the recurring
|
|
223
223
|
// "extra windows" problem, which then blocks sends as
|
|
224
224
|
// ambiguous_chatgpt_tabs). Reuse the running instance instead.
|
|
225
|
-
|
|
226
|
-
// A real browser on a virtual X display: no window anywhere, and
|
|
227
|
-
// Cloudflare sees an ordinary headed Chrome (headless it rejects).
|
|
228
|
-
const wantsVirtualDisplay = resolveVirtualDisplayPreference(browserArgs.includes("--virtual-display") ? true : undefined);
|
|
229
|
-
if (wantsVirtualDisplay && headless) {
|
|
225
|
+
if (browserArgs.includes("--virtual-display") && browserArgs.includes("--headless")) {
|
|
230
226
|
throw new Error("pro browser login cannot combine --headless and --virtual-display (a virtual display already hides the window).");
|
|
231
227
|
}
|
|
232
|
-
|
|
228
|
+
// Reopen the browser the way it was last opened unless a flag or the
|
|
229
|
+
// environment says otherwise. A virtual-display user who follows the
|
|
230
|
+
// `browser_unreachable` advice used to get a visible window back.
|
|
231
|
+
const savedLaunch = await readLastBrowserLoginLaunch();
|
|
232
|
+
const windowMode = resolveBrowserWindowMode({
|
|
233
|
+
flags: {
|
|
234
|
+
...(browserArgs.includes("--headless") ? { headless: true } : {}),
|
|
235
|
+
...(browserArgs.includes("--virtual-display") ? { virtualDisplay: true } : {}),
|
|
236
|
+
...(browserArgs.includes("--minimized") ? { minimized: true } : {})
|
|
237
|
+
},
|
|
238
|
+
...(savedLaunch ? { lastLogin: savedLaunch } : {})
|
|
239
|
+
});
|
|
240
|
+
const headless = windowMode.headless;
|
|
241
|
+
// A real browser on a virtual X display: no window anywhere, and
|
|
242
|
+
// Cloudflare sees an ordinary headed Chrome (headless it rejects).
|
|
243
|
+
const wantsVirtualDisplay = windowMode.virtualDisplay;
|
|
244
|
+
const virtualDisplay = wantsVirtualDisplay
|
|
245
|
+
? await ensureVirtualDisplay(savedLaunch?.virtual_display !== undefined && !browserArgs.includes("--virtual-display")
|
|
246
|
+
? { displayNumber: savedLaunch.virtual_display }
|
|
247
|
+
: {})
|
|
248
|
+
: undefined;
|
|
233
249
|
const alreadyRunning = (await getChatGptBrowserStatus({ port })).reachable;
|
|
234
250
|
if (alreadyRunning) {
|
|
235
251
|
// One Chrome profile cannot serve a headed and a headless instance at
|