@@ -2,16 +2,12 @@ import { rm } from "node:fs/promises";
22import { readFileSync } from "node:fs" ;
33import os from "node:os" ;
44import net from "node:net" ;
5- import { execFile } from "node:child_process" ;
6- import { promisify } from "node:util" ;
75import CDP from "chrome-remote-interface" ;
86import { launch , Launcher , type LaunchedChrome } from "chrome-launcher" ;
97import type { BrowserLogger , ResolvedBrowserConfig , ChromeClient } from "./types.js" ;
108import { cleanupStaleProfileState } from "./profileState.js" ;
119import { delay } from "./utils.js" ;
1210
13- const execFileAsync = promisify ( execFile ) ;
14-
1511export async function launchChrome (
1612 config : ResolvedBrowserConfig ,
1713 userDataDir : string ,
@@ -20,7 +16,11 @@ export async function launchChrome(
2016 const connectHost = resolveRemoteDebugHost ( ) ;
2117 const debugBindAddress = connectHost && connectHost !== "127.0.0.1" ? "0.0.0.0" : connectHost ;
2218 const debugPort = config . debugPort ?? parseDebugPortEnv ( ) ;
23- const chromeFlags = buildChromeFlags ( config . headless ?? false , debugBindAddress ) ;
19+ const chromeFlags = buildChromeFlags (
20+ config . headless ?? false ,
21+ debugBindAddress ,
22+ config . hideWindow ?? false ,
23+ ) ;
2424 const usePatchedLauncher = Boolean ( connectHost && connectHost !== "127.0.0.1" ) ;
2525 // copy-profile reuses a copied signed-in profile whose cookies are
2626 // Keychain-encrypted, so it must launch with the real Keychain (not mocked):
@@ -56,6 +56,27 @@ export async function launchChrome(
5656 } ;
5757}
5858
59+ export async function positionChromeWindowOffscreen (
60+ client : ChromeClient ,
61+ logger : BrowserLogger ,
62+ ) : Promise < void > {
63+ if ( process . platform !== "darwin" ) {
64+ logger ( "Window hiding is only supported on macOS" ) ;
65+ return ;
66+ }
67+ try {
68+ const { windowId } = await client . Browser . getWindowForTarget ( ) ;
69+ await client . Browser . setWindowBounds ( {
70+ windowId,
71+ bounds : { left : - 32_000 , top : - 32_000 , windowState : "normal" } ,
72+ } ) ;
73+ logger ( "Chrome window positioned off-screen" ) ;
74+ } catch ( error ) {
75+ const message = error instanceof Error ? error . message : String ( error ) ;
76+ logger ( `Failed to position Chrome window off-screen: ${ message } ` ) ;
77+ }
78+ }
79+
5980export function registerTerminationHooks (
6081 chrome : LaunchedChrome ,
6182 userDataDir : string ,
@@ -144,32 +165,6 @@ export function registerTerminationHooks(
144165 } ;
145166}
146167
147- export async function hideChromeWindow (
148- chrome : LaunchedChrome ,
149- logger : BrowserLogger ,
150- ) : Promise < void > {
151- if ( process . platform !== "darwin" ) {
152- logger ( "Window hiding is only supported on macOS" ) ;
153- return ;
154- }
155- if ( ! chrome . pid ) {
156- logger ( "Unable to hide window: missing Chrome PID" ) ;
157- return ;
158- }
159- const script = `tell application "System Events"
160- try
161- set visible of (first process whose unix id is ${ chrome . pid } ) to false
162- end try
163- end tell` ;
164- try {
165- await execFileAsync ( "osascript" , [ "-e" , script ] ) ;
166- logger ( "Chrome window hidden (Cmd-H)" ) ;
167- } catch ( error ) {
168- const message = error instanceof Error ? error . message : String ( error ) ;
169- logger ( `Failed to hide Chrome window: ${ message } ` ) ;
170- }
171- }
172-
173168export async function connectToChrome (
174169 port : number ,
175170 logger : BrowserLogger ,
@@ -630,7 +625,11 @@ function isBlankPageTarget(target: { type?: string; url?: string }): boolean {
630625 return url === "about:blank" || url === "chrome://newtab/" || url === "chrome://new-tab-page/" ;
631626}
632627
633- function buildChromeFlags ( headless : boolean , debugBindAddress ?: string | null ) : string [ ] {
628+ function buildChromeFlags (
629+ headless : boolean ,
630+ debugBindAddress ?: string | null ,
631+ hideWindow = false ,
632+ ) : string [ ] {
634633 const flags = [
635634 "--disable-background-networking" ,
636635 "--disable-background-timer-throttling" ,
@@ -662,11 +661,24 @@ function buildChromeFlags(headless: boolean, debugBindAddress?: string | null):
662661
663662 if ( headless ) {
664663 flags . push ( "--headless=new" ) ;
664+ } else if ( hideWindow && process . platform === "darwin" ) {
665+ // Cmd-H stops macOS Chrome from compositing the page, which can swallow
666+ // trusted CDP clicks and retain the prompt as a draft. Keeping the window
667+ // off-screen avoids desktop disruption while preserving normal rendering.
668+ flags . push ( "--window-position=-32000,-32000" ) ;
665669 }
666670
667671 return flags ;
668672}
669673
674+ export function buildChromeFlagsForTest (
675+ headless : boolean ,
676+ debugBindAddress ?: string | null ,
677+ hideWindow = false ,
678+ ) : string [ ] {
679+ return buildChromeFlags ( headless , debugBindAddress , hideWindow ) ;
680+ }
681+
670682function resolveChromeLaunchOptions (
671683 chromeFlags : string [ ] ,
672684 usingCopiedProfile : boolean ,
0 commit comments