From 4122d4d1b4acd491519d0e7b546f56c63888c2d4 Mon Sep 17 00:00:00 2001 From: wangjingjing Date: Thu, 2 Apr 2026 14:17:20 +0800 Subject: [PATCH] feat: add focus-window action and opencli open command - Add focus-window protocol action (extension/src/protocol.ts, daemon-client.ts) - Implement handleFocusWindow in Chrome extension background script - Add Page.focusWindow() method and IPage interface declaration - Auto-inject 'open' subcommand for all browser-capable sites in registerAllCommands --- extension/src/background.ts | 20 ++++++++++++++++++++ extension/src/protocol.ts | 2 +- src/browser/daemon-client.ts | 2 +- src/browser/page.ts | 5 +++++ src/commanderAdapter.ts | 23 +++++++++++++++++++++++ src/types.ts | 2 ++ 6 files changed, 52 insertions(+), 2 deletions(-) diff --git a/extension/src/background.ts b/extension/src/background.ts index 29766cd0..dd864451 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -279,6 +279,8 @@ async function handleCommand(cmd: Command): Promise { return await handleSessions(cmd); case 'set-file-input': return await handleSetFileInput(cmd, workspace); + case 'focus-window': + return await handleFocusWindow(cmd, workspace); default: return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` }; } @@ -692,6 +694,24 @@ async function handleCloseWindow(cmd: Command, workspace: string): Promise { + const session = automationSessions.get(workspace); + if (session) { + try { + await chrome.windows.update(session.windowId, { focused: true }); + return { id: cmd.id, ok: true, data: { focused: true } }; + } catch { + // Window was closed externally — clean up and fall through to create + if (session.idleTimer) clearTimeout(session.idleTimer); + automationSessions.delete(workspace); + } + } + // No live session — create one and bring it to the foreground + const windowId = await getAutomationWindow(workspace); + await chrome.windows.update(windowId, { focused: true }); + return { id: cmd.id, ok: true, data: { focused: true } }; +} + async function handleSetFileInput(cmd: Command, workspace: string): Promise { if (!cmd.files || !Array.isArray(cmd.files) || cmd.files.length === 0) { return { id: cmd.id, ok: false, error: 'Missing or empty files array' }; diff --git a/extension/src/protocol.ts b/extension/src/protocol.ts index 381761c2..bc2f9165 100644 --- a/extension/src/protocol.ts +++ b/extension/src/protocol.ts @@ -5,7 +5,7 @@ * Everything else is just JS code sent via 'exec'. */ -export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'cdp'; +export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'cdp' | 'focus-window'; export interface Command { /** Unique request ID */ diff --git a/src/browser/daemon-client.ts b/src/browser/daemon-client.ts index b6b9ab83..2c5c7c10 100644 --- a/src/browser/daemon-client.ts +++ b/src/browser/daemon-client.ts @@ -21,7 +21,7 @@ function generateId(): string { export interface DaemonCommand { id: string; - action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'cdp'; + action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'cdp' | 'focus-window'; tabId?: number; code?: string; workspace?: string; diff --git a/src/browser/page.ts b/src/browser/page.ts index 73db6b44..0d9458ee 100644 --- a/src/browser/page.ts +++ b/src/browser/page.ts @@ -123,6 +123,11 @@ export class Page extends BasePage { } } + /** Bring the automation window to the foreground. Creates one if none exists. */ + async focusWindow(): Promise { + await sendCommand('focus-window', { ...this._wsOpt() }); + } + async tabs(): Promise { const result = await sendCommand('tabs', { op: 'list', ...this._wsOpt() }); return Array.isArray(result) ? result : []; diff --git a/src/commanderAdapter.ts b/src/commanderAdapter.ts index 27c78f02..8c8c569c 100644 --- a/src/commanderAdapter.ts +++ b/src/commanderAdapter.ts @@ -13,6 +13,7 @@ import { Command } from 'commander'; import chalk from 'chalk'; import { type CliCommand, fullName, getRegistry } from './registry.js'; +import { sendCommand } from './browser/daemon-client.js'; import { formatRegistryHelpText } from './serialization.js'; import { render as renderOutput } from './output.js'; import { executeCommand } from './execution.js'; @@ -302,6 +303,8 @@ export function registerAllCommands( siteGroups: Map, ): void { const seen = new Set(); + const browserSites = new Set(); + for (const [, cmd] of getRegistry()) { if (seen.has(cmd)) continue; seen.add(cmd); @@ -311,5 +314,25 @@ export function registerAllCommands( siteGroups.set(cmd.site, siteCmd); } registerCommandToProgram(siteCmd, cmd); + if (cmd.browser) browserSites.add(cmd.site); + } + + // Inject `open` subcommand for every site that has at least one browser command. + for (const site of browserSites) { + const siteCmd = siteGroups.get(site); + if (!siteCmd) continue; + if (siteCmd.commands.some((c: Command) => c.name() === 'open')) continue; + siteCmd + .command('open') + .description('Bring the automation window to the foreground') + .action(async () => { + try { + await sendCommand('focus-window', { workspace: `site:${site}` }); + console.log(chalk.green(`✓ ${site} automation window is now in the foreground.`)); + } catch (err) { + console.error(chalk.red(`Failed to focus window: ${err instanceof Error ? err.message : err}`)); + process.exitCode = EXIT_CODES.GENERIC_ERROR; + } + }); } } diff --git a/src/types.ts b/src/types.ts index cb31594e..e1483dba 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,6 +71,8 @@ export interface IPage { */ setFileInput?(files: string[], selector?: string): Promise; closeWindow?(): Promise; + /** Bring the automation window to the foreground. Creates one if none exists. */ + focusWindow?(): Promise; /** Returns the current page URL, or null if unavailable. */ getCurrentUrl?(): Promise; /** Returns the active tab ID, or undefined if not yet resolved. */