Skip to content

Commit 361a962

Browse files
authored
fix(desktop): open external links in default browser (#7221)
1 parent fa9c283 commit 361a962

File tree

1 file changed

+77
-0
lines changed
  • packages/desktop/src-tauri/src

1 file changed

+77
-0
lines changed

packages/desktop/src-tauri/src/lib.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use tauri::{
1515
};
1616
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
1717
use tauri_plugin_shell::ShellExt;
18+
use tauri_plugin_store::StoreExt;
1819
use tokio::net::TcpSocket;
1920

2021
use crate::window_customizer::PinchZoomDisablePlugin;
@@ -45,6 +46,65 @@ impl ServerState {
4546
struct LogState(Arc<Mutex<VecDeque<String>>>);
4647

4748
const 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]
50110
fn 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

Comments
 (0)