@@ -13,12 +13,16 @@ use tauri::{
1313 path:: BaseDirectory , AppHandle , LogicalSize , Manager , RunEvent , State , WebviewUrl ,
1414 WebviewWindow ,
1515} ;
16+ use tauri_plugin_dialog:: { DialogExt , MessageDialogButtons , MessageDialogResult } ;
1617use tauri_plugin_shell:: process:: { CommandChild , CommandEvent } ;
1718use tauri_plugin_shell:: ShellExt ;
1819use tokio:: net:: TcpSocket ;
1920
2021use crate :: window_customizer:: PinchZoomDisablePlugin ;
2122
23+ const SETTINGS_STORE : & str = "opencode.settings.dat" ;
24+ const DEFAULT_SERVER_URL_KEY : & str = "defaultServerUrl" ;
25+
2226#[ derive( Clone ) ]
2327struct ServerState {
2428 child : Arc < Mutex < Option < CommandChild > > > ,
@@ -88,6 +92,41 @@ async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), Stri
8892 . map_err ( |_| "Failed to get server status" . to_string ( ) ) ?
8993}
9094
95+ #[ tauri:: command]
96+ async fn get_default_server_url ( app : AppHandle ) -> Result < Option < String > , String > {
97+ let store = app
98+ . store ( SETTINGS_STORE )
99+ . map_err ( |e| format ! ( "Failed to open settings store: {}" , e) ) ?;
100+
101+ let value = store. get ( DEFAULT_SERVER_URL_KEY ) ;
102+ match value {
103+ Some ( v) => Ok ( v. as_str ( ) . map ( String :: from) ) ,
104+ None => Ok ( None ) ,
105+ }
106+ }
107+
108+ #[ tauri:: command]
109+ async fn set_default_server_url ( app : AppHandle , url : Option < String > ) -> Result < ( ) , String > {
110+ let store = app
111+ . store ( SETTINGS_STORE )
112+ . map_err ( |e| format ! ( "Failed to open settings store: {}" , e) ) ?;
113+
114+ match url {
115+ Some ( u) => {
116+ store. set ( DEFAULT_SERVER_URL_KEY , serde_json:: Value :: String ( u) ) ;
117+ }
118+ None => {
119+ store. delete ( DEFAULT_SERVER_URL_KEY ) ;
120+ }
121+ }
122+
123+ store
124+ . save ( )
125+ . map_err ( |e| format ! ( "Failed to save settings: {}" , e) ) ?;
126+
127+ Ok ( ( ) )
128+ }
129+
91130fn get_sidecar_port ( ) -> u32 {
92131 option_env ! ( "OPENCODE_PORT" )
93132 . map ( |s| s. to_string ( ) )
@@ -193,6 +232,30 @@ async fn is_server_running(port: u32) -> bool {
193232 . is_ok ( )
194233}
195234
235+ async fn check_server_health ( url : & str ) -> bool {
236+ let health_url = format ! ( "{}/health" , url. trim_end_matches( '/' ) ) ;
237+ let client = reqwest:: Client :: builder ( )
238+ . timeout ( Duration :: from_secs ( 3 ) )
239+ . build ( ) ;
240+
241+ let Ok ( client) = client else {
242+ return false ;
243+ } ;
244+
245+ client
246+ . get ( & health_url)
247+ . send ( )
248+ . await
249+ . map ( |r| r. status ( ) . is_success ( ) )
250+ . unwrap_or ( false )
251+ }
252+
253+ fn get_configured_server_url ( app : & AppHandle ) -> Option < String > {
254+ let store = app. store ( SETTINGS_STORE ) . ok ( ) ?;
255+ let value = store. get ( DEFAULT_SERVER_URL_KEY ) ?;
256+ value. as_str ( ) . map ( String :: from)
257+ }
258+
196259#[ cfg_attr( mobile, tauri:: mobile_entry_point) ]
197260pub fn run ( ) {
198261 let updater_enabled = option_env ! ( "TAURI_SIGNING_PRIVATE_KEY" ) . is_some ( ) ;
@@ -219,7 +282,9 @@ pub fn run() {
219282 . invoke_handler ( tauri:: generate_handler![
220283 kill_sidecar,
221284 install_cli,
222- ensure_server_started
285+ ensure_server_started,
286+ get_default_server_url,
287+ set_default_server_url
223288 ] )
224289 . setup ( move |app| {
225290 let app = app. handle ( ) . clone ( ) ;
@@ -266,41 +331,114 @@ pub fn run() {
266331 {
267332 let app = app. clone ( ) ;
268333 tauri:: async_runtime:: spawn ( async move {
269- let should_spawn_sidecar = !is_server_running ( port) . await ;
270-
271- let ( child, res) = if should_spawn_sidecar {
272- let child = spawn_sidecar ( & app, port) ;
273-
274- let timestamp = Instant :: now ( ) ;
275- let res = loop {
276- if timestamp. elapsed ( ) > Duration :: from_secs ( 7 ) {
277- break Err ( format ! (
278- "Failed to spawn OpenCode Server. Logs:\n {}" ,
279- get_logs( app. clone( ) ) . await . unwrap( )
280- ) ) ;
281- }
334+ // Check for configured default server URL
335+ let configured_url = get_configured_server_url ( & app) ;
282336
283- tokio:: time:: sleep ( Duration :: from_millis ( 10 ) ) . await ;
337+ let ( child, res, server_url) = if let Some ( ref url) = configured_url {
338+ println ! ( "Configured default server URL: {}" , url) ;
284339
285- if is_server_running ( port ) . await {
286- // give the server a little bit more time to warm up
287- tokio :: time :: sleep ( Duration :: from_millis ( 10 ) ) . await ;
340+ // Try to connect to the configured server
341+ let mut healthy = false ;
342+ let mut should_fallback = false ;
288343
289- break Ok ( ( ) ) ;
344+ loop {
345+ if check_server_health ( url) . await {
346+ healthy = true ;
347+ println ! ( "Connected to configured server: {}" , url) ;
348+ break ;
290349 }
291- } ;
292350
293- println ! ( "Server ready after {:?}" , timestamp. elapsed( ) ) ;
351+ let res = app. dialog ( )
352+ . message ( format ! ( "Could not connect to configured server:\n {}\n \n Would you like to retry or start a local server instead?" , url) )
353+ . title ( "Connection Failed" )
354+ . buttons ( MessageDialogButtons :: OkCancelCustom ( "Retry" . to_string ( ) , "Start Local" . to_string ( ) ) )
355+ . blocking_show_with_result ( ) ;
356+
357+ match res {
358+ MessageDialogResult :: Custom ( name) if name == "Retry" => {
359+ continue ;
360+ } ,
361+ _ => {
362+ should_fallback = true ;
363+ break ;
364+ }
365+ }
366+ }
367+
368+ if healthy {
369+ ( None , Ok ( ( ) ) , Some ( url. clone ( ) ) )
370+ } else if should_fallback {
371+ // Fall back to spawning local sidecar
372+ let child = spawn_sidecar ( & app, port) ;
373+
374+ let timestamp = Instant :: now ( ) ;
375+ let res = loop {
376+ if timestamp. elapsed ( ) > Duration :: from_secs ( 7 ) {
377+ break Err ( format ! (
378+ "Failed to spawn OpenCode Server. Logs:\n {}" ,
379+ get_logs( app. clone( ) ) . await . unwrap( )
380+ ) ) ;
381+ }
294382
295- ( Some ( child) , res)
383+ tokio:: time:: sleep ( Duration :: from_millis ( 10 ) ) . await ;
384+
385+ if is_server_running ( port) . await {
386+ tokio:: time:: sleep ( Duration :: from_millis ( 10 ) ) . await ;
387+ break Ok ( ( ) ) ;
388+ }
389+ } ;
390+
391+ println ! ( "Server ready after {:?}" , timestamp. elapsed( ) ) ;
392+ ( Some ( child) , res, None )
393+ } else {
394+ ( None , Err ( "User cancelled" . to_string ( ) ) , None )
395+ }
296396 } else {
297- ( None , Ok ( ( ) ) )
397+ // No configured URL, spawn local sidecar as before
398+ let should_spawn_sidecar = !is_server_running ( port) . await ;
399+
400+ let ( child, res) = if should_spawn_sidecar {
401+ let child = spawn_sidecar ( & app, port) ;
402+
403+ let timestamp = Instant :: now ( ) ;
404+ let res = loop {
405+ if timestamp. elapsed ( ) > Duration :: from_secs ( 7 ) {
406+ break Err ( format ! (
407+ "Failed to spawn OpenCode Server. Logs:\n {}" ,
408+ get_logs( app. clone( ) ) . await . unwrap( )
409+ ) ) ;
410+ }
411+
412+ tokio:: time:: sleep ( Duration :: from_millis ( 10 ) ) . await ;
413+
414+ if is_server_running ( port) . await {
415+ tokio:: time:: sleep ( Duration :: from_millis ( 10 ) ) . await ;
416+ break Ok ( ( ) ) ;
417+ }
418+ } ;
419+
420+ println ! ( "Server ready after {:?}" , timestamp. elapsed( ) ) ;
421+
422+ ( Some ( child) , res)
423+ } else {
424+ ( None , Ok ( ( ) ) )
425+ } ;
426+
427+ ( child, res, None )
298428 } ;
299429
300430 app. state :: < ServerState > ( ) . set_child ( child) ;
301431
302432 if res. is_ok ( ) {
303433 let _ = window. eval ( "window.__OPENCODE__.serverReady = true;" ) ;
434+
435+ // If using a configured server URL, inject it
436+ if let Some ( url) = server_url {
437+ let escaped_url = url. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " ) ;
438+ let _ = window. eval ( format ! (
439+ "window.__OPENCODE__.serverUrl = \" {escaped_url}\" ;" ,
440+ ) ) ;
441+ }
304442 }
305443
306444 let _ = tx. send ( res) ;
0 commit comments