From 563f193f3c3d7ca93e7f98a3fa19dcb8e1d91b65 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 10 Feb 2026 10:56:39 +0800 Subject: [PATCH 01/10] basic wsl impl --- .../app/src/components/settings-general.tsx | 66 +++++++++- packages/app/src/context/platform.tsx | 9 ++ packages/app/src/i18n/ar.ts | 5 + packages/app/src/i18n/br.ts | 5 + packages/app/src/i18n/bs.ts | 5 + packages/app/src/i18n/da.ts | 5 + packages/app/src/i18n/de.ts | 5 + packages/app/src/i18n/en.ts | 5 + packages/app/src/i18n/es.ts | 5 + packages/app/src/i18n/fr.ts | 5 + packages/app/src/i18n/ja.ts | 5 + packages/app/src/i18n/ko.ts | 5 + packages/app/src/i18n/no.ts | 5 + packages/app/src/i18n/pl.ts | 5 + packages/app/src/i18n/ru.ts | 5 + packages/app/src/i18n/th.ts | 5 + packages/app/src/i18n/zh.ts | 5 + packages/app/src/i18n/zht.ts | 5 + packages/app/src/pages/home.tsx | 3 +- packages/app/src/pages/layout.tsx | 3 +- packages/desktop/src-tauri/src/cli.rs | 114 +++++++++++++++--- packages/desktop/src-tauri/src/constants.rs | 1 + packages/desktop/src-tauri/src/lib.rs | 2 + packages/desktop/src-tauri/src/server.rs | 69 ++++++++++- packages/desktop/src/index.tsx | 41 ++++++- 25 files changed, 364 insertions(+), 24 deletions(-) diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index b31cfb6cc79..007c4147023 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -1,4 +1,4 @@ -import { Component, createMemo, type JSX } from "solid-js" +import { Component, createEffect, createMemo, type JSX, Show } from "solid-js" import { createStore } from "solid-js/store" import { Button } from "@opencode-ai/ui/button" import { Select } from "@opencode-ai/ui/select" @@ -36,8 +36,12 @@ export const SettingsGeneral: Component = () => { const platform = usePlatform() const settings = useSettings() + type BackendMode = "native" | "wsl" + const [store, setStore] = createStore({ checking: false, + backend: "native" as BackendMode, + backendReady: false, }) const check = () => { @@ -111,6 +115,34 @@ export const SettingsGeneral: Component = () => { })), ) + const backendOptions = createMemo(() => [ + { value: "native" as const, label: language.t("settings.desktop.backend.option.native") }, + { value: "wsl" as const, label: language.t("settings.desktop.backend.option.wsl") }, + ]) + + const showBackend = () => + platform.platform === "desktop" && + platform.os === "windows" && + !!platform.getBackendConfig && + !!platform.setBackendConfig + + createEffect(() => { + if (!showBackend()) return + if (store.backendReady) return + const get = platform.getBackendConfig + if (!get) { + setStore("backendReady", true) + return + } + + void Promise.resolve(get()) + .then((config) => { + const mode = config?.mode === "wsl" ? "wsl" : "native" + setStore({ backend: mode, backendReady: true }) + }) + .catch(() => setStore("backendReady", true)) + }) + const fontOptions = [ { value: "ibm-plex-mono", label: "font.option.ibmPlexMono" }, { value: "cascadia-code", label: "font.option.cascadiaCode" }, @@ -363,6 +395,38 @@ export const SettingsGeneral: Component = () => { + +
+

{language.t("settings.desktop.section.backend")}

+ +
+ + o.value === value()?.mode)} - value={(option) => option.value} - label={(option) => option.label} - onSelect={(option) => { - if (!option) return - platform.setBackendConfig?.({ mode: option.value })?.finally(() => actions.refetch()) - }} - variant="secondary" - size="small" - triggerVariant="settings" - disabled={valueResource.state === "pending"} - /> +
+ + platform.setWslConfig?.({ enabled: checked })?.finally(() => actions.refetch()) + } + /> +
diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 41e70d259ee..f753f637c8d 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -33,8 +33,8 @@ export type Platform = { /** Open directory picker dialog (native on Tauri, server-backed on web) */ openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise - /** Whether native pickers should be used (desktop only) */ - supportsNativePickers?(): boolean + /** Whether WSL integration is enabled (desktop only) */ + wslEnabled?(): boolean /** Open native file picker dialog (Tauri only) */ openFilePickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise @@ -60,11 +60,11 @@ export type Platform = { /** Set the default server URL to use on app startup (platform-specific) */ setDefaultServerUrl?(url: string | null): Promise | void - /** Get the configured backend mode (desktop only) */ - getBackendConfig?(): Promise<{ mode: "native" | "wsl" } | null> | { mode: "native" | "wsl" } | null + /** Get the configured WSL integration (desktop only) */ + getWslConfig?(): Promise<{ enabled: boolean } | null> | { enabled: boolean } | null - /** Set the configured backend mode (desktop only) */ - setBackendConfig?(config: { mode: "native" | "wsl" }): Promise | void + /** Set the configured WSL integration (desktop only) */ + setWslConfig?(config: { enabled: boolean }): Promise | void /** Get the preferred display backend (desktop only) */ getDisplayBackend?(): Promise | DisplayBackend | null diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 7ee84050afc..7a09edc5184 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -508,11 +508,9 @@ export const dict = { "settings.section.server": "الخادم", "settings.tab.general": "عام", "settings.tab.shortcuts": "اختصارات", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "المظهر", "settings.general.section.notifications": "إشعارات النظام", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 177414c0a66..ba09fbe03db 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -512,11 +512,9 @@ export const dict = { "settings.section.server": "Servidor", "settings.tab.general": "Geral", "settings.tab.shortcuts": "Atalhos", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Aparência", "settings.general.section.notifications": "Notificações do sistema", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index da414b512eb..38d6b79c94d 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -539,11 +539,9 @@ export const dict = { "settings.section.server": "Server", "settings.tab.general": "Opšte", "settings.tab.shortcuts": "Prečice", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Izgled", "settings.general.section.notifications": "Sistemske obavijesti", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 5c3cdff59d7..e36fb16d5b7 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -512,11 +512,9 @@ export const dict = { "settings.section.server": "Server", "settings.tab.general": "Generelt", "settings.tab.shortcuts": "Genveje", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Udseende", "settings.general.section.notifications": "Systemmeddelelser", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 874665acfa0..633d51d0528 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -556,11 +556,9 @@ export const dict = { "settings.section.server": "Server", "settings.tab.general": "Allgemein", "settings.tab.shortcuts": "Tastenkombinationen", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Erscheinungsbild", "settings.general.section.notifications": "Systembenachrichtigungen", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 99f5fcea4e6..c138c7b6145 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -583,11 +583,9 @@ export const dict = { "settings.section.server": "Server", "settings.tab.general": "General", "settings.tab.shortcuts": "Shortcuts", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Appearance", "settings.general.section.notifications": "System notifications", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index cc67f708e1b..ff4198228a5 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -515,11 +515,9 @@ export const dict = { "settings.section.server": "Servidor", "settings.tab.general": "General", "settings.tab.shortcuts": "Atajos", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Apariencia", "settings.general.section.notifications": "Notificaciones del sistema", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index ed97d13a6c7..402c095ba59 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -522,11 +522,9 @@ export const dict = { "settings.section.server": "Serveur", "settings.tab.general": "Général", "settings.tab.shortcuts": "Raccourcis", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Apparence", "settings.general.section.notifications": "Notifications système", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 6e83c9f03a7..312ac3262c7 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -507,11 +507,9 @@ export const dict = { "settings.section.server": "サーバー", "settings.tab.general": "一般", "settings.tab.shortcuts": "ショートカット", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "外観", "settings.general.section.notifications": "システム通知", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 73c5a31450b..b162ab3916e 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -513,11 +513,9 @@ export const dict = { "settings.section.server": "서버", "settings.tab.general": "일반", "settings.tab.shortcuts": "단축키", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "모양", "settings.general.section.notifications": "시스템 알림", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index c1e3d273930..001b9eda656 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -515,11 +515,9 @@ export const dict = { "settings.section.server": "Server", "settings.tab.general": "Generelt", "settings.tab.shortcuts": "Snarveier", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Utseende", "settings.general.section.notifications": "Systemvarsler", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 56ac3981251..2a20cd57e39 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -514,11 +514,9 @@ export const dict = { "settings.section.server": "Serwer", "settings.tab.general": "Ogólne", "settings.tab.shortcuts": "Skróty", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Wygląd", "settings.general.section.notifications": "Powiadomienia systemowe", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 11fac2644aa..698c8db5819 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -517,11 +517,9 @@ export const dict = { "settings.section.server": "Сервер", "settings.tab.general": "Основные", "settings.tab.shortcuts": "Горячие клавиши", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "Внешний вид", "settings.general.section.notifications": "Системные уведомления", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 3ad38cba2a5..161f37f3ba2 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -516,11 +516,9 @@ export const dict = { "settings.section.server": "เซิร์ฟเวอร์", "settings.tab.general": "ทั่วไป", "settings.tab.shortcuts": "ทางลัด", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "รูปลักษณ์", "settings.general.section.notifications": "การแจ้งเตือนระบบ", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 10c7642f351..a2931cf98c8 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -548,11 +548,9 @@ export const dict = { "settings.section.server": "服务器", "settings.tab.general": "通用", "settings.tab.shortcuts": "快捷键", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "外观", "settings.general.section.notifications": "系统通知", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 659b495ac1c..cae0c75b46c 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -545,11 +545,9 @@ export const dict = { "settings.section.server": "伺服器", "settings.tab.general": "一般", "settings.tab.shortcuts": "快速鍵", - "settings.desktop.section.backend": "Backend", - "settings.desktop.backend.title": "Server backend", - "settings.desktop.backend.description": "Choose where the OpenCode server runs.", - "settings.desktop.backend.option.native": "Native (Windows)", - "settings.desktop.backend.option.wsl": "WSL (Linux)", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", "settings.general.section.appearance": "外觀", "settings.general.section.notifications": "系統通知", diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index c1d0d6ef116..bcdd8952582 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -46,8 +46,8 @@ export default function Home() { } } - const allowNative = platform.supportsNativePickers?.() !== false - if (platform.openDirectoryPickerDialog && server.isLocal() && allowNative) { + const wslEnabled = platform.wslEnabled?.() === true + if (platform.openDirectoryPickerDialog && server.isLocal() && !wslEnabled) { const result = await platform.openDirectoryPickerDialog?.({ title: language.t("command.project.open"), multiple: true, diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 1c02d49539e..96d05989bad 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1182,8 +1182,8 @@ export default function Layout(props: ParentProps) { } } - const allowNative = platform.supportsNativePickers?.() !== false - if (platform.openDirectoryPickerDialog && server.isLocal() && allowNative) { + const wslEnabled = platform.wslEnabled?.() === true + if (platform.openDirectoryPickerDialog && server.isLocal() && !wslEnabled) { const result = await platform.openDirectoryPickerDialog?.({ title: language.t("command.project.open"), multiple: true, diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 4dbd01ae64b..9f029cc1b1d 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -7,7 +7,7 @@ use tauri_plugin_store::StoreExt; use crate::{ LogState, - constants::{BACKEND_MODE_KEY, MAX_LOG_ENTRIES, SETTINGS_STORE}, + constants::{MAX_LOG_ENTRIES, SETTINGS_STORE, WSL_ENABLED_KEY}, }; const CLI_INSTALL_DIR: &str = ".opencode/bin"; @@ -154,16 +154,16 @@ fn get_user_shell() -> String { std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()) } -fn is_wsl_backend(app: &tauri::AppHandle) -> bool { +fn is_wsl_enabled(app: &tauri::AppHandle) -> bool { let Ok(store) = app.store(SETTINGS_STORE) else { return false; }; store - .get(BACKEND_MODE_KEY) + .get(WSL_ENABLED_KEY) .as_ref() - .and_then(|value| value.as_str()) - .is_some_and(|value| value == "wsl") + .and_then(|value| value.as_bool()) + .unwrap_or(false) } fn shell_escape(input: &str) -> String { @@ -205,7 +205,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St ); #[cfg(target_os = "windows")] - if is_wsl_backend(app) { + if is_wsl_enabled(app) { let version = app.package_info().version.to_string(); let mut script = vec![ "set -e".to_string(), diff --git a/packages/desktop/src-tauri/src/constants.rs b/packages/desktop/src-tauri/src/constants.rs index 8340cf5326a..cdf05fb458b 100644 --- a/packages/desktop/src-tauri/src/constants.rs +++ b/packages/desktop/src-tauri/src/constants.rs @@ -2,7 +2,7 @@ use tauri_plugin_window_state::StateFlags; pub const SETTINGS_STORE: &str = "opencode.settings.dat"; pub const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl"; -pub const BACKEND_MODE_KEY: &str = "serverBackendMode"; +pub const WSL_ENABLED_KEY: &str = "wslEnabled"; pub const UPDATER_ENABLED: bool = option_env!("TAURI_SIGNING_PRIVATE_KEY").is_some(); pub const MAX_LOG_ENTRIES: usize = 200; diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index dc944ff9113..ddd6a9aa0aa 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -279,8 +279,8 @@ pub fn run() { await_initialization, server::get_default_server_url, server::set_default_server_url, - server::get_backend_config, - server::set_backend_config, + server::get_wsl_config, + server::set_wsl_config, get_display_backend, set_display_backend, markdown::parse_markdown_command, diff --git a/packages/desktop/src-tauri/src/server.rs b/packages/desktop/src-tauri/src/server.rs index 10a09cd2b04..8113fc7e310 100644 --- a/packages/desktop/src-tauri/src/server.rs +++ b/packages/desktop/src-tauri/src/server.rs @@ -8,35 +8,17 @@ use tokio::task::JoinHandle; use crate::{ cli, - constants::{BACKEND_MODE_KEY, DEFAULT_SERVER_URL_KEY, SETTINGS_STORE}, + constants::{DEFAULT_SERVER_URL_KEY, SETTINGS_STORE, WSL_ENABLED_KEY}, }; -#[derive(Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type, Debug)] -#[serde(rename_all = "snake_case")] -pub enum BackendMode { - Native, - Wsl, -} - -impl BackendMode { - fn as_str(&self) -> &'static str { - match self { - BackendMode::Native => "native", - BackendMode::Wsl => "wsl", - } - } -} - #[derive(Clone, serde::Serialize, serde::Deserialize, specta::Type, Debug)] -pub struct BackendConfig { - pub mode: BackendMode, +pub struct WslConfig { + pub enabled: bool, } -impl Default for BackendConfig { +impl Default for WslConfig { fn default() -> Self { - Self { - mode: BackendMode::Native, - } + Self { enabled: false } } } @@ -79,35 +61,28 @@ pub async fn set_default_server_url(app: AppHandle, url: Option) -> Resu #[tauri::command] #[specta::specta] -pub fn get_backend_config(app: AppHandle) -> Result { +pub fn get_wsl_config(app: AppHandle) -> Result { let store = app .store(SETTINGS_STORE) .map_err(|e| format!("Failed to open settings store: {}", e))?; - let mode = store - .get(BACKEND_MODE_KEY) + let enabled = store + .get(WSL_ENABLED_KEY) .as_ref() - .and_then(|v| v.as_str()) - .map(|v| match v { - "wsl" => BackendMode::Wsl, - _ => BackendMode::Native, - }) - .unwrap_or(BackendMode::Native); - - Ok(BackendConfig { mode }) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + Ok(WslConfig { enabled }) } #[tauri::command] #[specta::specta] -pub fn set_backend_config(app: AppHandle, config: BackendConfig) -> Result<(), String> { +pub fn set_wsl_config(app: AppHandle, config: WslConfig) -> Result<(), String> { let store = app .store(SETTINGS_STORE) .map_err(|e| format!("Failed to open settings store: {}", e))?; - store.set( - BACKEND_MODE_KEY, - serde_json::Value::String(config.mode.as_str().to_string()), - ); + store.set(WSL_ENABLED_KEY, serde_json::Value::Bool(config.enabled)); store .save() diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts index ca5041327b5..2fe0d2d2ab0 100644 --- a/packages/desktop/src/bindings.ts +++ b/packages/desktop/src/bindings.ts @@ -10,8 +10,10 @@ export const commands = { awaitInitialization: (events: Channel) => __TAURI_INVOKE("await_initialization", { events }), getDefaultServerUrl: () => __TAURI_INVOKE("get_default_server_url"), setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), - getBackendConfig: () => __TAURI_INVOKE("get_backend_config"), - setBackendConfig: (config: BackendConfig) => __TAURI_INVOKE("set_backend_config", { config }), + getWslConfig: () => __TAURI_INVOKE("get_wsl_config"), + setWslConfig: (config: WslConfig) => __TAURI_INVOKE("set_wsl_config", { config }), + getDisplayBackend: () => __TAURI_INVOKE("get_display_backend"), + setDisplayBackend: (backend: DisplayBackend) => __TAURI_INVOKE("set_display_backend", { backend }), parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), checkAppExists: (appName: string) => __TAURI_INVOKE("check_app_exists", { appName }), wslPath: (path: string, mode: "windows" | "linux" | null) => __TAURI_INVOKE("wsl_path", { path, mode }), @@ -23,11 +25,11 @@ export const events = { }; /* Types */ -export type BackendConfig = { - mode: BackendMode, +export type WslConfig = { + enabled: boolean, }; -export type BackendMode = "native" | "wsl"; +export type DisplayBackend = "wayland" | "auto"; export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" }; @@ -56,4 +58,3 @@ function makeEvent(name: string) { return Object.assign(fn, base); } - diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 2d1764f2660..4990731b06e 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -16,7 +16,6 @@ import { open as shellOpen } from "@tauri-apps/plugin-shell" import { type as ostype } from "@tauri-apps/plugin-os" import { check, Update } from "@tauri-apps/plugin-updater" import { getCurrentWindow } from "@tauri-apps/api/window" -import { invoke } from "@tauri-apps/api/core" import { isPermissionGranted, requestPermission } from "@tauri-apps/plugin-notification" import { relaunch } from "@tauri-apps/plugin-process" import { AsyncStorage } from "@solid-primitives/storage" @@ -30,7 +29,7 @@ import { UPDATER_ENABLED } from "./updater" import { initI18n, t } from "./i18n" import pkg from "../package.json" import "./styles.css" -import { commands, InitStep, type BackendConfig } from "./bindings" +import { commands, InitStep, type WslConfig } from "./bindings" import { Channel } from "@tauri-apps/api/core" import { createMenu } from "./menu" @@ -59,13 +58,13 @@ const listenForDeepLinks = async () => { await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined) } -const defaultBackend: BackendConfig = { mode: "native" } +const defaultWsl: WslConfig = { enabled: false } const createPlatform = ( password: Accessor, - backend: Accessor, - setBackend: (next: BackendConfig) => void, - fetchBackend: () => Promise, + wsl: Accessor, + setWsl: (next: WslConfig) => void, + fetchWsl: () => Promise, ): Platform => { const os = (() => { const type = ostype() @@ -79,7 +78,7 @@ const createPlatform = ( version: pkg.version, async openDirectoryPickerDialog(opts) { - if (backend().mode === "wsl") return null + if (wsl().enabled) return null const result = await open({ directory: true, multiple: opts?.multiple ?? false, @@ -110,7 +109,7 @@ const createPlatform = ( }, async openPath(path: string, app?: string) { - if (backend().mode === "wsl" && os === "windows") { + if (wsl().enabled && os === "windows") { const converted = await commands.wslPath(path, "windows").catch(() => null) return openerOpenPath(converted && converted.length > 0 ? converted : path, app) } @@ -332,18 +331,18 @@ const createPlatform = ( .catch(() => undefined) }, - getBackendConfig: async () => { - const next = await fetchBackend() + getWslConfig: async () => { + const next = await fetchWsl() if (next) return next - return backend() + return wsl() }, - setBackendConfig: async (config: BackendConfig) => { - setBackend(config) - await commands.setBackendConfig(config).catch(() => undefined) + setWslConfig: async (config: WslConfig) => { + setWsl(config) + await commands.setWslConfig(config).catch(() => undefined) }, - supportsNativePickers: () => backend().mode !== "wsl", + wslEnabled: () => wsl().enabled, getDefaultServerUrl: async () => { const result = await commands.getDefaultServerUrl().catch(() => null) @@ -355,12 +354,12 @@ const createPlatform = ( }, getDisplayBackend: async () => { - const result = await invoke("get_display_backend").catch(() => null) + const result = await commands.getDisplayBackend().catch(() => null) return result }, setDisplayBackend: async (backend) => { - await invoke("set_display_backend", { backend }).catch(() => undefined) + await commands.setDisplayBackend(backend).catch(() => undefined) }, parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown), @@ -404,16 +403,16 @@ void listenForDeepLinks() render(() => { const [serverPassword, setServerPassword] = createSignal(null) - const [backend, setBackend] = createSignal(defaultBackend) + const [wsl, setWsl] = createSignal(defaultWsl) - const fetchBackend = async () => { - const next = await commands.getBackendConfig().catch(() => null) + const fetchWsl = async () => { + const next = await commands.getWslConfig().catch(() => null) if (!next) return null - setBackend(next) + setWsl(next) return next } - const platform = createPlatform(() => serverPassword(), backend, setBackend, fetchBackend) + const platform = createPlatform(() => serverPassword(), wsl, setWsl, fetchWsl) function handleClick(e: MouseEvent) { const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null @@ -425,7 +424,7 @@ render(() => { onMount(() => { document.addEventListener("click", handleClick) - void fetchBackend() + void fetchWsl() onCleanup(() => { document.removeEventListener("click", handleClick) }) From 655bcd27f7f990bcdd7d83ce4101484b7fb3fabb Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 10 Feb 2026 16:48:22 +0800 Subject: [PATCH 05/10] cleanup --- packages/app/src/app.tsx | 2 +- .../app/src/components/settings-general.tsx | 14 ++- packages/app/src/context/platform.tsx | 4 +- packages/app/src/pages/home.tsx | 3 +- packages/app/src/pages/layout.tsx | 3 +- packages/desktop/src-tauri/src/cli.rs | 100 +++++++++--------- packages/desktop/src-tauri/src/windows.rs | 11 +- packages/desktop/src/bindings.ts | 17 +-- packages/desktop/src/index.tsx | 55 +++++----- 9 files changed, 110 insertions(+), 99 deletions(-) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index 5bbe86e2093..e49b725a197 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -43,7 +43,7 @@ function UiI18nBridge(props: ParentProps) { declare global { interface Window { - __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] } + __OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[]; wsl?: boolean } } } diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 5835f2afad2..72135c342e5 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -367,10 +367,10 @@ export const SettingsGeneral: Component = () => { - + {(_) => { - const [valueResource, actions] = createResource(() => platform.getWslConfig?.()) - const value = () => (valueResource.state === "pending" ? undefined : valueResource.latest) + const [enabledResource, actions] = createResource(() => platform.getWslEnabled?.()) + const enabled = () => (enabledResource.state === "pending" ? undefined : enabledResource.latest) return (
@@ -383,11 +383,9 @@ export const SettingsGeneral: Component = () => { >
- platform.setWslConfig?.({ enabled: checked })?.finally(() => actions.refetch()) - } + checked={enabled() ?? false} + disabled={enabledResource.state === "pending"} + onChange={(checked) => platform.setWslEnabled?.(checked)?.finally(() => actions.refetch())} />
diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index f753f637c8d..63066f3e879 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -61,10 +61,10 @@ export type Platform = { setDefaultServerUrl?(url: string | null): Promise | void /** Get the configured WSL integration (desktop only) */ - getWslConfig?(): Promise<{ enabled: boolean } | null> | { enabled: boolean } | null + getWslEnabled?(): Promise /** Set the configured WSL integration (desktop only) */ - setWslConfig?(config: { enabled: boolean }): Promise | void + setWslEnabled?(config: boolean): Promise | void /** Get the preferred display backend (desktop only) */ getDisplayBackend?(): Promise | DisplayBackend | null diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index bcdd8952582..d5b9573338e 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -46,8 +46,7 @@ export default function Home() { } } - const wslEnabled = platform.wslEnabled?.() === true - if (platform.openDirectoryPickerDialog && server.isLocal() && !wslEnabled) { + if (platform.openDirectoryPickerDialog && server.isLocal() && !(await platform.getWslEnabled?.())) { const result = await platform.openDirectoryPickerDialog?.({ title: language.t("command.project.open"), multiple: true, diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 96d05989bad..0351c4c0c5e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1182,8 +1182,7 @@ export default function Layout(props: ParentProps) { } } - const wslEnabled = platform.wslEnabled?.() === true - if (platform.openDirectoryPickerDialog && server.isLocal() && !wslEnabled) { + if (platform.openDirectoryPickerDialog && server.isLocal() && !(await platform.getWslEnabled?.())) { const result = await platform.openDirectoryPickerDialog?.({ title: language.t("command.project.open"), multiple: true, diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 9f029cc1b1d..121197dea51 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -204,57 +204,55 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St .map(|(key, value)| (key.to_string(), value.clone())), ); - #[cfg(target_os = "windows")] - if is_wsl_enabled(app) { - let version = app.package_info().version.to_string(); - let mut script = vec![ - "set -e".to_string(), - "BIN=\"$HOME/.opencode/bin/opencode\"".to_string(), - "if [ ! -x \"$BIN\" ]; then".to_string(), - format!( - " curl -fsSL https://opencode.ai/install | bash -s -- --version {} --no-modify-path", - shell_escape(&version) - ), - "fi".to_string(), - ]; - - let mut env_prefix = vec![ - "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY=true".to_string(), - "OPENCODE_EXPERIMENTAL_FILEWATCHER=true".to_string(), - "OPENCODE_CLIENT=desktop".to_string(), - "XDG_STATE_HOME=\"$HOME/.local/state\"".to_string(), - ]; - env_prefix.extend( - envs.iter() - .filter(|(key, _)| key != "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY") - .filter(|(key, _)| key != "OPENCODE_EXPERIMENTAL_FILEWATCHER") - .filter(|(key, _)| key != "OPENCODE_CLIENT") - .filter(|(key, _)| key != "XDG_STATE_HOME") - .map(|(key, value)| format!("{}={}", key, shell_escape(value))), - ); - - script.push(format!("{} exec \"$BIN\" {}", env_prefix.join(" "), args)); - - return app - .shell() - .command("wsl") - .args(["-e", "bash", "-lc", &script.join("\n")]); - } - - let mut cmd = app - .shell() - .sidecar("opencode-cli") - .unwrap() - .args(args.split_whitespace()); - - for (key, value) in envs { - cmd = cmd.env(key, value); - } - - return cmd; + if cfg!(windows) { + if is_wsl_enabled(app) { + let version = app.package_info().version.to_string(); + let mut script = vec![ + "set -e".to_string(), + "BIN=\"$HOME/.opencode/bin/opencode\"".to_string(), + "if [ ! -x \"$BIN\" ]; then".to_string(), + format!( + " curl -fsSL https://opencode.ai/install | bash -s -- --version {} --no-modify-path", + shell_escape(&version) + ), + "fi".to_string(), + ]; + + let mut env_prefix = vec![ + "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY=true".to_string(), + "OPENCODE_EXPERIMENTAL_FILEWATCHER=true".to_string(), + "OPENCODE_CLIENT=desktop".to_string(), + "XDG_STATE_HOME=\"$HOME/.local/state\"".to_string(), + ]; + env_prefix.extend( + envs.iter() + .filter(|(key, _)| key != "OPENCODE_EXPERIMENTAL_ICON_DISCOVERY") + .filter(|(key, _)| key != "OPENCODE_EXPERIMENTAL_FILEWATCHER") + .filter(|(key, _)| key != "OPENCODE_CLIENT") + .filter(|(key, _)| key != "XDG_STATE_HOME") + .map(|(key, value)| format!("{}={}", key, shell_escape(value))), + ); + + script.push(format!("{} exec \"$BIN\" {}", env_prefix.join(" "), args)); + + return app + .shell() + .command("wsl") + .args(["-e", "bash", "-lc", &script.join("\n")]); + } else { + let mut cmd = app + .shell() + .sidecar("opencode-cli") + .unwrap() + .args(args.split_whitespace()); + + for (key, value) in envs { + cmd = cmd.env(key, value); + } - #[cfg(not(target_os = "windows"))] - return { + return cmd; + } + } else { let sidecar = get_sidecar_path(app); let shell = get_user_shell(); @@ -271,7 +269,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St } cmd - }; + } } pub fn serve(app: &AppHandle, hostname: &str, port: u32, password: &str) -> CommandChild { diff --git a/packages/desktop/src-tauri/src/windows.rs b/packages/desktop/src-tauri/src/windows.rs index cf3e399e34b..2ddcb0506d8 100644 --- a/packages/desktop/src-tauri/src/windows.rs +++ b/packages/desktop/src-tauri/src/windows.rs @@ -1,4 +1,7 @@ -use crate::constants::{UPDATER_ENABLED, window_state_flags}; +use crate::{ + constants::{UPDATER_ENABLED, window_state_flags}, + server::get_wsl_config, +}; use std::{ops::Deref, time::Duration}; use tauri::{AppHandle, Manager, Runtime, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; use tauri_plugin_window_state::AppHandleExt; @@ -22,6 +25,11 @@ impl MainWindow { return Ok(Self(window)); } + let wsl_enabled = get_wsl_config(app.clone()) + .ok() + .map(|v| v.enabled) + .unwrap_or(false); + let window_builder = base_window_config( WebviewWindowBuilder::new(app, Self::LABEL, WebviewUrl::App("/".into())), app, @@ -36,6 +44,7 @@ impl MainWindow { r#" window.__OPENCODE__ ??= {{}}; window.__OPENCODE__.updaterEnabled = {UPDATER_ENABLED}; + window.__OPENCODE__.wsl = {wsl_enabled}; "# )); diff --git a/packages/desktop/src/bindings.ts b/packages/desktop/src/bindings.ts index 2fe0d2d2ab0..4c1e5b2d64c 100644 --- a/packages/desktop/src/bindings.ts +++ b/packages/desktop/src/bindings.ts @@ -12,8 +12,8 @@ export const commands = { setDefaultServerUrl: (url: string | null) => __TAURI_INVOKE("set_default_server_url", { url }), getWslConfig: () => __TAURI_INVOKE("get_wsl_config"), setWslConfig: (config: WslConfig) => __TAURI_INVOKE("set_wsl_config", { config }), - getDisplayBackend: () => __TAURI_INVOKE("get_display_backend"), - setDisplayBackend: (backend: DisplayBackend) => __TAURI_INVOKE("set_display_backend", { backend }), + getDisplayBackend: () => __TAURI_INVOKE<"wayland" | "auto" | null>("get_display_backend"), + setDisplayBackend: (backend: LinuxDisplayBackend) => __TAURI_INVOKE("set_display_backend", { backend }), parseMarkdownCommand: (markdown: string) => __TAURI_INVOKE("parse_markdown_command", { markdown }), checkAppExists: (appName: string) => __TAURI_INVOKE("check_app_exists", { appName }), wslPath: (path: string, mode: "windows" | "linux" | null) => __TAURI_INVOKE("wsl_path", { path, mode }), @@ -25,14 +25,10 @@ export const events = { }; /* Types */ -export type WslConfig = { - enabled: boolean, - }; - -export type DisplayBackend = "wayland" | "auto"; - export type InitStep = { phase: "server_waiting" } | { phase: "sqlite_waiting" } | { phase: "done" }; +export type LinuxDisplayBackend = "wayland" | "auto"; + export type LoadingWindowComplete = null; export type ServerReadyData = { @@ -40,6 +36,10 @@ export type ServerReadyData = { password: string | null, }; +export type WslConfig = { + enabled: boolean, + }; + export type WslPathMode = "windows" | "linux"; /* Tauri Specta runtime */ @@ -58,3 +58,4 @@ function makeEvent(name: string) { return Object.assign(fn, base); } + diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 4990731b06e..3b636cf966d 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -58,13 +58,10 @@ const listenForDeepLinks = async () => { await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined) } -const defaultWsl: WslConfig = { enabled: false } - const createPlatform = ( password: Accessor, - wsl: Accessor, + wsl: Accessor, setWsl: (next: WslConfig) => void, - fetchWsl: () => Promise, ): Platform => { const os = (() => { const type = ostype() @@ -78,7 +75,6 @@ const createPlatform = ( version: pkg.version, async openDirectoryPickerDialog(opts) { - if (wsl().enabled) return null const result = await open({ directory: true, multiple: opts?.multiple ?? false, @@ -109,7 +105,7 @@ const createPlatform = ( }, async openPath(path: string, app?: string) { - if (wsl().enabled && os === "windows") { + if (wsl() && os === "windows") { const converted = await commands.wslPath(path, "windows").catch(() => null) return openerOpenPath(converted && converted.length > 0 ? converted : path, app) } @@ -331,18 +327,37 @@ const createPlatform = ( .catch(() => undefined) }, - getWslConfig: async () => { - const next = await fetchWsl() - if (next) return next + 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, + }) + } + }, + + getWslEnabled: async () => { + const next = await commands.getWslConfig().catch(() => null) + if (next) return next.enabled return wsl() }, - setWslConfig: async (config: WslConfig) => { - setWsl(config) - await commands.setWslConfig(config).catch(() => undefined) + setWslEnabled: async (enabled) => { + await commands.setWslConfig({ enabled }) }, - wslEnabled: () => wsl().enabled, + wslEnabled: () => wsl(), getDefaultServerUrl: async () => { const result = await commands.getDefaultServerUrl().catch(() => null) @@ -359,7 +374,7 @@ const createPlatform = ( }, setDisplayBackend: async (backend) => { - await commands.setDisplayBackend(backend).catch(() => undefined) + await commands.setDisplayBackend(backend) }, parseMarkdown: (markdown: string) => commands.parseMarkdownCommand(markdown), @@ -403,16 +418,9 @@ void listenForDeepLinks() render(() => { const [serverPassword, setServerPassword] = createSignal(null) - const [wsl, setWsl] = createSignal(defaultWsl) - - const fetchWsl = async () => { - const next = await commands.getWslConfig().catch(() => null) - if (!next) return null - setWsl(next) - return next - } + const [wsl, setWsl] = createSignal(window.__OPENCODE__?.wsl ?? false) - const platform = createPlatform(() => serverPassword(), wsl, setWsl, fetchWsl) + const platform = createPlatform(() => serverPassword(), wsl, setWsl) function handleClick(e: MouseEvent) { const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null @@ -424,7 +432,6 @@ render(() => { onMount(() => { document.addEventListener("click", handleClick) - void fetchWsl() onCleanup(() => { document.removeEventListener("click", handleClick) }) From ca51d6347442c602aae90667a809739fd6eec65b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 11 Feb 2026 14:03:13 +0800 Subject: [PATCH 06/10] remove wslEnabled --- packages/app/src/context/platform.tsx | 3 --- packages/desktop/src/index.tsx | 2 -- 2 files changed, 5 deletions(-) diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 63066f3e879..e260c1977ed 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -33,9 +33,6 @@ export type Platform = { /** Open directory picker dialog (native on Tauri, server-backed on web) */ openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise - /** Whether WSL integration is enabled (desktop only) */ - wslEnabled?(): boolean - /** Open native file picker dialog (Tauri only) */ openFilePickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 3b636cf966d..0fad606bd66 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -357,8 +357,6 @@ const createPlatform = ( await commands.setWslConfig({ enabled }) }, - wslEnabled: () => wsl(), - getDefaultServerUrl: async () => { const result = await commands.getDefaultServerUrl().catch(() => null) return result From b4d11e658fbfcb51e4959f79735d3e161a7fb5a3 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 11 Feb 2026 14:15:12 +0800 Subject: [PATCH 07/10] remove wsl signal --- packages/desktop/src/index.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 0fad606bd66..d8dcc837279 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -58,11 +58,7 @@ const listenForDeepLinks = async () => { await onOpenUrl((urls) => emitDeepLinks(urls)).catch(() => undefined) } -const createPlatform = ( - password: Accessor, - wsl: Accessor, - setWsl: (next: WslConfig) => void, -): Platform => { +const createPlatform = (password: Accessor): Platform => { const os = (() => { const type = ostype() if (type === "macos" || type === "windows" || type === "linux") return type @@ -105,7 +101,7 @@ const createPlatform = ( }, async openPath(path: string, app?: string) { - if (wsl() && os === "windows") { + if (os === "windows" && window.__OPENCODE__?.wsl) { const converted = await commands.wslPath(path, "windows").catch(() => null) return openerOpenPath(converted && converted.length > 0 ? converted : path, app) } @@ -350,7 +346,7 @@ const createPlatform = ( getWslEnabled: async () => { const next = await commands.getWslConfig().catch(() => null) if (next) return next.enabled - return wsl() + return window.__OPENCODE__!.wsl ?? false }, setWslEnabled: async (enabled) => { @@ -416,9 +412,8 @@ void listenForDeepLinks() render(() => { const [serverPassword, setServerPassword] = createSignal(null) - const [wsl, setWsl] = createSignal(window.__OPENCODE__?.wsl ?? false) - const platform = createPlatform(() => serverPassword(), wsl, setWsl) + const platform = createPlatform(() => serverPassword()) function handleClick(e: MouseEvent) { const link = (e.target as HTMLElement).closest("a.external-link") as HTMLAnchorElement | null From 848557af5d0a5e0b89f7ffcfd09609a35ca51921 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 11 Feb 2026 14:44:20 +0800 Subject: [PATCH 08/10] add more logs --- packages/desktop/src-tauri/src/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs index 121197dea51..6882d369e93 100644 --- a/packages/desktop/src-tauri/src/cli.rs +++ b/packages/desktop/src-tauri/src/cli.rs @@ -206,6 +206,7 @@ pub fn create_command(app: &tauri::AppHandle, args: &str, extra_env: &[(&str, St if cfg!(windows) { if is_wsl_enabled(app) { + println!("WSL is enabled, spawning CLI server in WSL."); let version = app.package_info().version.to_string(); let mut script = vec![ "set -e".to_string(), From 0aa28f83daaf3247c8096c1827df0a87725bb914 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 11 Feb 2026 15:28:54 +0800 Subject: [PATCH 09/10] support native picker in wsl --- packages/app/src/pages/home.tsx | 2 +- packages/app/src/pages/layout.tsx | 2 +- packages/desktop/src-tauri/src/lib.rs | 18 ++++++++++++++---- packages/desktop/src/index.tsx | 21 ++++++++++++++++++--- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 2b50b8b41a0..6b61ed30041 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -47,7 +47,7 @@ export default function Home() { } } - if (platform.openDirectoryPickerDialog && server.isLocal() && !(await platform.getWslEnabled?.())) { + if (platform.openDirectoryPickerDialog && server.isLocal()) { const result = await platform.openDirectoryPickerDialog?.({ title: language.t("command.project.open"), multiple: true, diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 982f312aa49..a18b7ef237a 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1168,7 +1168,7 @@ export default function Layout(props: ParentProps) { } } - if (platform.openDirectoryPickerDialog && server.isLocal() && !(await platform.getWslEnabled?.())) { + if (platform.openDirectoryPickerDialog && server.isLocal()) { const result = await platform.openDirectoryPickerDialog?.({ title: language.t("command.project.open"), multiple: true, diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 7e8e749efd4..565cd868e63 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -407,10 +407,20 @@ fn wsl_path(path: String, mode: Option) -> Result { WslPathMode::Linux => "-u", }; - let output = Command::new("wsl") - .args(["-e", "wslpath", flag, &path]) - .output() - .map_err(|e| format!("Failed to run wslpath: {e}"))?; + let output = if path.starts_with('~') { + let suffix = path.strip_prefix('~').unwrap_or(""); + let escaped = suffix.replace('"', "\\\""); + let cmd = format!("wslpath {flag} \"$HOME{escaped}\""); + Command::new("wsl") + .args(["-e", "sh", "-lc", &cmd]) + .output() + .map_err(|e| format!("Failed to run wslpath: {e}"))? + } else { + Command::new("wsl") + .args(["-e", "wslpath", flag, &path]) + .output() + .map_err(|e| format!("Failed to run wslpath: {e}"))? + }; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 51ffdb75b84..ca603da5f97 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -65,18 +65,33 @@ const createPlatform = (password: Accessor): Platform => { return undefined })() + const wslHome = async () => { + if (os !== "windows" || !window.__OPENCODE__?.wsl) return undefined + return commands.wslPath("~", "windows").catch(() => undefined) + } + + const handleWslPicker = async (result: T | null): Promise => { + if (!result || !window.__OPENCODE__?.wsl) return result + if (Array.isArray(result)) { + return Promise.all(result.map((path) => commands.wslPath(path, "linux").catch(() => path))) as any + } + return commands.wslPath(result, "linux").catch(() => result) as any + } + return { platform: "desktop", os, version: pkg.version, async openDirectoryPickerDialog(opts) { + const defaultPath = await wslHome() const result = await open({ directory: true, multiple: opts?.multiple ?? false, title: opts?.title ?? t("desktop.dialog.chooseFolder"), + defaultPath, }) - return result + return await handleWslPicker(result) }, async openFilePickerDialog(opts) { @@ -85,7 +100,7 @@ const createPlatform = (password: Accessor): Platform => { multiple: opts?.multiple ?? false, title: opts?.title ?? t("desktop.dialog.chooseFile"), }) - return result + return handleWslPicker(result) }, async saveFilePickerDialog(opts) { @@ -93,7 +108,7 @@ const createPlatform = (password: Accessor): Platform => { title: opts?.title ?? t("desktop.dialog.saveFile"), defaultPath: opts?.defaultPath, }) - return result + return handleWslPicker(result) }, openLink(url: string) { From 9ba3f6cc11744e8fbc93a797aae9ba7dd9f8ebf4 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Wed, 11 Feb 2026 15:44:39 +0800 Subject: [PATCH 10/10] make wsl_path windows-only --- packages/desktop/src-tauri/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 565cd868e63..5c3915e81a8 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -402,6 +402,10 @@ fn check_linux_app(app_name: &str) -> bool { #[tauri::command] #[specta::specta] fn wsl_path(path: String, mode: Option) -> Result { + if !cfg(windows) { + return Ok(path); + } + let flag = match mode.unwrap_or(WslPathMode::Linux) { WslPathMode::Windows => "-w", WslPathMode::Linux => "-u",