Read remote shell config when creating a terminal shell (#61451)

# Objective

- Makes shell for remote configurable.
- fixes #35226 

## Solution

- Adds rpc so client can retrieve terminal settings from remote server
- Vibe coded with codex Sol, I did my best to review it and make sure
it's the minimal changes.
- Added terminal dependency to remote_server, rather than a larger
change (e.g. adding something to project to expose terminal shell
setting to remote_server)

## Testing

- Tested and works Windows client / linux remote server

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable


---

Release Notes:

- Added support for terminal.shell in remote sessions

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
Quinn Shanahan 2026-09-29 13:38:22 +00:00 • committed by GitHub
parent 7604aa3f19
commit c87632ef44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 113 additions and 8 deletions

3
Cargo.lock generated
View file

@ -3218,7 +3218,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror 2.0.17",
"tokio-rustls 0.26.4",
"tokio-rustls",
"yawc",
]
@ -15307,6 +15307,7 @@ dependencies = [
"task",
"telemetry",
"tempfile",
"terminal",
"theme",
"theme_settings",
"thiserror 2.0.17",

View file

@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
@ -6,6 +6,7 @@ use futures::{FutureExt, future::Shared};
use itertools::Itertools as _;
use language::LanguageName;
use remote::{Interactive, RemoteClient};
use rpc::proto;
use settings::{Settings, SettingsLocation};
use std::{
borrow::Cow,
@ -353,6 +354,16 @@ impl Project {
} else {
self.remote_client.clone()
};
let remote_shell_request = remote_client.as_ref().map(|remote_client| {
remote_client
.read(cx)
.proto_client()
.request(proto::GetTerminalShell {
project_id: proto::REMOTE_SERVER_PROJECT_ID,
worktree_id: settings_location
.map(|settings_location| settings_location.worktree_id.to_proto()),
})
});
let shell = match &remote_client {
Some(remote_client) => remote_client
.read(cx)
@ -373,7 +384,24 @@ impl Project {
let lang_registry = self.languages.clone();
cx.spawn(async move |project, cx| {
let shell_kind = ShellKind::new(&shell, path_style.is_windows());
let remote_shell = if let Some(remote_shell_request) = remote_shell_request {
let response = remote_shell_request
.await
.context("failed to get terminal shell settings from remote server")?;
let shell = response
.shell
.context("remote server returned no terminal shell")?;
let shell = task::shell_from_proto(shell)
.context("remote server returned an invalid terminal shell")?;
log::debug!(
"create_terminal_shell_internal: using remote terminal shell setting: {shell:?}"
);
Some(shell)
} else {
None
};
let shell_program = remote_shell.as_ref().map(Shell::program).unwrap_or(shell);
let shell_kind = ShellKind::new(&shell_program, path_style.is_windows());
let mut env = env_task.await.unwrap_or_default();
env.extend(settings.env);
@ -401,7 +429,15 @@ impl Project {
let (shell, env) = {
match remote_client {
Some(remote_client) => {
create_remote_shell(None, env, path, remote_client, cx)?
let empty_args = Vec::new();
let spawn_command = match remote_shell.as_ref() {
Some(Shell::System) | None => None,
Some(Shell::Program(program)) => Some((program, &empty_args)),
Some(Shell::WithArguments { program, args, .. }) => {
Some((program, args))
}
};
create_remote_shell(spawn_command, env, path, remote_client, cx)?
}
None => (settings.shell, env),
}

View file

@ -58,3 +58,12 @@ message GetDirectoryEnvironment {
message DirectoryEnvironment {
map<string, string> environment = 1;
}
message GetTerminalShell {
uint64 project_id = 1;
optional uint64 worktree_id = 2;
}
message GetTerminalShellResponse {
Shell shell = 1;
}

View file

@ -522,7 +522,9 @@ message Envelope {
LanguageServerShowDocumentRequest language_server_show_document_request = 490;
ExecuteLspCommand execute_lsp_command = 491;
ExecuteLspCommandResponse execute_lsp_command_response = 492;
RefreshDocumentHighlights refresh_document_highlights = 493; // current max
RefreshDocumentHighlights refresh_document_highlights = 493;
GetTerminalShell get_terminal_shell = 494;
GetTerminalShellResponse get_terminal_shell_response = 495; // current max
}
reserved 87 to 88;

View file

@ -143,6 +143,8 @@ messages!(
(GetReferencesResponse, Background),
(GetSignatureHelp, Background),
(GetSignatureHelpResponse, Background),
(GetTerminalShell, Background),
(GetTerminalShellResponse, Background),
(GetTypeDefinition, Background),
(GetTypeDefinitionResponse, Background),
(GetImplementation, Background),
@ -650,6 +652,7 @@ request_messages!(
(GetTreeDiff, GetTreeDiffResponse),
(GitClone, GitCloneResponse),
(GetDirectoryEnvironment, DirectoryEnvironment),
(GetTerminalShell, GetTerminalShellResponse),
(GetProcesses, GetProcessesResponse),
(GetAgentServerCommand, AgentServerCommand),
(GetContextServerCommand, ContextServerCommand),
@ -853,6 +856,7 @@ entity_messages!(
SetIndexText,
ToggleLspLogs,
GetDirectoryEnvironment,
GetTerminalShell,
Push,
Fetch,

View file

@ -68,6 +68,7 @@ smol.workspace = true
sysinfo.workspace = true
task.workspace = true
telemetry.workspace = true
terminal.workspace = true
util.workspace = true
watch.workspace = true
worktree.workspace = true

View file

@ -37,7 +37,7 @@ use rpc::{
};
use smol::process::Child;
use settings::initial_server_settings_content;
use settings::{Settings as _, SettingsLocation, initial_server_settings_content};
use std::{
num::NonZeroU64,
path::{Path, PathBuf},
@ -48,6 +48,7 @@ use std::{
time::Instant,
};
use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind};
use terminal::terminal_settings::TerminalSettings;
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
use worktree::Worktree;
@ -306,6 +307,7 @@ impl HeadlessProject {
session.add_entity_request_handler(Self::handle_find_search_candidates);
session.add_entity_request_handler(Self::handle_open_server_settings);
session.add_entity_request_handler(Self::handle_get_directory_environment);
session.add_entity_request_handler(Self::handle_get_terminal_shell);
session.add_entity_message_handler(Self::handle_toggle_lsp_logs);
session.add_entity_request_handler(Self::handle_open_image_by_path);
session.add_entity_request_handler(Self::handle_trust_worktrees);
@ -1400,6 +1402,26 @@ impl HeadlessProject {
.collect();
Ok(proto::DirectoryEnvironment { environment })
}
async fn handle_get_terminal_shell(
_this: Entity<Self>,
envelope: TypedEnvelope<proto::GetTerminalShell>,
cx: AsyncApp,
) -> Result<proto::GetTerminalShellResponse> {
let worktree_id = envelope.payload.worktree_id.map(WorktreeId::from_proto);
let shell = cx.update(|cx| {
let settings_location = worktree_id.map(|worktree_id| SettingsLocation {
worktree_id,
path: RelPath::empty(),
});
TerminalSettings::get(settings_location, cx).shell.clone()
});
log::debug!("handle_get_terminal_shell: resolved remote terminal shell setting: {shell:?}");
Ok(proto::GetTerminalShellResponse {
shell: Some(task::shell_to_proto(shell)),
})
}
}
fn prompt_to_proto(

View file

@ -870,7 +870,10 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
cx.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store.set_user_settings(
r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
r#"{
"languages": {"Rust": {"language_servers": ["from-local-settings"]}},
"terminal": {"shell": {"program": "client-shell"}}
}"#,
cx,
)
})
@ -891,7 +894,10 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
server_cx
.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store.set_server_settings(
r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
r#"{
"languages": {"Rust": {"language_servers": ["from-server-settings"]}},
"terminal": {"shell": {"program": "remote-shell"}}
}"#,
cx,
)
})
@ -909,6 +915,30 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
)
});
let remote_client = project.read_with(cx, |project, _| project.remote_client());
let remote_shell = if let Some(remote_client) = remote_client {
remote_client
.read_with(cx, |remote_client, _| {
remote_client
.proto_client()
.request(proto::GetTerminalShell {
project_id: proto::REMOTE_SERVER_PROJECT_ID,
worktree_id: None,
})
})
.await
.ok()
.and_then(|response| response.shell)
.and_then(|shell| task::shell_from_proto(shell).ok())
} else {
None
};
assert_eq!(
remote_shell,
Some(task::Shell::Program("remote-shell".to_string())),
"Server terminal shell settings should take precedence over user settings"
);
fs.insert_tree(
"/code/project1/.zed",
json!({