fix(extension): use idle-timeout for automation window lifecycle

Replace eager close-window (which caused race conditions when
parallel commands shared the window) with an idle-based timer:

- Window auto-closes 30s after the last command completes
- Each incoming command resets the idle timer
- Consecutive commands reuse the same window (faster)
- No race conditions with parallel execution
- Close-window action kept for explicit cleanup if needed
This commit is contained in:
jackwener committed 2026-03-20 00:05:48 +08:00
1 parent 2f6d28a3e9
commit b5c1b242e2
3 files changed
+43 -7

No files matched your search

+20
View File
@@ -169,6 +169,21 @@ function scheduleReconnect() {
}, delay);
}
var automationWindowId = null;
var windowIdleTimer = null;
var WINDOW_IDLE_TIMEOUT = 3e4;
function resetWindowIdleTimer() {
if (windowIdleTimer) clearTimeout(windowIdleTimer);
windowIdleTimer = setTimeout(async () => {
if (automationWindowId !== null) {
try {
await chrome.windows.remove(automationWindowId);
console.log(`[opencli] Automation window ${automationWindowId} closed (idle timeout)`);
} catch {}
automationWindowId = null;
}
windowIdleTimer = null;
}, WINDOW_IDLE_TIMEOUT);
}
/** Get or create the dedicated automation window. */
async function getAutomationWindow() {
if (automationWindowId !== null) try {
@@ -191,6 +206,10 @@ chrome.windows.onRemoved.addListener((windowId) => {
if (windowId === automationWindowId) {
console.log("[opencli] Automation window closed");
automationWindowId = null;
if (windowIdleTimer) {
clearTimeout(windowIdleTimer);
windowIdleTimer = null;
}
}
});
var initialized = false;
@@ -212,6 +231,7 @@ chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "keepalive") connect();
});
async function handleCommand(cmd) {
resetWindowIdleTimer();
try {
switch (cmd.action) {
case "exec": return await handleExec(cmd);
+22
View File
@@ -88,8 +88,27 @@ function scheduleReconnect(): void {
// ─── Automation window isolation ─────────────────────────────────────
// All opencli operations happen in a dedicated Chrome window so the
// user's active browsing session is never touched.
// The window auto-closes after 30s of idle (no commands).
let automationWindowId: number | null = null;
let windowIdleTimer: ReturnType<typeof setTimeout> | null = null;
const WINDOW_IDLE_TIMEOUT = 30000; // 30s
function resetWindowIdleTimer(): void {
if (windowIdleTimer) clearTimeout(windowIdleTimer);
windowIdleTimer = setTimeout(async () => {
if (automationWindowId !== null) {
try {
await chrome.windows.remove(automationWindowId);
console.log(`[opencli] Automation window ${automationWindowId} closed (idle timeout)`);
} catch {
// Already gone
}
automationWindowId = null;
}
windowIdleTimer = null;
}, WINDOW_IDLE_TIMEOUT);
}
/** Get or create the dedicated automation window. */
async function getAutomationWindow(): Promise<number> {
@@ -122,6 +141,7 @@ chrome.windows.onRemoved.addListener((windowId) => {
if (windowId === automationWindowId) {
console.log('[opencli] Automation window closed');
automationWindowId = null;
if (windowIdleTimer) { clearTimeout(windowIdleTimer); windowIdleTimer = null; }
}
});
@@ -153,6 +173,8 @@ chrome.alarms.onAlarm.addListener((alarm) => {
// ─── Command dispatcher ─────────────────────────────────────────────
async function handleCommand(cmd: Command): Promise<Result> {
// Reset idle timer on every command (window stays alive while active)
resetWindowIdleTimer();
try {
switch (cmd.action) {
case 'exec':
+1 -7
View File
@@ -1,5 +1,4 @@
import type { IPage } from './types.js';
import type { Page } from './browser/page.js';
export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '60', 10);
@@ -42,12 +41,7 @@ export async function browserSession<T>(
const mcp = new BrowserFactory();
try {
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT });
const result = await fn(page);
// Close the automation window after command completes
if (typeof (page as Page).closeWindow === 'function') {
await (page as Page).closeWindow();
}
return result;
return await fn(page);
} finally {
await mcp.close().catch(() => {});
}