* Hand off browser control to the user by relaunching in headed mode. * * Flow (launch-first-close-second for safe rollback): * 1. Save state from current headless browser * 2. Launch NEW headed browser * 3. Restore state into new browser * 4. Close OLD headless browser
(message: string)
| 1535 | * If step 2 fails → return error, headless browser untouched |
| 1536 | */ |
| 1537 | async handoff(message: string): Promise<string> { |
| 1538 | if (this.connectionMode === 'headed' || this.isHeaded) { |
| 1539 | return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`; |
| 1540 | } |
| 1541 | if (!this.browser || !this.context) { |
| 1542 | throw new Error('Browser not launched'); |
| 1543 | } |
| 1544 | |
| 1545 | // 1. Save state from current browser |
| 1546 | const state = await this.saveState(); |
| 1547 | const currentUrl = this.getCurrentUrl(); |
| 1548 | |
| 1549 | // 2. Launch new headed browser with extension (same as launchHeaded) |
| 1550 | // Uses launchPersistentContext so the extension auto-loads. |
| 1551 | let newContext: BrowserContext; |
| 1552 | try { |
| 1553 | const fs = require('fs'); |
| 1554 | const path = require('path'); |
| 1555 | const extensionPath = this.findExtensionPath(); |
| 1556 | const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth'); |
| 1557 | // Same blink-level stealth flags as launch()/launchHeaded(). Without |
| 1558 | // STEALTH_LAUNCH_ARGS the handed-off browser kept the AutomationControlled |
| 1559 | // tell that the other two paths strip. |
| 1560 | const launchArgs: string[] = ['--hide-crash-restore-bubble', ...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()]; |
| 1561 | if (extensionPath) { |
| 1562 | launchArgs.push(`--disable-extensions-except=${extensionPath}`); |
| 1563 | launchArgs.push(`--load-extension=${extensionPath}`); |
| 1564 | // Auth token is served via /health endpoint now (no file write needed). |
| 1565 | // Extension reads token from /health on connect. |
| 1566 | console.log(`[browse] Handoff: loading extension from ${extensionPath}`); |
| 1567 | } else { |
| 1568 | console.log('[browse] Handoff: extension not found — headed mode without side panel'); |
| 1569 | } |
| 1570 | |
| 1571 | const userDataDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile'); |
| 1572 | fs.mkdirSync(userDataDir, { recursive: true }); |
| 1573 | |
| 1574 | // T1: same automation-tell-stripping defaults as launchHeaded(). |
| 1575 | // The handoff path (headless → headed re-launch) takes the same |
| 1576 | // anti-detection posture. |
| 1577 | const { STEALTH_IGNORE_DEFAULT_ARGS } = await import('./stealth'); |
| 1578 | newContext = await chromium.launchPersistentContext(userDataDir, { |
| 1579 | headless: false, |
| 1580 | // Match the sandbox policy used by launchHeaded() / launch(). The |
| 1581 | // handoff path is the headless→headed re-launch and shares the same |
| 1582 | // anti-detection posture, including no spurious --no-sandbox infobar. |
| 1583 | chromiumSandbox: shouldEnableChromiumSandbox(), |
| 1584 | args: launchArgs, |
| 1585 | viewport: null, |
| 1586 | ...(this.proxyConfig ? { proxy: this.proxyConfig } : {}), |
| 1587 | ignoreDefaultArgs: STEALTH_IGNORE_DEFAULT_ARGS, |
| 1588 | timeout: 15000, |
| 1589 | }); |
| 1590 | } catch (err: unknown) { |
| 1591 | const msg = err instanceof Error ? err.message : String(err); |
| 1592 | return `ERROR: Cannot open headed browser — ${msg}. Headless browser still running.`; |
| 1593 | } |
| 1594 |
no test coverage detected