@@ -321,6 +321,133 @@ fn incompatible_launch_mode_error(flags: &Flags) -> Option<&'static str> {
321321 None
322322}
323323
324+ fn build_local_launch_command ( flags : & Flags ) -> serde_json:: Value {
325+ let mut launch_cmd = json ! ( {
326+ "id" : gen_id( ) ,
327+ "action" : "launch" ,
328+ } ) ;
329+ // Only send headless when the user set it on this invocation. When
330+ // absent, the daemon falls back to its spawn-time AGENT_BROWSER_HEADED
331+ // env, so a follow-up command without --headed (common when env vars
332+ // like AGENT_BROWSER_ARGS force a launch command on every call) does
333+ // not flip a headed session back to headless and relaunch the browser
334+ // onto about:blank.
335+ if flags. headed || flags. cli_headed {
336+ launch_cmd[ "headless" ] = json ! ( !flags. headed) ;
337+ }
338+ launch_cmd[ "plugins" ] = json ! ( flags. plugins. clone( ) ) ;
339+ attach_restore_config_to_command ( & mut launch_cmd, flags) ;
340+
341+ let cmd_obj = launch_cmd
342+ . as_object_mut ( )
343+ . expect ( "json! macro guarantees object type" ) ;
344+
345+ // Add executable path if specified
346+ if let Some ( ref exec_path) = flags. executable_path {
347+ cmd_obj. insert ( "executablePath" . to_string ( ) , json ! ( exec_path) ) ;
348+ }
349+
350+ // Add profile path if specified
351+ if let Some ( ref profile_path) = flags. profile {
352+ cmd_obj. insert ( "profile" . to_string ( ) , json ! ( profile_path) ) ;
353+ }
354+
355+ // Add state path if specified
356+ if let Some ( ref state_path) = flags. state {
357+ cmd_obj. insert ( "storageState" . to_string ( ) , json ! ( state_path) ) ;
358+ }
359+
360+ if let Some ( ref proxy_str) = flags. proxy {
361+ let parsed = parse_proxy ( proxy_str) ;
362+ let mut proxy_obj = json ! ( { "server" : parsed. server } ) ;
363+ if let Some ( ref username) = parsed. username {
364+ proxy_obj[ "username" ] = json ! ( username) ;
365+ }
366+ if let Some ( ref password) = parsed. password {
367+ proxy_obj[ "password" ] = json ! ( password) ;
368+ }
369+ if let Some ( ref bypass) = flags. proxy_bypass {
370+ proxy_obj[ "bypass" ] = json ! ( bypass) ;
371+ }
372+ cmd_obj. insert ( "proxy" . to_string ( ) , proxy_obj) ;
373+ }
374+
375+ if let Some ( ref ua) = flags. user_agent {
376+ cmd_obj. insert ( "userAgent" . to_string ( ) , json ! ( ua) ) ;
377+ }
378+
379+ if let Some ( ref a) = flags. args {
380+ cmd_obj. insert ( "args" . to_string ( ) , json ! ( split_launch_args( a) ) ) ;
381+ }
382+
383+ if !flags. extensions . is_empty ( ) {
384+ cmd_obj. insert ( "extensions" . to_string ( ) , json ! ( flags. extensions) ) ;
385+ }
386+
387+ if !flags. init_scripts . is_empty ( ) {
388+ cmd_obj. insert ( "initScripts" . to_string ( ) , json ! ( flags. init_scripts) ) ;
389+ }
390+
391+ if !flags. enable . is_empty ( ) {
392+ cmd_obj. insert ( "enable" . to_string ( ) , json ! ( flags. enable) ) ;
393+ }
394+
395+ if flags. ignore_https_errors {
396+ launch_cmd[ "ignoreHTTPSErrors" ] = json ! ( true ) ;
397+ }
398+
399+ attach_ca_cert_to_launch_command ( & mut launch_cmd, flags) ;
400+
401+ if flags. allow_file_access {
402+ launch_cmd[ "allowFileAccess" ] = json ! ( true ) ;
403+ }
404+
405+ apply_hide_scrollbars_launch_option (
406+ & mut launch_cmd,
407+ flags. cli_hide_scrollbars ,
408+ flags. hide_scrollbars ,
409+ ) ;
410+
411+ if flags. webgpu || flags. cli_webgpu {
412+ launch_cmd[ "webgpu" ] = json ! ( flags. webgpu) ;
413+ }
414+ attach_webmcp_launch_option ( & mut launch_cmd, flags) ;
415+
416+ // Env-only opt-out for automatic Xvfb; always stamped from the CLI's
417+ // fresh environment so both setting and unsetting the var take effect
418+ // on daemons spawned before the change.
419+ launch_cmd[ "noXvfb" ] = json ! ( flags. no_xvfb) ;
420+
421+ if let Some ( ref cs) = flags. color_scheme {
422+ launch_cmd[ "colorScheme" ] = json ! ( cs) ;
423+ }
424+
425+ if let Some ( ref dp) = flags. download_path {
426+ launch_cmd[ "downloadPath" ] = json ! ( dp) ;
427+ }
428+
429+ attach_allowed_domains_to_launch_command ( & mut launch_cmd, flags) ;
430+
431+ if let Some ( ref engine) = flags. engine {
432+ launch_cmd[ "engine" ] = json ! ( engine) ;
433+ }
434+ attach_obscura_proxy_bypass ( & mut launch_cmd, flags) ;
435+
436+ launch_cmd
437+ }
438+
439+ fn merge_local_launch_config ( cmd : & mut serde_json:: Value , mut launch_cmd : serde_json:: Value ) {
440+ let launch_obj = launch_cmd
441+ . as_object_mut ( )
442+ . expect ( "json! macro guarantees object type" ) ;
443+ if let Some ( cmd_obj) = cmd. as_object ( ) {
444+ for ( key, value) in cmd_obj {
445+ launch_obj. insert ( key. clone ( ) , value. clone ( ) ) ;
446+ }
447+ }
448+ * cmd = launch_cmd;
449+ }
450+
324451fn should_send_local_launch_config ( flags : & Flags , command : & serde_json:: Value ) -> bool {
325452 ( flags. headed
326453 || flags. cli_headed
@@ -2098,144 +2225,41 @@ fn main() {
20982225
20992226 // Launch headed browser or configure browser options (without CDP or provider)
21002227 if should_send_local_launch_config ( & flags, & cmd) {
2101- let mut launch_cmd = json ! ( {
2102- "id" : gen_id( ) ,
2103- "action" : "launch" ,
2104- } ) ;
2105- // Only send headless when the user set it on this invocation. When
2106- // absent, the daemon falls back to its spawn-time AGENT_BROWSER_HEADED
2107- // env, so a follow-up command without --headed (common when env vars
2108- // like AGENT_BROWSER_ARGS force a launch command on every call) does
2109- // not flip a headed session back to headless and relaunch the browser
2110- // onto about:blank.
2111- if flags. headed || flags. cli_headed {
2112- launch_cmd[ "headless" ] = json ! ( !flags. headed) ;
2113- }
2114- launch_cmd[ "plugins" ] = json ! ( flags. plugins. clone( ) ) ;
2115- attach_restore_config_to_command ( & mut launch_cmd, & flags) ;
2116-
2117- let cmd_obj = launch_cmd
2118- . as_object_mut ( )
2119- . expect ( "json! macro guarantees object type" ) ;
2120-
2121- // Add executable path if specified
2122- if let Some ( ref exec_path) = flags. executable_path {
2123- cmd_obj. insert ( "executablePath" . to_string ( ) , json ! ( exec_path) ) ;
2124- }
2125-
2126- // Add profile path if specified
2127- if let Some ( ref profile_path) = flags. profile {
2128- cmd_obj. insert ( "profile" . to_string ( ) , json ! ( profile_path) ) ;
2129- }
2130-
2131- // Add state path if specified
2132- if let Some ( ref state_path) = flags. state {
2133- cmd_obj. insert ( "storageState" . to_string ( ) , json ! ( state_path) ) ;
2134- }
2135-
2136- if let Some ( ref proxy_str) = flags. proxy {
2137- let parsed = parse_proxy ( proxy_str) ;
2138- let mut proxy_obj = json ! ( { "server" : parsed. server } ) ;
2139- if let Some ( ref username) = parsed. username {
2140- proxy_obj[ "username" ] = json ! ( username) ;
2141- }
2142- if let Some ( ref password) = parsed. password {
2143- proxy_obj[ "password" ] = json ! ( password) ;
2144- }
2145- if let Some ( ref bypass) = flags. proxy_bypass {
2146- proxy_obj[ "bypass" ] = json ! ( bypass) ;
2147- }
2148- cmd_obj. insert ( "proxy" . to_string ( ) , proxy_obj) ;
2149- }
2150-
2151- if let Some ( ref ua) = flags. user_agent {
2152- cmd_obj. insert ( "userAgent" . to_string ( ) , json ! ( ua) ) ;
2153- }
2154-
2155- if let Some ( ref a) = flags. args {
2156- cmd_obj. insert ( "args" . to_string ( ) , json ! ( split_launch_args( a) ) ) ;
2157- }
2158-
2159- if !flags. extensions . is_empty ( ) {
2160- cmd_obj. insert ( "extensions" . to_string ( ) , json ! ( & flags. extensions) ) ;
2161- }
2162-
2163- if !flags. init_scripts . is_empty ( ) {
2164- cmd_obj. insert ( "initScripts" . to_string ( ) , json ! ( & flags. init_scripts) ) ;
2165- }
2166-
2167- if !flags. enable . is_empty ( ) {
2168- cmd_obj. insert ( "enable" . to_string ( ) , json ! ( & flags. enable) ) ;
2169- }
2170-
2171- if flags. ignore_https_errors {
2172- launch_cmd[ "ignoreHTTPSErrors" ] = json ! ( true ) ;
2173- }
2174-
2175- attach_ca_cert_to_launch_command ( & mut launch_cmd, & flags) ;
2176-
2177- if flags. allow_file_access {
2178- launch_cmd[ "allowFileAccess" ] = json ! ( true ) ;
2179- }
2180-
2181- apply_hide_scrollbars_launch_option (
2182- & mut launch_cmd,
2183- flags. cli_hide_scrollbars ,
2184- flags. hide_scrollbars ,
2185- ) ;
2186-
2187- if flags. webgpu || flags. cli_webgpu {
2188- launch_cmd[ "webgpu" ] = json ! ( flags. webgpu) ;
2189- }
2190- attach_webmcp_launch_option ( & mut launch_cmd, & flags) ;
2191-
2192- // Env-only opt-out for automatic Xvfb; always stamped from the CLI's
2193- // fresh environment so both setting and unsetting the var take effect
2194- // on daemons spawned before the change.
2195- launch_cmd[ "noXvfb" ] = json ! ( flags. no_xvfb) ;
2196-
2197- if let Some ( ref cs) = flags. color_scheme {
2198- launch_cmd[ "colorScheme" ] = json ! ( cs) ;
2199- }
2200-
2201- if let Some ( ref dp) = flags. download_path {
2202- launch_cmd[ "downloadPath" ] = json ! ( dp) ;
2203- }
2204-
2205- attach_allowed_domains_to_launch_command ( & mut launch_cmd, & flags) ;
2206-
2207- if let Some ( ref engine) = flags. engine {
2208- launch_cmd[ "engine" ] = json ! ( engine) ;
2209- }
2210- attach_obscura_proxy_bypass ( & mut launch_cmd, & flags) ;
2211-
2212- match send_command ( launch_cmd, & flags. session ) {
2213- Ok ( resp) if !resp. success => {
2214- // Launch command failed (e.g., invalid state file, profile error)
2215- let error_msg = resp
2216- . error
2217- . unwrap_or_else ( || "Browser launch failed" . to_string ( ) ) ;
2218- if flags. json {
2219- print_json_error_with_code ( error_msg, resp. code . as_deref ( ) ) ;
2220- } else {
2221- eprintln ! ( "{} {}" , color:: error_indicator( ) , error_msg) ;
2228+ let launch_cmd = build_local_launch_command ( & flags) ;
2229+ if cmd. get ( "action" ) . and_then ( |v| v. as_str ( ) ) == Some ( "launch" ) {
2230+ // Bare `open` is itself a launch, so it carries the local launch
2231+ // options instead of following them with a second envelope that
2232+ // would change the launch hash and relaunch without them.
2233+ merge_local_launch_config ( & mut cmd, launch_cmd) ;
2234+ } else {
2235+ match send_command ( launch_cmd, & flags. session ) {
2236+ Ok ( resp) if !resp. success => {
2237+ // Launch command failed (e.g., invalid state file, profile error)
2238+ let error_msg = resp
2239+ . error
2240+ . unwrap_or_else ( || "Browser launch failed" . to_string ( ) ) ;
2241+ if flags. json {
2242+ print_json_error_with_code ( error_msg, resp. code . as_deref ( ) ) ;
2243+ } else {
2244+ eprintln ! ( "{} {}" , color:: error_indicator( ) , error_msg) ;
2245+ }
2246+ exit ( 1 ) ;
22222247 }
2223- exit ( 1 ) ;
2224- }
2225- Err ( e) => {
2226- if flags. json {
2227- print_json_error ( e) ;
2228- } else {
2229- eprintln ! (
2230- "{} Could not configure browser: {}" ,
2231- color:: error_indicator( ) ,
2232- e
2233- ) ;
2248+ Err ( e) => {
2249+ if flags. json {
2250+ print_json_error ( e) ;
2251+ } else {
2252+ eprintln ! (
2253+ "{} Could not configure browser: {}" ,
2254+ color:: error_indicator( ) ,
2255+ e
2256+ ) ;
2257+ }
2258+ exit ( 1 ) ;
2259+ }
2260+ Ok ( _) => {
2261+ // Launch succeeded
22342262 }
2235- exit ( 1 ) ;
2236- }
2237- Ok ( _) => {
2238- // Launch succeeded
22392263 }
22402264 }
22412265 }
@@ -2523,6 +2547,39 @@ mod tests {
25232547 assert_eq ! ( command[ "inputMode" ] , "human" ) ;
25242548 }
25252549
2550+ #[ test]
2551+ fn bare_open_carries_local_launch_config_and_session_settings ( ) {
2552+ let args: Vec < String > = [
2553+ "--profile" ,
2554+ "/tmp/profile" ,
2555+ "--args" ,
2556+ "--lang=en-US" ,
2557+ "--pin-tab" ,
2558+ "--input-mode" ,
2559+ "smooth" ,
2560+ "open" ,
2561+ ]
2562+ . into_iter ( )
2563+ . map ( str:: to_string)
2564+ . collect ( ) ;
2565+ let flags = crate :: flags:: parse_flags ( & args) ;
2566+ let mut command =
2567+ crate :: commands:: parse_command ( & crate :: flags:: clean_args ( & args) , & flags) . unwrap ( ) ;
2568+ super :: attach_input_mode ( & mut command, & flags) ;
2569+ super :: attach_pin_tab_to_command ( & mut command, & flags) ;
2570+ let id = command[ "id" ] . clone ( ) ;
2571+ assert ! ( super :: should_send_local_launch_config( & flags, & command) ) ;
2572+
2573+ super :: merge_local_launch_config ( & mut command, super :: build_local_launch_command ( & flags) ) ;
2574+
2575+ assert_eq ! ( command[ "action" ] , "launch" ) ;
2576+ assert_eq ! ( command[ "id" ] , id) ;
2577+ assert_eq ! ( command[ "profile" ] , "/tmp/profile" ) ;
2578+ assert_eq ! ( command[ "args" ] , serde_json:: json!( [ "--lang=en-US" ] ) ) ;
2579+ assert_eq ! ( command[ "pinTab" ] , true ) ;
2580+ assert_eq ! ( command[ "defaultInputMode" ] , "smooth" ) ;
2581+ }
2582+
25262583 use super :: * ;
25272584
25282585 #[ test]
0 commit comments