Compare commits

...

8 Commits

Author SHA1 Message Date
localcc
2793dd77ad Fix duplicate WSL entries (#40255)
Release Notes:

- N/A
2025-10-15 16:33:53 +02:00
localcc
10720d64a3 Improve musl libc detection (#40254)
Release Notes:

- N/A
2025-10-15 16:33:28 +02:00
Ben Brandt
2adb979ed7 acp: Fix /logout for agents that support it (#40248)
We were clearing the message editor too early. We only want to clear the
message editor if we are going to short circuit and return early before
submitting.
Otherwise, the agents that can handle this themselves won't have the
ability to do so.

Release Notes:

- acp: Fix /logout not working for some agents
2025-10-15 08:25:55 -06:00
Ben Brandt
bec6cd94a4 acp: Allow updating default mode for Codex (#40238)
Release Notes:

- acp: Save default mode for codex
2025-10-15 08:25:55 -06:00
Conrad Irwin
f6c0fa43ef Avoid gap between titlebar and body on linux
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: John Tur <john-tur@outlook.com>
2025-10-14 22:41:34 -06:00
Cole Miller
62da0dc402 Fix triggers for debugger thread and session lists not rendering (#40227)
Release Notes:

- N/A
2025-10-14 22:37:49 -06:00
Cole Miller
154405ff3b Fix a couple of bugs in remote browser debugging implementation (#40225)
Follow-up to #39248 

- Correctly forward ports over SSH, including the port from the debug
scenario's `url`
- Give the companion time to start up, instead of bailing if the first
connection attempt fails

Release Notes:

- Fixed not being able to launch a browser debugging session in an SSH
project.
2025-10-14 23:23:27 -04:00
Mikayla Maki
b558313181 v0.209.x preview 2025-10-14 17:33:03 -07:00
12 changed files with 255 additions and 134 deletions

View File

@@ -1,11 +1,16 @@
use std::rc::Rc;
use std::sync::Arc;
use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use project::agent_server_store::CODEX_NAME;
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
use settings::{SettingsStore, update_settings_file};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
#[derive(Clone)]
pub struct Codex;
@@ -30,6 +35,27 @@ impl AgentServer for Codex {
ui::IconName::AiOpenAi
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_mode = mode_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -1045,9 +1045,6 @@ impl AcpThreadView {
return;
};
self.message_editor
.update(cx, |editor, cx| editor.clear(window, cx));
let connection = thread.read(cx).connection().clone();
let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
// Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
@@ -1058,6 +1055,9 @@ impl AcpThreadView {
.iter()
.any(|command| command.name == "logout");
if can_login && !logout_supported {
self.message_editor
.update(cx, |editor, cx| editor.clear(window, cx));
let this = cx.weak_entity();
let agent = self.agent.clone();
window.defer(cx, |window, cx| {

View File

@@ -1,7 +1,7 @@
use std::rc::Rc;
use collections::HashMap;
use gpui::{Entity, WeakEntity};
use gpui::{Corner, Entity, WeakEntity};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use util::{maybe, truncate_and_trailoff};
@@ -211,6 +211,7 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone());
@@ -322,6 +323,7 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.disabled(session_terminated)
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),

View File

@@ -57,29 +57,65 @@ impl RustLspAdapter {
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer");
#[cfg(target_os = "linux")]
enum LibcType {
Gnu,
Musl,
}
impl RustLspAdapter {
#[cfg(target_os = "linux")]
fn build_arch_server_name_linux() -> String {
enum LibcType {
Gnu,
Musl,
async fn determine_libc_type() -> LibcType {
use futures::pin_mut;
use smol::process::Command;
async fn from_ldd_version() -> Option<LibcType> {
let ldd_output = Command::new("ldd").arg("--version").output().await.ok()?;
let ldd_version = String::from_utf8_lossy(&ldd_output.stdout);
if ldd_version.contains("GNU libc") || ldd_version.contains("GLIBC") {
Some(LibcType::Gnu)
} else if ldd_version.contains("musl") {
Some(LibcType::Musl)
} else {
None
}
}
let has_musl = std::fs::exists(&format!("/lib/ld-musl-{}.so.1", std::env::consts::ARCH))
.unwrap_or(false);
let has_gnu = std::fs::exists(&format!("/lib/ld-linux-{}.so.1", std::env::consts::ARCH))
.unwrap_or(false);
if let Some(libc_type) = from_ldd_version().await {
return libc_type;
}
let libc_type = match (has_musl, has_gnu) {
let Ok(dir_entries) = smol::fs::read_dir("/lib").await else {
// defaulting to gnu because nix doesn't have /lib files due to not following FHS
return LibcType::Gnu;
};
let dir_entries = dir_entries.filter_map(async move |e| e.ok());
pin_mut!(dir_entries);
let mut has_musl = false;
let mut has_gnu = false;
while let Some(entry) = dir_entries.next().await {
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
if file_name.starts_with("ld-musl-") {
has_musl = true;
} else if file_name.starts_with("ld-linux-") {
has_gnu = true;
}
}
match (has_musl, has_gnu) {
(true, _) => LibcType::Musl,
(_, true) => LibcType::Gnu,
_ => {
// defaulting to gnu because nix doesn't have either of those files due to not following FHS
LibcType::Gnu
}
};
_ => LibcType::Gnu,
}
}
let libc = match libc_type {
#[cfg(target_os = "linux")]
async fn build_arch_server_name_linux() -> String {
let libc = match Self::determine_libc_type().await {
LibcType::Musl => "musl",
LibcType::Gnu => "gnu",
};
@@ -87,7 +123,7 @@ impl RustLspAdapter {
format!("{}-{}", Self::ARCH_SERVER_NAME, libc)
}
fn build_asset_name() -> String {
async fn build_asset_name() -> String {
let extension = match Self::GITHUB_ASSET_KIND {
AssetKind::TarGz => "tar.gz",
AssetKind::Gz => "gz",
@@ -95,7 +131,7 @@ impl RustLspAdapter {
};
#[cfg(target_os = "linux")]
let arch_server_name = Self::build_arch_server_name_linux();
let arch_server_name = Self::build_arch_server_name_linux().await;
#[cfg(not(target_os = "linux"))]
let arch_server_name = Self::ARCH_SERVER_NAME.to_string();
@@ -447,7 +483,7 @@ impl LspInstaller for RustLspAdapter {
delegate.http_client(),
)
.await?;
let asset_name = Self::build_asset_name();
let asset_name = Self::build_asset_name().await;
let asset = release
.assets
.into_iter()

View File

@@ -14,12 +14,13 @@ use super::dap_command::{
TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
};
use super::dap_store::DapStore;
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result, anyhow, bail};
use base64::Engine;
use collections::{HashMap, HashSet, IndexMap};
use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
use dap::messages::Response;
use dap::requests::{Request, RunInTerminal, StartDebugging};
use dap::transport::TcpTransport;
use dap::{
Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId,
SteppingGranularity, StoppedEvent, VariableReference,
@@ -47,12 +48,14 @@ use remote::RemoteClient;
use rpc::ErrorExt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::net::TcpListener;
use smol::net::{TcpListener, TcpStream};
use std::any::TypeId;
use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use std::u64;
use std::{
any::Any,
@@ -63,6 +66,7 @@ use std::{
};
use task::TaskContext;
use text::{PointUtf16, ToPointUtf16};
use url::Url;
use util::command::new_smol_command;
use util::{ResultExt, debug_panic, maybe};
use worktree::Worktree;
@@ -2768,31 +2772,42 @@ impl Session {
let mut console_output = self.console_output(cx);
let task = cx.spawn(async move |this, cx| {
let (dap_port, _child) =
if remote_client.read_with(cx, |client, _| client.shares_network_interface())? {
(request.server_port, None)
} else {
let port = {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.context("getting port for DAP")?;
listener.local_addr()?.port()
};
let child = remote_client.update(cx, |client, _| {
let command = client.build_forward_port_command(
port,
"localhost".into(),
request.server_port,
)?;
let child = new_smol_command(command.program)
.args(command.args)
.envs(command.env)
.spawn()
.context("spawning port forwarding process")?;
anyhow::Ok(child)
})??;
(port, Some(child))
};
let forward_ports_process = if remote_client
.read_with(cx, |client, _| client.shares_network_interface())?
{
request.other.insert(
"proxyUri".into(),
format!("127.0.0.1:{}", request.server_port).into(),
);
None
} else {
let port = TcpTransport::unused_port(Ipv4Addr::LOCALHOST)
.await
.context("getting port for DAP")?;
request
.other
.insert("proxyUri".into(), format!("127.0.0.1:{port}").into());
let mut port_forwards = vec![(port, "localhost".to_owned(), request.server_port)];
if let Some(value) = request.params.get("url")
&& let Some(url) = value.as_str()
&& let Some(url) = Url::parse(url).ok()
&& let Some(frontend_port) = url.port()
{
port_forwards.push((frontend_port, "localhost".to_owned(), frontend_port));
}
let child = remote_client.update(cx, |client, _| {
let command = client.build_forward_ports_command(port_forwards)?;
let child = new_smol_command(command.program)
.args(command.args)
.envs(command.env)
.spawn()
.context("spawning port forwarding process")?;
anyhow::Ok(child)
})??;
Some(child)
};
let mut companion_process = None;
let companion_port =
@@ -2814,14 +2829,17 @@ impl Session {
}
}
};
this.update(cx, |this, cx| {
this.companion_port = Some(companion_port);
let Some(mut child) = companion_process else {
return;
};
if let Some(stderr) = child.stderr.take() {
let mut background_tasks = Vec::new();
if let Some(mut forward_ports_process) = forward_ports_process {
background_tasks.push(cx.spawn(async move |_| {
forward_ports_process.status().await.log_err();
}));
};
if let Some(mut companion_process) = companion_process {
if let Some(stderr) = companion_process.stderr.take() {
let mut console_output = console_output.clone();
this.background_tasks.push(cx.spawn(async move |_, _| {
background_tasks.push(cx.spawn(async move |_| {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
@@ -2835,9 +2853,9 @@ impl Session {
}
}));
}
this.background_tasks.push(cx.spawn({
background_tasks.push(cx.spawn({
let mut console_output = console_output.clone();
async move |_, _| match child.status().await {
async move |_| match companion_process.status().await {
Ok(status) => {
if status.success() {
console_output
@@ -2860,17 +2878,33 @@ impl Session {
.ok();
}
}
}))
})?;
}));
}
request
.other
.insert("proxyUri".into(), format!("127.0.0.1:{dap_port}").into());
// TODO pass wslInfo as needed
let companion_address = format!("127.0.0.1:{companion_port}");
let mut companion_started = false;
for _ in 0..10 {
if TcpStream::connect(&companion_address).await.is_ok() {
companion_started = true;
break;
}
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
}
if !companion_started {
console_output
.send("Browser companion failed to start".into())
.await
.ok();
bail!("Browser companion failed to start");
}
let response = http_client
.post_json(
&format!("http://127.0.0.1:{companion_port}/launch-and-attach"),
&format!("http://{companion_address}/launch-and-attach"),
serde_json::to_string(&request)
.context("serializing request")?
.into(),
@@ -2895,6 +2929,11 @@ impl Session {
}
}
this.update(cx, |this, _| {
this.background_tasks.extend(background_tasks);
this.companion_port = Some(companion_port);
})?;
anyhow::Ok(())
});
self.background_tasks.push(cx.spawn(async move |_, _| {
@@ -2926,15 +2965,16 @@ impl Session {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct LaunchBrowserInCompanionParams {
server_port: u16,
params: HashMap<String, serde_json::Value>,
#[serde(flatten)]
other: HashMap<String, serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct KillCompanionBrowserParams {
launch_id: u64,

View File

@@ -1385,14 +1385,21 @@ impl RemoteServerProjects {
cx: &mut Context<Self>,
) {
self.update_settings_file(cx, move |setting, _| {
setting
.wsl_connections
.get_or_insert(Default::default())
.push(settings::WslConnection {
distro_name: SharedString::from(connection_options.distro_name),
user: connection_options.user,
let connections = setting.wsl_connections.get_or_insert(Default::default());
let distro_name = SharedString::from(connection_options.distro_name);
let user = connection_options.user;
if !connections
.iter()
.any(|conn| conn.distro_name == distro_name && conn.user == user)
{
connections.push(settings::WslConnection {
distro_name,
user,
projects: BTreeSet::new(),
})
}
});
}

View File

@@ -836,16 +836,14 @@ impl RemoteClient {
connection.build_command(program, args, env, working_dir, port_forward)
}
pub fn build_forward_port_command(
pub fn build_forward_ports_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
let Some(connection) = self.remote_connection() else {
return Err(anyhow!("no ssh connection"));
};
connection.build_forward_port_command(local_port, host, remote_port)
connection.build_forward_ports_command(forwards)
}
pub fn upload_directory(
@@ -1116,11 +1114,9 @@ pub(crate) trait RemoteConnection: Send + Sync {
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate>;
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
local_port: u16,
remote: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate>;
fn connection_options(&self) -> RemoteConnectionOptions;
fn path_style(&self) -> PathStyle;
@@ -1551,19 +1547,17 @@ mod fake {
})
}
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> anyhow::Result<CommandTemplate> {
Ok(CommandTemplate {
program: "ssh".into(),
args: vec![
"-N".into(),
"-L".into(),
format!("{local_port}:{host}:{remote_port}"),
],
args: std::iter::once("-N".to_owned())
.chain(forwards.into_iter().map(|(local_port, host, remote_port)| {
format!("{local_port}:{host}:{remote_port}")
}))
.collect(),
env: Default::default(),
})
}

View File

@@ -146,19 +146,20 @@ impl RemoteConnection for SshRemoteConnection {
)
}
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
forwards: Vec<(u16, String, u16)>,
) -> Result<CommandTemplate> {
let Self { socket, .. } = self;
let mut args = socket.ssh_args();
args.push("-N".into());
for (local_port, host, remote_port) in forwards {
args.push("-L".into());
args.push(format!("{local_port}:{host}:{remote_port}"));
}
Ok(CommandTemplate {
program: "ssh".into(),
args: vec![
"-N".into(),
"-L".into(),
format!("{local_port}:{host}:{remote_port}"),
],
args,
env: Default::default(),
})
}

View File

@@ -485,11 +485,9 @@ impl RemoteConnection for WslRemoteConnection {
})
}
fn build_forward_port_command(
fn build_forward_ports_command(
&self,
_: u16,
_: String,
_: u16,
_: Vec<(u16, String, u16)>,
) -> anyhow::Result<CommandTemplate> {
Err(anyhow!("WSL shares a network interface with the host"))
}

View File

@@ -97,6 +97,7 @@ impl Render for PlatformTitleBar {
})
// this border is to avoid a transparent gap in the rounded corners
.mt(px(-1.))
.mb(px(-1.))
.border(px(1.))
.border_color(titlebar_color),
})

View File

@@ -1,6 +1,6 @@
use gpui::{Corner, Entity, Pixels, Point};
use crate::{ContextMenu, PopoverMenu, prelude::*};
use crate::{ButtonLike, ContextMenu, PopoverMenu, prelude::*};
use super::PopoverMenuHandle;
@@ -137,36 +137,52 @@ impl RenderOnce for DropdownMenu {
let full_width = self.full_width;
let trigger_size = self.trigger_size;
let button = match self.label {
LabelKind::Text(text) => Button::new(self.id.clone(), text)
.style(button_style)
.when(self.chevron, |this| {
this.icon(IconName::ChevronUpDown)
.icon_position(IconPosition::End)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled),
LabelKind::Element(_element) => Button::new(self.id.clone(), "")
.style(button_style)
.when(self.chevron, |this| {
this.icon(IconName::ChevronUpDown)
.icon_position(IconPosition::End)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled),
}
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index));
let (text_button, element_button) = match self.label {
LabelKind::Text(text) => (
Some(
Button::new(self.id.clone(), text)
.style(button_style)
.when(self.chevron, |this| {
this.icon(IconName::ChevronUpDown)
.icon_position(IconPosition::End)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled)
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
),
None,
),
LabelKind::Element(element) => (
None,
Some(
ButtonLike::new(self.id.clone())
.child(element)
.style(button_style)
.when(self.chevron, |this| {
this.child(
Icon::new(IconName::ChevronUpDown)
.size(IconSize::XSmall)
.color(Color::Muted),
)
})
.when(full_width, |this| this.full_width())
.size(trigger_size)
.disabled(self.disabled)
.when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)),
),
),
};
PopoverMenu::new((self.id.clone(), "popover"))
.full_width(self.full_width)
.menu(move |_window, _cx| Some(self.menu.clone()))
.trigger(button)
.when_some(text_button, |this, text_button| this.trigger(text_button))
.when_some(element_button, |this, element_button| {
this.trigger(element_button)
})
.attach(match self.attach {
Some(attach) => attach,
None => Corner::BottomRight,

View File

@@ -1 +1 @@
dev
preview