MCPcopy
hub / github.com/garrytan/gstack / handoff

Method handoff

browse/src/browser-manager.ts:1537–1647  ·  view source on GitHub ↗

* 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)

Source from the content-addressed store, hash-verified

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

Callers 2

handoff.test.tsFile · 0.80
handleMetaCommandFunction · 0.80

Calls 11

getCurrentUrlMethod · 0.95
saveStateMethod · 0.95
findExtensionPathMethod · 0.95
restoreStateMethod · 0.95
buildGStackLaunchArgsFunction · 0.85
applyStealthFunction · 0.85
handleChromiumDisconnectFunction · 0.85
closeMethod · 0.65
pushMethod · 0.45
clearMethod · 0.45

Tested by

no test coverage detected