diff --git a/extension/dist/background.js b/extension/dist/background.js index e3605fb..dfe3542 100644 --- a/extension/dist/background.js +++ b/extension/dist/background.js @@ -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); diff --git a/extension/src/background.ts b/extension/src/background.ts index ba64fe1..629d7a0 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -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 | 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 { @@ -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 { + // Reset idle timer on every command (window stays alive while active) + resetWindowIdleTimer(); try { switch (cmd.action) { case 'exec': diff --git a/src/runtime.ts b/src/runtime.ts index 9677e12..642c988 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -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( 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(() => {}); }