Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/apps/desktop/src/api/acp_client_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use tauri::{AppHandle, Emitter, State};
#[serde(rename_all = "camelCase")]
pub struct AcpClientIdRequest {
pub client_id: String,
#[serde(default)]
pub remote_connection_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -146,7 +148,7 @@ pub async fn install_acp_client_cli(
.as_ref()
.ok_or_else(|| "ACP client service not initialized".to_string())?;
service
.install_client_cli(&request.client_id)
.install_client_cli(&request.client_id, request.remote_connection_id.as_deref())
.await
.map_err(|e| e.to_string())
}
Expand Down
72 changes: 0 additions & 72 deletions src/crates/acp/src/client/builtin_clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,6 @@ pub(crate) fn builtin_acp_client_preset(
.find(|preset| preset.id == client_id)
}

pub(crate) fn supported_remote_acp_clients() -> String {
builtin_client_ids().collect::<Vec<_>>().join(", ")
}

pub(crate) fn default_config_for_builtin_client(client_id: &str) -> Option<AcpClientConfig> {
let preset = builtin_acp_client_preset(client_id)?;
Some(AcpClientConfig {
Expand All @@ -75,65 +71,10 @@ pub(crate) fn default_config_for_builtin_client(client_id: &str) -> Option<AcpCl
})
}

pub(crate) fn remote_command_for_builtin_client(client_id: &str) -> Option<String> {
let preset = builtin_acp_client_preset(client_id)?;
Some(render_shell_command(preset.command, preset.args))
}

pub(crate) fn remote_command_for_builtin_client_in_workspace(
client_id: &str,
workspace_path: &str,
) -> Option<String> {
let command = remote_command_for_builtin_client(client_id)?;
let workspace_path = workspace_path.trim();
if workspace_path.is_empty() {
return Some(command);
}
Some(format!(
"cd {} && {}",
shell_escape(workspace_path),
command
))
}

fn render_shell_command(command: &str, args: &[&str]) -> String {
std::iter::once(command)
.chain(args.iter().copied())
.map(shell_escape)
.collect::<Vec<_>>()
.join(" ")
}

fn shell_escape(value: &str) -> String {
if value.chars().all(|ch| {
ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '-' | '_' | ':' | '=' | '@')
}) {
value.to_string()
} else {
format!("'{}'", value.replace('\'', "'\\''"))
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn builds_remote_builtin_command_for_opencode() {
assert_eq!(
remote_command_for_builtin_client("opencode").as_deref(),
Some("opencode acp")
);
}

#[test]
fn builds_remote_builtin_command_for_npx_adapter() {
assert_eq!(
remote_command_for_builtin_client("codex").as_deref(),
Some("npx --yes @zed-industries/codex-acp@latest")
);
}

#[test]
fn returns_default_config_for_builtin_client() {
let config = default_config_for_builtin_client("claude-code").expect("builtin config");
Expand All @@ -144,17 +85,4 @@ mod tests {
vec!["--yes", "@zed-industries/claude-code-acp@latest"]
);
}

#[test]
fn shell_escape_quotes_spaces() {
assert_eq!(shell_escape("hello world"), "'hello world'");
}

#[test]
fn builds_remote_builtin_command_in_workspace() {
assert_eq!(
remote_command_for_builtin_client_in_workspace("opencode", "/tmp/my repo").as_deref(),
Some("cd '/tmp/my repo' && opencode acp")
);
}
}
Loading
Loading