summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/context/global-sdk.tsx3
-rw-r--r--packages/app/src/context/global-sync.tsx4
-rw-r--r--packages/desktop/scripts/predev.ts4
-rwxr-xr-xpackages/desktop/scripts/prepare.ts6
-rw-r--r--packages/desktop/scripts/utils.ts7
-rw-r--r--packages/desktop/src-tauri/Cargo.lock7
-rw-r--r--packages/desktop/src-tauri/Cargo.toml1
-rw-r--r--packages/desktop/src-tauri/src/lib.rs76
-rw-r--r--packages/desktop/src/index.tsx54
9 files changed, 118 insertions, 44 deletions
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<String>,
+}
+
#[derive(Clone)]
struct ServerState {
child: Arc<Mutex<Option<CommandChild>>>,
- status: futures::future::Shared<tokio::sync::oneshot::Receiver<Result<String, String>>>,
+ status: future::Shared<oneshot::Receiver<Result<ServerReadyData, String>>>,
}
impl ServerState {
pub fn new(
child: Option<CommandChild>,
- status: tokio::sync::oneshot::Receiver<Result<String, String>>,
+ status: oneshot::Receiver<Result<ServerReadyData, String>>,
) -> Self {
Self {
child: Arc::new(Mutex::new(child)),
@@ -80,7 +88,7 @@ async fn get_logs(app: AppHandle) -> Result<String, String> {
}
#[tauri::command]
-async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<String, String> {
+async fn ensure_server_ready(state: State<'_, ServerState>) -> Result<ServerReadyData, String> {
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::<LogState>();
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<String> {
async fn setup_server_connection(
app: &AppHandle,
custom_url: Option<String>,
-) -> Result<(Option<CommandChild>, String), String> {
+) -> Result<(Option<CommandChild>, 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<CommandChild, String> {
- let child = spawn_sidecar(app, port);
+async fn spawn_local_server(
+ app: &AppHandle,
+ port: u32,
+ password: &str,
+) -> Result<CommandChild, String> {
+ 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<CommandChild,
tokio::time::sleep(Duration::from_millis(10)).await;
- if check_server_health(&url).await {
+ if check_server_health(&url, Some(password)).await {
println!("Server ready after {:?}", timestamp.elapsed());
break Ok(child);
}
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index a26cc61ca..1ad015aff 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -13,12 +13,11 @@ import { AsyncStorage } from "@solid-primitives/storage"
import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
import { Store } from "@tauri-apps/plugin-store"
import { Logo } from "@opencode-ai/ui/logo"
-import { Accessor, JSX, createResource } from "solid-js"
+import { createSignal, Show, Accessor, JSX, createResource } from "solid-js"
import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu"
import pkg from "../package.json"
-import { Show } from "solid-js"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -29,7 +28,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
let update: Update | null = null
-const platform: Platform = {
+const createPlatform = (password: Accessor<string | null>): 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<string | null>("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<string | null>(null)
+ const platform = createPlatform(() => serverPassword())
+
return (
<PlatformProvider value={platform}>
- {ostype() === "macos" && (
- <div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
- )}
<AppBaseProviders>
- <ServerGate>{(serverUrl) => <AppInterface defaultUrl={serverUrl()} />}</ServerGate>
+ {ostype() === "macos" && (
+ <div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
+ )}
+ <ServerGate>
+ {(data) => {
+ setServerPassword(data().password)
+
+ return <AppInterface defaultUrl={data().url} />
+ }}
+ </ServerGate>
</AppBaseProviders>
</PlatformProvider>
)
}, root!)
+type ServerReadyData = { url: string; password: string | null }
+
// Gate component that waits for the server to be ready
-function ServerGate(props: { children: (url: Accessor<string>) => JSX.Element }) {
- const [serverUrl] = createResource<string>(() => invoke("ensure_server_ready"))
+function ServerGate(props: { children: (data: Accessor<ServerReadyData>) => JSX.Element }) {
+ const [serverData] = createResource<ServerReadyData>(() => invoke("ensure_server_ready"))
return (
// Not using suspense as not all components are compatible with it (undefined refs)
<Show
- when={serverUrl.state !== "pending" && serverUrl()}
+ when={serverData.state !== "pending" && serverData()}
fallback={
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
<Logo class="w-xl opacity-12 animate-pulse" />
@@ -303,7 +331,7 @@ function ServerGate(props: { children: (url: Accessor<string>) => JSX.Element })
</div>
}
>
- {(serverUrl) => props.children(serverUrl)}
+ {(data) => props.children(data)}
</Show>
)
}