mirror of https://github.com/garrytan/gstack.git
feat: converge handoff into connect — extension loads on handoff
Handoff now uses launchPersistentContext() with extension auto-loading, same as the connect/launchHeaded() path. This means when the agent gets stuck (2FA, CAPTCHA) and hands off to the user, the Chrome extension + side panel are available automatically. Before: handoff used chromium.launch() + newContext() — no extension After: handoff uses chromium.launchPersistentContext() — extension loads Also sets connectionMode to 'headed' and disables dialog auto-accept on handoff, matching connect behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28bc69aba9
commit
dff217838b
|
|
@ -639,49 +639,68 @@ export class BrowserManager {
|
||||||
const state = await this.saveState();
|
const state = await this.saveState();
|
||||||
const currentUrl = this.getCurrentUrl();
|
const currentUrl = this.getCurrentUrl();
|
||||||
|
|
||||||
// 2. Launch new headed browser (try-catch — if this fails, headless stays running)
|
// 2. Launch new headed browser with extension (same as launchHeaded)
|
||||||
let newBrowser: Browser;
|
// Uses launchPersistentContext so the extension auto-loads.
|
||||||
|
let newContext: BrowserContext;
|
||||||
try {
|
try {
|
||||||
newBrowser = await chromium.launch({ headless: false, timeout: 15000 });
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const extensionPath = this.findExtensionPath();
|
||||||
|
const launchArgs = ['--hide-crash-restore-bubble'];
|
||||||
|
if (extensionPath) {
|
||||||
|
launchArgs.push(`--disable-extensions-except=${extensionPath}`);
|
||||||
|
launchArgs.push(`--load-extension=${extensionPath}`);
|
||||||
|
console.log(`[browse] Handoff: loading extension from ${extensionPath}`);
|
||||||
|
} else {
|
||||||
|
console.log('[browse] Handoff: extension not found — headed mode without side panel');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDataDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
|
||||||
|
fs.mkdirSync(userDataDir, { recursive: true });
|
||||||
|
|
||||||
|
newContext = await chromium.launchPersistentContext(userDataDir, {
|
||||||
|
headless: false,
|
||||||
|
args: launchArgs,
|
||||||
|
viewport: null,
|
||||||
|
ignoreDefaultArgs: [
|
||||||
|
'--disable-extensions',
|
||||||
|
'--disable-component-extensions-with-background-pages',
|
||||||
|
],
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
return `ERROR: Cannot open headed browser — ${msg}. Headless browser still running.`;
|
return `ERROR: Cannot open headed browser — ${msg}. Headless browser still running.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create context and restore state into new headed browser
|
// 3. Restore state into new headed browser
|
||||||
try {
|
try {
|
||||||
const contextOptions: BrowserContextOptions = {
|
// Swap to new browser/context before restoreState (it uses this.context)
|
||||||
viewport: { width: 1280, height: 720 },
|
const oldBrowser = this.browser;
|
||||||
};
|
|
||||||
if (this.customUserAgent) {
|
this.context = newContext;
|
||||||
contextOptions.userAgent = this.customUserAgent;
|
this.browser = newContext.browser();
|
||||||
}
|
this.pages.clear();
|
||||||
const newContext = await newBrowser.newContext(contextOptions);
|
this.connectionMode = 'headed';
|
||||||
|
|
||||||
if (Object.keys(this.extraHeaders).length > 0) {
|
if (Object.keys(this.extraHeaders).length > 0) {
|
||||||
await newContext.setExtraHTTPHeaders(this.extraHeaders);
|
await newContext.setExtraHTTPHeaders(this.extraHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swap to new browser/context before restoreState (it uses this.context)
|
|
||||||
const oldBrowser = this.browser;
|
|
||||||
const oldContext = this.context;
|
|
||||||
|
|
||||||
this.browser = newBrowser;
|
|
||||||
this.context = newContext;
|
|
||||||
this.pages.clear();
|
|
||||||
|
|
||||||
// Register crash handler on new browser
|
// Register crash handler on new browser
|
||||||
this.browser.on('disconnected', () => {
|
if (this.browser) {
|
||||||
console.error('[browse] FATAL: Chromium process crashed or was killed. Server exiting.');
|
this.browser.on('disconnected', () => {
|
||||||
console.error('[browse] Console/network logs flushed to .gstack/browse-*.log');
|
if (this.intentionalDisconnect) return;
|
||||||
process.exit(1);
|
console.error('[browse] FATAL: Chromium process crashed or was killed. Server exiting.');
|
||||||
});
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await this.restoreState(state);
|
await this.restoreState(state);
|
||||||
this.isHeaded = true;
|
this.isHeaded = true;
|
||||||
|
this.dialogAutoAccept = false; // User controls dialogs in headed mode
|
||||||
|
|
||||||
// 4. Close old headless browser (fire-and-forget — close() can hang
|
// 4. Close old headless browser (fire-and-forget)
|
||||||
// when another Playwright instance is active, so we don't await it)
|
|
||||||
oldBrowser.removeAllListeners('disconnected');
|
oldBrowser.removeAllListeners('disconnected');
|
||||||
oldBrowser.close().catch(() => {});
|
oldBrowser.close().catch(() => {});
|
||||||
|
|
||||||
|
|
@ -691,8 +710,8 @@ export class BrowserManager {
|
||||||
`STATUS: Waiting for user. Run 'resume' when done.`,
|
`STATUS: Waiting for user. Run 'resume' when done.`,
|
||||||
].join('\n');
|
].join('\n');
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
// Restore failed — close the new browser, keep old one
|
// Restore failed — close the new context, keep old state
|
||||||
await newBrowser.close().catch(() => {});
|
await newContext.close().catch(() => {});
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
return `ERROR: Handoff failed during state restore — ${msg}. Headless browser still running.`;
|
return `ERROR: Handoff failed during state restore — ${msg}. Headless browser still running.`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue