|
|
@@ -1,23 +1,18 @@
|
|
|
mod cli;
|
|
|
mod window_customizer;
|
|
|
|
|
|
-use cli::{get_sidecar_path, install_cli, sync_cli};
|
|
|
+use cli::{install_cli, sync_cli};
|
|
|
use futures::FutureExt;
|
|
|
use std::{
|
|
|
collections::VecDeque,
|
|
|
- net::{SocketAddr, TcpListener},
|
|
|
+ net::TcpListener,
|
|
|
sync::{Arc, Mutex},
|
|
|
time::{Duration, Instant},
|
|
|
};
|
|
|
-use tauri::{
|
|
|
- path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl,
|
|
|
- WebviewWindow,
|
|
|
-};
|
|
|
+use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, WebviewWindow};
|
|
|
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
|
|
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
|
|
-use tauri_plugin_shell::ShellExt;
|
|
|
use tauri_plugin_store::StoreExt;
|
|
|
-use tokio::net::TcpSocket;
|
|
|
|
|
|
use crate::window_customizer::PinchZoomDisablePlugin;
|
|
|
|
|
|
@@ -27,13 +22,13 @@ const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl";
|
|
|
#[derive(Clone)]
|
|
|
struct ServerState {
|
|
|
child: Arc<Mutex<Option<CommandChild>>>,
|
|
|
- status: futures::future::Shared<tokio::sync::oneshot::Receiver<Result<(), String>>>,
|
|
|
+ status: futures::future::Shared<tokio::sync::oneshot::Receiver<Result<String, String>>>,
|
|
|
}
|
|
|
|
|
|
impl ServerState {
|
|
|
pub fn new(
|
|
|
child: Option<CommandChild>,
|
|
|
- status: tokio::sync::oneshot::Receiver<Result<(), String>>,
|
|
|
+ status: tokio::sync::oneshot::Receiver<Result<String, String>>,
|
|
|
) -> Self {
|
|
|
Self {
|
|
|
child: Arc::new(Mutex::new(child)),
|
|
|
@@ -85,7 +80,7 @@ async fn get_logs(app: AppHandle) -> Result<String, String> {
|
|
|
}
|
|
|
|
|
|
#[tauri::command]
|
|
|
-async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), String> {
|
|
|
+async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<String, String> {
|
|
|
state
|
|
|
.status
|
|
|
.clone()
|
|
|
@@ -94,7 +89,7 @@ async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), Stri
|
|
|
}
|
|
|
|
|
|
#[tauri::command]
|
|
|
-async fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
|
|
|
+fn get_default_server_url(app: AppHandle) -> Result<Option<String>, String> {
|
|
|
let store = app
|
|
|
.store(SETTINGS_STORE)
|
|
|
.map_err(|e| format!("Failed to open settings store: {}", e))?;
|
|
|
@@ -142,49 +137,16 @@ fn get_sidecar_port() -> u32 {
|
|
|
}) as u32
|
|
|
}
|
|
|
|
|
|
-fn get_user_shell() -> String {
|
|
|
- std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
|
|
|
-}
|
|
|
-
|
|
|
fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
|
|
|
let log_state = app.state::<LogState>();
|
|
|
let log_state_clone = log_state.inner().clone();
|
|
|
|
|
|
- let state_dir = app
|
|
|
- .path()
|
|
|
- .resolve("", BaseDirectory::AppLocalData)
|
|
|
- .expect("Failed to resolve app local data dir");
|
|
|
-
|
|
|
- #[cfg(target_os = "windows")]
|
|
|
- let (mut rx, child) = app
|
|
|
- .shell()
|
|
|
- .sidecar("opencode-cli")
|
|
|
- .unwrap()
|
|
|
- .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
|
|
|
- .env("OPENCODE_CLIENT", "desktop")
|
|
|
- .env("XDG_STATE_HOME", &state_dir)
|
|
|
- .args(["serve", &format!("--port={port}")])
|
|
|
+ println!("spawning sidecar on port {port}");
|
|
|
+
|
|
|
+ let (mut rx, child) = cli::create_command(app, format!("serve --port {port}").as_str())
|
|
|
.spawn()
|
|
|
.expect("Failed to spawn opencode");
|
|
|
|
|
|
- #[cfg(not(target_os = "windows"))]
|
|
|
- let (mut rx, child) = {
|
|
|
- let sidecar = get_sidecar_path(app);
|
|
|
- let shell = get_user_shell();
|
|
|
- app.shell()
|
|
|
- .command(&shell)
|
|
|
- .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
|
|
|
- .env("OPENCODE_CLIENT", "desktop")
|
|
|
- .env("XDG_STATE_HOME", &state_dir)
|
|
|
- .args([
|
|
|
- "-il",
|
|
|
- "-c",
|
|
|
- &format!("\"{}\" serve --port={}", sidecar.display(), port),
|
|
|
- ])
|
|
|
- .spawn()
|
|
|
- .expect("Failed to spawn opencode")
|
|
|
- };
|
|
|
-
|
|
|
tauri::async_runtime::spawn(async move {
|
|
|
while let Some(event) = rx.recv().await {
|
|
|
match event {
|
|
|
@@ -222,17 +184,6 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
|
|
|
child
|
|
|
}
|
|
|
|
|
|
-async fn is_server_running(port: u32) -> bool {
|
|
|
- TcpSocket::new_v4()
|
|
|
- .unwrap()
|
|
|
- .connect(SocketAddr::new(
|
|
|
- "127.0.0.1".parse().expect("Failed to parse IP"),
|
|
|
- port as u16,
|
|
|
- ))
|
|
|
- .await
|
|
|
- .is_ok()
|
|
|
-}
|
|
|
-
|
|
|
async fn check_server_health(url: &str) -> bool {
|
|
|
let health_url = format!("{}/health", url.trim_end_matches('/'));
|
|
|
let client = reqwest::Client::builder()
|
|
|
@@ -251,12 +202,6 @@ async fn check_server_health(url: &str) -> bool {
|
|
|
.unwrap_or(false)
|
|
|
}
|
|
|
|
|
|
-fn get_configured_server_url(app: &AppHandle) -> Option<String> {
|
|
|
- let store = app.store(SETTINGS_STORE).ok()?;
|
|
|
- let value = store.get(DEFAULT_SERVER_URL_KEY)?;
|
|
|
- value.as_str().map(String::from)
|
|
|
-}
|
|
|
-
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
|
pub fn run() {
|
|
|
let updater_enabled = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some();
|
|
|
@@ -283,7 +228,7 @@ pub fn run() {
|
|
|
.invoke_handler(tauri::generate_handler![
|
|
|
kill_sidecar,
|
|
|
install_cli,
|
|
|
- ensure_server_started,
|
|
|
+ ensure_server_ready,
|
|
|
get_default_server_url,
|
|
|
set_default_server_url
|
|
|
])
|
|
|
@@ -293,15 +238,11 @@ pub fn run() {
|
|
|
// Initialize log state
|
|
|
app.manage(LogState(Arc::new(Mutex::new(VecDeque::new()))));
|
|
|
|
|
|
- // Get port and create window immediately for faster perceived startup
|
|
|
- let port = get_sidecar_port();
|
|
|
-
|
|
|
let primary_monitor = app.primary_monitor().ok().flatten();
|
|
|
let size = primary_monitor
|
|
|
.map(|m| m.size().to_logical(m.scale_factor()))
|
|
|
.unwrap_or(LogicalSize::new(1920, 1080));
|
|
|
|
|
|
- // Create window immediately with serverReady = false
|
|
|
#[allow(unused_mut)]
|
|
|
let mut window_builder =
|
|
|
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
|
|
|
@@ -314,7 +255,6 @@ pub fn run() {
|
|
|
r#"
|
|
|
window.__OPENCODE__ ??= {{}};
|
|
|
window.__OPENCODE__.updaterEnabled = {updater_enabled};
|
|
|
- window.__OPENCODE__.port = {port};
|
|
|
"#
|
|
|
));
|
|
|
|
|
|
@@ -325,7 +265,7 @@ pub fn run() {
|
|
|
.hidden_title(true);
|
|
|
}
|
|
|
|
|
|
- let window = window_builder.build().expect("Failed to create window");
|
|
|
+ window_builder.build().expect("Failed to create window");
|
|
|
|
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
|
app.manage(ServerState::new(None, rx));
|
|
|
@@ -333,115 +273,28 @@ pub fn run() {
|
|
|
{
|
|
|
let app = app.clone();
|
|
|
tauri::async_runtime::spawn(async move {
|
|
|
- // Check for configured default server URL
|
|
|
- let configured_url = get_configured_server_url(&app);
|
|
|
-
|
|
|
- let (child, res, server_url) = if let Some(ref url) = configured_url {
|
|
|
- println!("Configured default server URL: {}", url);
|
|
|
-
|
|
|
- // Try to connect to the configured server
|
|
|
- let mut healthy = false;
|
|
|
- let mut should_fallback = false;
|
|
|
-
|
|
|
- loop {
|
|
|
- if check_server_health(url).await {
|
|
|
- healthy = true;
|
|
|
- println!("Connected to configured server: {}", url);
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- let res = app.dialog()
|
|
|
- .message(format!("Could not connect to configured server:\n{}\n\nWould you like to retry or start a local server instead?", url))
|
|
|
- .title("Connection Failed")
|
|
|
- .buttons(MessageDialogButtons::OkCancelCustom("Retry".to_string(), "Start Local".to_string()))
|
|
|
- .blocking_show_with_result();
|
|
|
-
|
|
|
- match res {
|
|
|
- MessageDialogResult::Custom(name) if name == "Retry" => {
|
|
|
- continue;
|
|
|
- },
|
|
|
- _ => {
|
|
|
- should_fallback = true;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if healthy {
|
|
|
- (None, Ok(()), Some(url.clone()))
|
|
|
- } else if should_fallback {
|
|
|
- // Fall back to spawning local sidecar
|
|
|
- let child = spawn_sidecar(&app, port);
|
|
|
-
|
|
|
- let timestamp = Instant::now();
|
|
|
- let res = loop {
|
|
|
- if timestamp.elapsed() > Duration::from_secs(7) {
|
|
|
- break Err(format!(
|
|
|
- "Failed to spawn OpenCode Server. Logs:\n{}",
|
|
|
- get_logs(app.clone()).await.unwrap()
|
|
|
- ));
|
|
|
- }
|
|
|
-
|
|
|
- tokio::time::sleep(Duration::from_millis(10)).await;
|
|
|
-
|
|
|
- if is_server_running(port).await {
|
|
|
- tokio::time::sleep(Duration::from_millis(10)).await;
|
|
|
- break Ok(());
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- println!("Server ready after {:?}", timestamp.elapsed());
|
|
|
- (Some(child), res, None)
|
|
|
- } else {
|
|
|
- (None, Err("User cancelled".to_string()), None)
|
|
|
- }
|
|
|
- } else {
|
|
|
- // No configured URL, spawn local sidecar as before
|
|
|
- let should_spawn_sidecar = !is_server_running(port).await;
|
|
|
-
|
|
|
- let (child, res) = if should_spawn_sidecar {
|
|
|
- let child = spawn_sidecar(&app, port);
|
|
|
-
|
|
|
- let timestamp = Instant::now();
|
|
|
- let res = loop {
|
|
|
- if timestamp.elapsed() > Duration::from_secs(7) {
|
|
|
- break Err(format!(
|
|
|
- "Failed to spawn OpenCode Server. Logs:\n{}",
|
|
|
- get_logs(app.clone()).await.unwrap()
|
|
|
- ));
|
|
|
- }
|
|
|
-
|
|
|
- tokio::time::sleep(Duration::from_millis(10)).await;
|
|
|
-
|
|
|
- if is_server_running(port).await {
|
|
|
- tokio::time::sleep(Duration::from_millis(10)).await;
|
|
|
- break Ok(());
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- println!("Server ready after {:?}", timestamp.elapsed());
|
|
|
-
|
|
|
- (Some(child), res)
|
|
|
- } else {
|
|
|
- (None, Ok(()))
|
|
|
- };
|
|
|
-
|
|
|
- (child, res, None)
|
|
|
- };
|
|
|
+ let mut custom_url = None;
|
|
|
|
|
|
- app.state::<ServerState>().set_child(child);
|
|
|
+ if let Some(url) = get_default_server_url(app.clone()).ok().flatten() {
|
|
|
+ println!("Using desktop-specific custom URL: {url}");
|
|
|
+ custom_url = Some(url);
|
|
|
+ }
|
|
|
|
|
|
- if res.is_ok() {
|
|
|
- let _ = window.eval("window.__OPENCODE__.serverReady = true;");
|
|
|
+ if custom_url.is_none()
|
|
|
+ && let Some(cli_config) = cli::get_config(&app).await
|
|
|
+ && let Some(url) = get_server_url_from_config(&cli_config)
|
|
|
+ {
|
|
|
+ println!("Using custom server URL from config: {url}");
|
|
|
+ custom_url = Some(url);
|
|
|
+ }
|
|
|
|
|
|
- // If using a configured server URL, inject it
|
|
|
- if let Some(url) = server_url {
|
|
|
- let escaped_url = url.replace('\\', "\\\\").replace('"', "\\\"");
|
|
|
- let _ = window.eval(format!(
|
|
|
- "window.__OPENCODE__.serverUrl = \"{escaped_url}\";",
|
|
|
- ));
|
|
|
+ let res = match setup_server_connection(&app, custom_url).await {
|
|
|
+ Ok((child, url)) => {
|
|
|
+ app.state::<ServerState>().set_child(child);
|
|
|
+ Ok(url)
|
|
|
}
|
|
|
- }
|
|
|
+ Err(e) => Err(e),
|
|
|
+ };
|
|
|
|
|
|
let _ = tx.send(res);
|
|
|
});
|
|
|
@@ -474,3 +327,82 @@ pub fn run() {
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
+
|
|
|
+fn get_server_url_from_config(config: &cli::Config) -> Option<String> {
|
|
|
+ let server = config.server.as_ref()?;
|
|
|
+ let port = server.port?;
|
|
|
+ println!("server.port found in OC config: {port}");
|
|
|
+ let hostname = server.hostname.as_ref();
|
|
|
+
|
|
|
+ Some(format!(
|
|
|
+ "http://{}:{}",
|
|
|
+ hostname.map(|v| v.as_str()).unwrap_or("127.0.0.1"),
|
|
|
+ port
|
|
|
+ ))
|
|
|
+}
|
|
|
+
|
|
|
+async fn setup_server_connection(
|
|
|
+ app: &AppHandle,
|
|
|
+ custom_url: Option<String>,
|
|
|
+) -> Result<(Option<CommandChild>, String), String> {
|
|
|
+ if let Some(url) = custom_url {
|
|
|
+ loop {
|
|
|
+ if check_server_health(&url).await {
|
|
|
+ println!("Connected to custom server: {}", url);
|
|
|
+ return Ok((None, url.clone()));
|
|
|
+ }
|
|
|
+
|
|
|
+ const RETRY: &str = "Retry";
|
|
|
+
|
|
|
+ let res = app.dialog()
|
|
|
+ .message(format!("Could not connect to configured server:\n{}\n\nWould you like to retry or start a local server instead?", url))
|
|
|
+ .title("Connection Failed")
|
|
|
+ .buttons(MessageDialogButtons::OkCancelCustom(RETRY.to_string(), "Start Local".to_string()))
|
|
|
+ .blocking_show_with_result();
|
|
|
+
|
|
|
+ match res {
|
|
|
+ MessageDialogResult::Custom(name) if name == RETRY => {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ _ => {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let local_port = get_sidecar_port();
|
|
|
+ let local_url = format!("http://127.0.0.1:{local_port}");
|
|
|
+
|
|
|
+ if !check_server_health(&local_url).await {
|
|
|
+ match spawn_local_server(app, local_port).await {
|
|
|
+ Ok(child) => Ok(Some(child)),
|
|
|
+ Err(err) => Err(err),
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ Ok(None)
|
|
|
+ }
|
|
|
+ .map(|child| (child, local_url))
|
|
|
+}
|
|
|
+
|
|
|
+async fn spawn_local_server(app: &AppHandle, port: u32) -> Result<CommandChild, String> {
|
|
|
+ let child = spawn_sidecar(app, port);
|
|
|
+ let url = format!("http://127.0.0.1:{port}");
|
|
|
+
|
|
|
+ let timestamp = Instant::now();
|
|
|
+ loop {
|
|
|
+ if timestamp.elapsed() > Duration::from_secs(7) {
|
|
|
+ break Err(format!(
|
|
|
+ "Failed to spawn OpenCode Server. Logs:\n{}",
|
|
|
+ get_logs(app.clone()).await.unwrap()
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ tokio::time::sleep(Duration::from_millis(10)).await;
|
|
|
+
|
|
|
+ if check_server_health(&url).await {
|
|
|
+ println!("Server ready after {:?}", timestamp.elapsed());
|
|
|
+ break Ok(child);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|