diff --git a/packages/app/src/context/global-sdk.tsx b/packages/app/src/context/global-sdk.tsx index dc8f937ff..7d93682bf 100644 --- a/packages/app/src/context/global-sdk.tsx +++ b/packages/app/src/context/global-sdk.tsx @@ -9,11 +9,13 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo name: "GlobalSDK", init: () => { const server = useServer() + const platform = usePlatform() const abort = new AbortController() const eventSdk = createOpencodeClient({ baseUrl: server.url, signal: abort.signal, + fetch: platform.fetch, }) const emitter = createGlobalEmitter<{ [key: string]: Event @@ -93,7 +95,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo stop() }) - const platform = usePlatform() const sdk = createOpencodeClient({ baseUrl: server.url, fetch: platform.fetch, diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index a0656c5fc..ddac1f228 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -26,6 +26,7 @@ import { ErrorPage, type InitError } from "../pages/error" import { batch, createContext, useContext, onCleanup, onMount, type ParentProps, Switch, Match } from "solid-js" import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/util/path" +import { usePlatform } from "./platform" type State = { status: "loading" | "partial" | "complete" @@ -64,6 +65,7 @@ type State = { function createGlobalSync() { const globalSDK = useGlobalSDK() + const platform = usePlatform() const [globalStore, setGlobalStore] = createStore<{ ready: boolean error?: InitError @@ -139,6 +141,7 @@ function createGlobalSync() { const [store, setStore] = child(directory) const sdk = createOpencodeClient({ baseUrl: globalSDK.url, + fetch: platform.fetch, directory, throwOnError: true, }) @@ -396,6 +399,7 @@ function createGlobalSync() { case "lsp.updated": { const sdk = createOpencodeClient({ baseUrl: globalSDK.url, + fetch: platform.fetch, directory, throwOnError: true, }) diff --git a/packages/desktop/scripts/predev.ts b/packages/desktop/scripts/predev.ts index 3d0cd5e92..3e14250b1 100644 --- a/packages/desktop/scripts/predev.ts +++ b/packages/desktop/scripts/predev.ts @@ -1,12 +1,12 @@ import { $ } from "bun" -import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils" +import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils" const RUST_TARGET = Bun.env.TAURI_ENV_TARGET_TRIPLE const sidecarConfig = getCurrentSidecar(RUST_TARGET) -const binaryPath = `../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}` +const binaryPath = windowsify(`../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode`) await $`cd ../opencode && bun run build --single` diff --git a/packages/desktop/scripts/prepare.ts b/packages/desktop/scripts/prepare.ts index 495a0baea..24ff9e7e0 100755 --- a/packages/desktop/scripts/prepare.ts +++ b/packages/desktop/scripts/prepare.ts @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { $ } from "bun" -import { copyBinaryToSidecarFolder, getCurrentSidecar } from "./utils" +import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils" const sidecarConfig = getCurrentSidecar() @@ -10,6 +10,4 @@ const dir = "src-tauri/target/opencode-binaries" await $`mkdir -p ${dir}` await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir) -await copyBinaryToSidecarFolder( - `${dir}/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`, -) +await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`)) diff --git a/packages/desktop/scripts/utils.ts b/packages/desktop/scripts/utils.ts index 885d0afce..c3019f0b9 100644 --- a/packages/desktop/scripts/utils.ts +++ b/packages/desktop/scripts/utils.ts @@ -41,8 +41,13 @@ export function getCurrentSidecar(target = RUST_TARGET) { export async function copyBinaryToSidecarFolder(source: string, target = RUST_TARGET) { await $`mkdir -p src-tauri/sidecars` - const dest = `src-tauri/sidecars/opencode-cli-${target}${process.platform === "win32" ? ".exe" : ""}` + const dest = windowsify(`src-tauri/sidecars/opencode-cli-${target}`) await $`cp ${source} ${dest}` console.log(`Copied ${source} to ${dest}`) } + +export function windowsify(path: string) { + if (path.endsWith(".exe")) return path + return `${path}${process.platform === "win32" ? ".exe" : ""}` +} diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 92953ea19..43f24a6ad 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2814,6 +2814,7 @@ dependencies = [ "tauri-plugin-updater", "tauri-plugin-window-state", "tokio", + "uuid", "webkit2gtk", ] @@ -5364,13 +5365,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index 8033d4f14..3145ae4b2 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -39,6 +39,7 @@ tauri-plugin-os = "2" futures = "0.3.31" semver = "1.0.27" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +uuid = { version = "1.19.0", features = ["v4"] } [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index b479ed0b6..e2682ec71 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -3,6 +3,7 @@ mod window_customizer; use cli::{install_cli, sync_cli}; use futures::FutureExt; +use futures::future; use std::{ collections::VecDeque, net::TcpListener, @@ -13,22 +14,29 @@ use tauri::{AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl, Webvie use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_store::StoreExt; +use tokio::sync::oneshot; use crate::window_customizer::PinchZoomDisablePlugin; const SETTINGS_STORE: &str = "opencode.settings.dat"; const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; +#[derive(Clone, serde::Serialize)] +struct ServerReadyData { + url: String, + password: Option, +} + #[derive(Clone)] struct ServerState { child: Arc>>, - status: futures::future::Shared>>, + status: future::Shared>>, } impl ServerState { pub fn new( child: Option, - status: tokio::sync::oneshot::Receiver>, + status: oneshot::Receiver>, ) -> Self { Self { child: Arc::new(Mutex::new(child)), @@ -80,7 +88,7 @@ async fn get_logs(app: AppHandle) -> Result { } #[tauri::command] -async fn ensure_server_ready(state: State<'_, ServerState>) -> Result { +async fn ensure_server_ready(state: State<'_, ServerState>) -> Result { state .status .clone() @@ -137,13 +145,14 @@ fn get_sidecar_port() -> u32 { }) as u32 } -fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { +fn spawn_sidecar(app: &AppHandle, port: u32, password: &str) -> CommandChild { let log_state = app.state::(); let log_state_clone = log_state.inner().clone(); println!("spawning sidecar on port {port}"); let (mut rx, child) = cli::create_command(app, format!("serve --port {port}").as_str()) + .env("OPENCODE_SERVER_PASSWORD", password) .spawn() .expect("Failed to spawn opencode"); @@ -184,7 +193,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { child } -async fn check_server_health(url: &str) -> bool { +async fn check_server_health(url: &str, password: Option<&str>) -> bool { let health_url = format!("{}/health", url.trim_end_matches('/')); let client = reqwest::Client::builder() .timeout(Duration::from_secs(3)) @@ -194,9 +203,13 @@ async fn check_server_health(url: &str) -> bool { return false; }; - client - .get(&health_url) - .send() + let mut req = client.get(&health_url); + + if let Some(password) = password { + req = req.basic_auth("opencode", Some(password)); + } + + req.send() .await .map(|r| r.status().is_success()) .unwrap_or(false) @@ -267,7 +280,7 @@ pub fn run() { window_builder.build().expect("Failed to create window"); - let (tx, rx) = tokio::sync::oneshot::channel(); + let (tx, rx) = oneshot::channel(); app.manage(ServerState::new(None, rx)); { @@ -344,12 +357,18 @@ fn get_server_url_from_config(config: &cli::Config) -> Option { async fn setup_server_connection( app: &AppHandle, custom_url: Option, -) -> Result<(Option, String), String> { +) -> Result<(Option, ServerReadyData), String> { if let Some(url) = custom_url { loop { - if check_server_health(&url).await { + if check_server_health(&url, None).await { println!("Connected to custom server: {}", url); - return Ok((None, url.clone())); + return Ok(( + None, + ServerReadyData { + url: url.clone(), + password: None, + }, + )); } const RETRY: &str = "Retry"; @@ -374,19 +393,36 @@ async fn setup_server_connection( 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)), + if !check_server_health(&local_url, None).await { + let password = uuid::Uuid::new_v4().to_string(); + + match spawn_local_server(app, local_port, &password).await { + Ok(child) => Ok(( + Some(child), + ServerReadyData { + url: local_url, + password: Some(password), + }, + )), Err(err) => Err(err), } } else { - Ok(None) + Ok(( + None, + ServerReadyData { + url: local_url, + password: None, + }, + )) } - .map(|child| (child, local_url)) } -async fn spawn_local_server(app: &AppHandle, port: u32) -> Result { - let child = spawn_sidecar(app, port); +async fn spawn_local_server( + app: &AppHandle, + port: u32, + password: &str, +) -> Result { + let child = spawn_sidecar(app, port, password); let url = format!("http://127.0.0.1:{port}"); let timestamp = Instant::now(); @@ -400,7 +436,7 @@ async fn spawn_local_server(app: &AppHandle, port: u32) -> Result): Platform => ({ platform: "desktop", version: pkg.version, @@ -256,7 +255,25 @@ const platform: Platform = { }, // @ts-expect-error - fetch: tauriFetch, + fetch: (input, init) => { + const pw = password() + + const addHeader = (headers: Headers, password: string) => { + headers.append("Authorization", `Basic ${btoa(`opencode:${password}`)}`) + } + + if (input instanceof Request) { + if (pw) addHeader(input.headers, pw) + return tauriFetch(input) + } else { + const headers = new Headers(init?.headers) + if (pw) addHeader(headers, pw) + return tauriFetch(input, { + ...(init as any), + headers: headers, + }) + } + }, getDefaultServerUrl: async () => { const result = await invoke("get_default_server_url").catch(() => null) @@ -266,7 +283,7 @@ const platform: Platform = { setDefaultServerUrl: async (url: string | null) => { await invoke("set_default_server_url", { url }) }, -} +}) createMenu() @@ -276,26 +293,37 @@ root?.addEventListener("mousewheel", (e) => { }) render(() => { + const [serverPassword, setServerPassword] = createSignal(null) + const platform = createPlatform(() => serverPassword()) + return ( - {ostype() === "macos" && ( -
- )} - {(serverUrl) => } + {ostype() === "macos" && ( +
+ )} + + {(data) => { + setServerPassword(data().password) + + return + }} + ) }, root!) +type ServerReadyData = { url: string; password: string | null } + // Gate component that waits for the server to be ready -function ServerGate(props: { children: (url: Accessor) => JSX.Element }) { - const [serverUrl] = createResource(() => invoke("ensure_server_ready")) +function ServerGate(props: { children: (data: Accessor) => JSX.Element }) { + const [serverData] = createResource(() => invoke("ensure_server_ready")) return ( // Not using suspense as not all components are compatible with it (undefined refs) @@ -303,7 +331,7 @@ function ServerGate(props: { children: (url: Accessor) => JSX.Element })
} > - {(serverUrl) => props.children(serverUrl)} + {(data) => props.children(data)} ) }