@@ -16,11 +16,12 @@ pub async fn pty_spawn(
1616 runbox_id : String ,
1717 cwd : String ,
1818 agent_cmd : Option < String > ,
19- cols : Option < u16 > , // ← added
20- rows : Option < u16 > , // ← added
19+ cols : Option < u16 > ,
20+ rows : Option < u16 > ,
21+ docker : Option < bool > , // ← NEW: passed from frontend workspace card
2122 state : tauri:: State < ' _ , AppState > ,
2223) -> Result < ( ) , String > {
23- pty:: spawn ( app, session_id, runbox_id, cwd, agent_cmd, cols, rows, & state) . await
24+ pty:: spawn ( app, session_id, runbox_id, cwd, agent_cmd, cols, rows, docker . unwrap_or ( false ) , & state) . await
2425}
2526
2627#[ tauri:: command]
@@ -29,23 +30,29 @@ pub fn pty_write(
2930 data : String ,
3031 state : tauri:: State < ' _ , AppState > ,
3132) -> Result < ( ) , String > {
32- let mut inject: Option < ( String , String , AgentKind ) > = None ;
33+ let mut inject: Option < ( String , String , AgentKind ) > = None ;
34+ let mut open_url: Option < String > = None ;
35+ let mut send_data: Option < String > = None ;
36+
3337 {
3438 let mut sessions = state. sessions . lock ( ) . unwrap ( ) ;
3539 if let Some ( s) = sessions. get_mut ( & session_id) {
36- let _ = s. writer . write_all ( data. as_bytes ( ) ) ;
37- let _ = s. writer . flush ( ) ;
3840 for ch in data. chars ( ) {
3941 match ch {
4042 '\r' | '\n' => {
4143 let line = s. input_buf . trim ( ) . to_string ( ) ;
4244 s. input_buf . clear ( ) ;
4345 if !line. is_empty ( ) {
44- let token = line. split_whitespace ( ) . next ( ) . unwrap_or ( "" ) ;
45- let base_cmd = token. rsplit ( [ '/' , '\\' ] ) . next ( ) . unwrap_or ( token) ;
46- let kind = AgentKind :: detect ( base_cmd) ;
47- if kind != AgentKind :: Shell {
48- inject = Some ( ( s. runbox_id . clone ( ) , s. cwd . clone ( ) , kind) ) ;
46+ if let Some ( url) = intercept_start_cmd ( & line, & s. cwd ) {
47+ open_url = Some ( url) ;
48+ send_data = Some ( "\x03 \r \n " . to_string ( ) ) ;
49+ } else {
50+ let token = line. split_whitespace ( ) . next ( ) . unwrap_or ( "" ) ;
51+ let base_cmd = token. rsplit ( [ '/' , '\\' ] ) . next ( ) . unwrap_or ( token) ;
52+ let kind = AgentKind :: detect ( base_cmd) ;
53+ if kind != AgentKind :: Shell {
54+ inject = Some ( ( s. runbox_id . clone ( ) , s. cwd . clone ( ) , kind) ) ;
55+ }
4956 }
5057 }
5158 }
@@ -54,8 +61,17 @@ pub fn pty_write(
5461 _ => { }
5562 }
5663 }
64+
65+ let to_write = send_data. as_deref ( ) . unwrap_or ( & data) ;
66+ let _ = s. writer . write_all ( to_write. as_bytes ( ) ) ;
67+ let _ = s. writer . flush ( ) ;
5768 }
5869 }
70+
71+ if let Some ( url) = open_url {
72+ crate :: agent:: globals:: emit_event ( "browser-open-url" , serde_json:: json!( url) ) ;
73+ }
74+
5975 if let Some ( ( rb, cwd, kind) ) = inject {
6076 let sid = session_id. clone ( ) ;
6177 let db = state. db . clone ( ) ;
@@ -68,6 +84,41 @@ pub fn pty_write(
6884 Ok ( ( ) )
6985}
7086
87+ fn intercept_start_cmd ( line : & str , cwd : & str ) -> Option < String > {
88+ let mut parts = line. splitn ( 2 , char:: is_whitespace) ;
89+ let cmd = parts. next ( ) . unwrap_or ( "" ) . to_lowercase ( ) ;
90+ if cmd != "start" { return None ; }
91+
92+ let arg = parts. next ( ) ?. trim ( ) . trim_matches ( '"' ) . trim_matches ( '\'' ) ;
93+ if arg. is_empty ( ) { return None ; }
94+
95+ let lower = arg. to_lowercase ( ) ;
96+ if !lower. ends_with ( ".html" ) && !lower. ends_with ( ".htm" )
97+ && !lower. ends_with ( ".svg" ) && !lower. ends_with ( ".pdf" ) {
98+ return None ;
99+ }
100+
101+ if arg. starts_with ( "file://" ) { return Some ( arg. to_string ( ) ) ; }
102+
103+ if arg. len ( ) >= 2 && arg. chars ( ) . nth ( 1 ) == Some ( ':' ) {
104+ let normalised = arg. replace ( '\\' , "/" ) ;
105+ return Some ( format ! ( "file:///{normalised}" ) ) ;
106+ }
107+
108+ let abs = std:: path:: Path :: new ( cwd) . join ( arg) ;
109+
110+ if let Ok ( canonical) = abs. canonicalize ( ) {
111+ if let Ok ( url) = url:: Url :: from_file_path ( & canonical) {
112+ return Some ( url. to_string ( ) ) ;
113+ }
114+ let s = canonical. to_string_lossy ( ) . replace ( '\\' , "/" ) ;
115+ return Some ( format ! ( "file:///{}" , s. trim_start_matches( '/' ) ) ) ;
116+ }
117+
118+ let s = abs. to_string_lossy ( ) . replace ( '\\' , "/" ) ;
119+ Some ( format ! ( "file:///{}" , s. trim_start_matches( '/' ) ) )
120+ }
121+
71122#[ tauri:: command]
72123pub fn pty_resize (
73124 session_id : String , cols : u16 , rows : u16 ,
0 commit comments