From 91e0dac658958624f8fb2b59701c2a57cf018de3 Mon Sep 17 00:00:00 2001 From: MostlyK <135974627+MostlyKIGuess@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:58:10 +0530 Subject: [PATCH] repl: Treat WSL as a separate kernel type from SSH remote (#50721) Split WslRemote out of the remote_kernels bucket in the kernel picker, giving it its own "WSL Kernels" section. Use the distro name and kernelspec display name for WSL entries instead of the generic "WSL" string. In python_env_kernel_specifications, detect WSL projects via RemoteConnectionOptions and return WslRemote instead of SshRemote. Stop marking WSL worktrees as remote so global kernel specs load. Fix ark kernel stdout pollution by building the wsl.exe bash command with a quoted cd and inline env assignment, so exec replaces the shell and doesn't echo input back. Closes #50459 Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - N/A --- crates/repl/src/components/kernel_options.rs | 21 ++++++++--- crates/repl/src/kernels/mod.rs | 37 ++++++++++++++++---- crates/repl/src/kernels/wsl_kernel.rs | 30 +++++++++++++++- crates/repl/src/repl_store.rs | 11 +++++- 4 files changed, 85 insertions(+), 14 deletions(-) diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs index 3b9535767b..45e55f0d5f 100644 --- a/crates/repl/src/components/kernel_options.rs +++ b/crates/repl/src/components/kernel_options.rs @@ -27,6 +27,7 @@ fn build_grouped_entries(store: &ReplStore, worktree_id: WorktreeId) -> Vec Vec { + KernelSpecification::JupyterServer(_) | KernelSpecification::SshRemote(_) => { remote_kernels.push(KernelPickerEntry::Kernel { spec: spec.clone(), is_recommended, }); } + KernelSpecification::WslRemote(_) => { + wsl_kernels.push(KernelPickerEntry::Kernel { + spec: spec.clone(), + is_recommended, + }); + } } } @@ -105,6 +110,12 @@ fn build_grouped_entries(store: &ReplStore, worktree_id: WorktreeId) -> Vec None, + KernelSpecification::WslRemote(_) => Some(spec.path().to_string()), KernelSpecification::PythonEnv(_) | KernelSpecification::JupyterServer(_) - | KernelSpecification::SshRemote(_) - | KernelSpecification::WslRemote(_) => { + | KernelSpecification::SshRemote(_) => { let env_kind = spec.environment_kind_label(); let path = spec.path(); match env_kind { diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index 9ec2ddb497..0f1ee9dabe 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -9,6 +9,7 @@ pub use native_kernel::*; mod remote_kernels; use project::{Project, ProjectPath, Toolchains, WorktreeId}; +use remote::RemoteConnectionOptions; pub use remote_kernels::*; mod ssh_kernel; @@ -238,7 +239,7 @@ impl KernelSpecification { Self::PythonEnv(spec) => spec.name.clone().into(), Self::JupyterServer(spec) => spec.name.clone().into(), Self::SshRemote(spec) => spec.name.clone().into(), - Self::WslRemote(spec) => spec.name.clone().into(), + Self::WslRemote(spec) => spec.kernelspec.display_name.clone().into(), } } @@ -262,7 +263,7 @@ impl KernelSpecification { Self::PythonEnv(spec) => spec.path.to_string_lossy().into_owned(), Self::JupyterServer(spec) => spec.url.to_string(), Self::SshRemote(spec) => spec.path.to_string(), - Self::WslRemote(_) => "WSL".to_string(), + Self::WslRemote(spec) => spec.distro.clone(), }) } @@ -348,7 +349,16 @@ pub fn python_env_kernel_specifications( ) -> impl Future>> + use<> { let python_language = LanguageName::new_static("Python"); let is_remote = project.read(cx).is_remote(); - log::info!("python_env_kernel_specifications: is_remote: {}", is_remote); + let wsl_distro = project + .read(cx) + .remote_connection_options(cx) + .and_then(|opts| { + if let RemoteConnectionOptions::Wsl(wsl) = opts { + Some(wsl.distro_name) + } else { + None + } + }); let toolchains = project.read(cx).available_toolchains( ProjectPath { @@ -383,6 +393,7 @@ pub fn python_env_kernel_specifications( .flatten() .chain(toolchains.toolchains) .map(|toolchain| { + let wsl_distro = wsl_distro.clone(); background_executor.spawn(async move { // For remote projects, we assume python is available assuming toolchain is reported. // We can skip the `ipykernel` check or run it remotely. @@ -390,10 +401,6 @@ pub fn python_env_kernel_specifications( // `new_smol_command` runs locally. We need to run remotely if `is_remote`. if is_remote { - log::info!( - "python_env_kernel_specifications: returning SshRemote for toolchain {}", - toolchain.name - ); let default_kernelspec = JupyterKernelspec { argv: vec![ toolchain.path.to_string(), @@ -409,6 +416,22 @@ pub fn python_env_kernel_specifications( env: None, }; + if let Some(distro) = wsl_distro { + log::debug!( + "python_env_kernel_specifications: returning WslRemote for toolchain {}", + toolchain.name + ); + return Some(KernelSpecification::WslRemote(WslKernelSpecification { + name: toolchain.name.to_string(), + kernelspec: default_kernelspec, + distro, + })); + } + + log::debug!( + "python_env_kernel_specifications: returning SshRemote for toolchain {}", + toolchain.name + ); return Some(KernelSpecification::SshRemote( SshRemoteKernelSpecification { name: format!("Remote {}", toolchain.name), diff --git a/crates/repl/src/kernels/wsl_kernel.rs b/crates/repl/src/kernels/wsl_kernel.rs index 34340c74fe..d9ac05c5fc 100644 --- a/crates/repl/src/kernels/wsl_kernel.rs +++ b/crates/repl/src/kernels/wsl_kernel.rs @@ -274,7 +274,23 @@ impl WslRunningKernel { cd_command, set_env_command, arg_string, arg_string, arg_string, arg_string ) } else { - quote_posix_shell_arguments(&kernel_args)? + let args_string = quote_posix_shell_arguments(&resolved_argv)?; + + let cd_command = if let Some(wd) = wsl_working_directory.as_ref() { + let quoted_wd = shlex::try_quote(wd) + .map(|quoted| quoted.into_owned())?; + format!("cd {quoted_wd} && ") + } else { + String::new() + }; + + let env_prefix_inline = if !env_assignments.is_empty() { + format!("env {} ", env_assignments.join(" ")) + } else { + String::new() + }; + + format!("{cd_command}exec {env_prefix_inline}{args_string}") }; cmd.arg("bash") @@ -578,8 +594,20 @@ pub async fn wsl_kernel_specifications( }) }) .collect::>(); + } else if let Err(e) = + serde_json::from_str::(&json_str) + { + log::error!( + "wsl_kernel_specifications parse error: {} \nJSON: {}", + e, + json_str + ); } + } else { + log::error!("wsl_kernel_specifications command failed"); } + } else if let Err(e) = output { + log::error!("wsl_kernel_specifications command execution failed: {}", e); } Vec::new() diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs index 8da94eaa7f..ff0a279361 100644 --- a/crates/repl/src/repl_store.rs +++ b/crates/repl/src/repl_store.rs @@ -8,6 +8,7 @@ use gpui::{App, Context, Entity, EntityId, Global, SharedString, Subscription, T use jupyter_websocket_client::RemoteServer; use language::{Language, LanguageName}; use project::{Fs, Project, ProjectPath, WorktreeId}; +use remote::RemoteConnectionOptions; use settings::{Settings, SettingsStore}; use util::rel_path::RelPath; @@ -144,6 +145,14 @@ impl ReplStore { cx: &mut Context, ) -> Task> { let is_remote = project.read(cx).is_remote(); + // WSL does require access to global kernel specs, so we only exclude remote worktrees that aren't WSL. + // TODO: a better way to handle WSL vs SSH/remote projects, + let is_wsl_remote = project + .read(cx) + .remote_connection_options(cx) + .map_or(false, |opts| { + matches!(opts, RemoteConnectionOptions::Wsl(_)) + }); let kernel_specifications = python_env_kernel_specifications(project, worktree_id, cx); let active_toolchain = project.read(cx).active_toolchain( ProjectPath { @@ -168,7 +177,7 @@ impl ReplStore { this.active_python_toolchain_for_worktree .insert(worktree_id, path); } - if is_remote { + if is_remote && !is_wsl_remote { this.remote_worktrees.insert(worktree_id); } else { this.remote_worktrees.remove(&worktree_id);