@@ -15,6 +15,7 @@ use tauri::{
1515} ;
1616use tauri_plugin_shell:: process:: { CommandChild , CommandEvent } ;
1717use tauri_plugin_shell:: ShellExt ;
18+ use tauri_plugin_store:: StoreExt ;
1819use tokio:: net:: TcpSocket ;
1920
2021use crate :: window_customizer:: PinchZoomDisablePlugin ;
@@ -45,6 +46,65 @@ impl ServerState {
4546struct LogState ( Arc < Mutex < VecDeque < String > > > ) ;
4647
4748const MAX_LOG_ENTRIES : usize = 200 ;
49+ const GLOBAL_STORAGE : & str = "opencode.global.dat" ;
50+
51+ /// Check if a URL's origin matches any configured server in the store.
52+ /// Returns true if the URL should be allowed for internal navigation.
53+ fn is_allowed_server ( app : & AppHandle , url : & tauri:: Url ) -> bool {
54+ // Always allow localhost and 127.0.0.1
55+ if let Some ( host) = url. host_str ( ) {
56+ if host == "localhost" || host == "127.0.0.1" {
57+ return true ;
58+ }
59+ }
60+
61+ // Try to read the server list from the store
62+ let Ok ( store) = app. store ( GLOBAL_STORAGE ) else {
63+ return false ;
64+ } ;
65+
66+ let Some ( server_data) = store. get ( "server" ) else {
67+ return false ;
68+ } ;
69+
70+ // Parse the server list from the stored JSON
71+ let Some ( list) = server_data. get ( "list" ) . and_then ( |v| v. as_array ( ) ) else {
72+ return false ;
73+ } ;
74+
75+ // Get the origin of the navigation URL (scheme + host + port)
76+ let url_origin = format ! (
77+ "{}://{}{}" ,
78+ url. scheme( ) ,
79+ url. host_str( ) . unwrap_or( "" ) ,
80+ url. port( ) . map( |p| format!( ":{}" , p) ) . unwrap_or_default( )
81+ ) ;
82+
83+ // Check if any configured server matches the URL's origin
84+ for server in list {
85+ let Some ( server_url) = server. as_str ( ) else {
86+ continue ;
87+ } ;
88+
89+ // Parse the server URL to extract its origin
90+ let Ok ( parsed) = tauri:: Url :: parse ( server_url) else {
91+ continue ;
92+ } ;
93+
94+ let server_origin = format ! (
95+ "{}://{}{}" ,
96+ parsed. scheme( ) ,
97+ parsed. host_str( ) . unwrap_or( "" ) ,
98+ parsed. port( ) . map( |p| format!( ":{}" , p) ) . unwrap_or_default( )
99+ ) ;
100+
101+ if url_origin == server_origin {
102+ return true ;
103+ }
104+ }
105+
106+ false
107+ }
48108
49109#[ tauri:: command]
50110fn kill_sidecar ( app : AppHandle ) {
@@ -236,13 +296,30 @@ pub fn run() {
236296 . unwrap_or ( LogicalSize :: new ( 1920 , 1080 ) ) ;
237297
238298 // Create window immediately with serverReady = false
299+ let app_for_nav = app. clone ( ) ;
239300 let mut window_builder =
240301 WebviewWindow :: builder ( & app, "main" , WebviewUrl :: App ( "/" . into ( ) ) )
241302 . title ( "OpenCode" )
242303 . inner_size ( size. width as f64 , size. height as f64 )
243304 . decorations ( true )
244305 . zoom_hotkeys_enabled ( true )
245306 . disable_drag_drop_handler ( )
307+ . on_navigation ( move |url| {
308+ // Allow internal navigation (tauri:// scheme)
309+ if url. scheme ( ) == "tauri" {
310+ return true ;
311+ }
312+ // Allow navigation to configured servers (localhost, 127.0.0.1, or remote)
313+ if is_allowed_server ( & app_for_nav, url) {
314+ return true ;
315+ }
316+ // Open external http/https URLs in default browser
317+ if url. scheme ( ) == "http" || url. scheme ( ) == "https" {
318+ let _ = app_for_nav. shell ( ) . open ( url. as_str ( ) , None ) ;
319+ return false ; // Cancel internal navigation
320+ }
321+ true
322+ } )
246323 . initialization_script ( format ! (
247324 r#"
248325 window.__OPENCODE__ ??= {{}};
0 commit comments