acp: Support terminal auth methods (#51999)
## Context Brings in support for the new terminal auth methods. Currently behind AcpBeta feature flag while the RFD stabilizes ## Self-Review Checklist <!-- Check before requesting review: --> - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A
This commit is contained in:
1 parent
b45534afa6
commit
d1177dc43c
7 files changed
+353
-143
No files matched your search
Generated
+1
@@ -271,6 +271,7 @@ dependencies = [
|
||||
"collections",
|
||||
"credentials_provider",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
|
||||
@@ -2,12 +2,13 @@ use crate::AcpThread;
|
||||
use agent_client_protocol::{self as acp};
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::IndexMap;
|
||||
use collections::{HashMap, IndexMap};
|
||||
use gpui::{Entity, SharedString, Task};
|
||||
use language_model::LanguageModelProviderId;
|
||||
use project::{AgentId, Project};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{any::Any, error::Error, fmt, path::PathBuf, rc::Rc, sync::Arc};
|
||||
use task::{HideStrategy, SpawnInTerminal, TaskId};
|
||||
use ui::{App, IconName};
|
||||
use util::path_list::PathList;
|
||||
use uuid::Uuid;
|
||||
@@ -21,6 +22,28 @@ impl UserMessageId {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_terminal_auth_task(
|
||||
id: String,
|
||||
label: String,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
) -> SpawnInTerminal {
|
||||
SpawnInTerminal {
|
||||
id: TaskId(id),
|
||||
full_label: label.clone(),
|
||||
label: label.clone(),
|
||||
command: Some(command),
|
||||
args,
|
||||
command_label: label,
|
||||
env,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
hide: HideStrategy::Always,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AgentConnection {
|
||||
fn agent_id(&self) -> AgentId;
|
||||
|
||||
@@ -90,6 +113,14 @@ pub trait AgentConnection {
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod];
|
||||
|
||||
fn terminal_auth_task(
|
||||
&self,
|
||||
_method: &acp::AuthMethodId,
|
||||
_cx: &App,
|
||||
) -> Option<SpawnInTerminal> {
|
||||
None
|
||||
}
|
||||
|
||||
fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
fn prompt(
|
||||
|
||||
@@ -30,6 +30,7 @@ env_logger = { workspace = true, optional = true }
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui_tokio = { workspace = true, optional = true }
|
||||
credentials_provider.workspace = true
|
||||
google_ai.workspace = true
|
||||
|
||||
@@ -7,13 +7,14 @@ use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::io::BufReader;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use project::{AgentId, Project};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
use task::ShellBuilder;
|
||||
use task::{ShellBuilder, SpawnInTerminal};
|
||||
use util::ResultExt as _;
|
||||
use util::path_list::PathList;
|
||||
use util::process::Child;
|
||||
@@ -33,6 +34,8 @@ use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings
|
||||
|
||||
use crate::GEMINI_ID;
|
||||
|
||||
pub const GEMINI_TERMINAL_AUTH_METHOD_ID: &str = "spawn-gemini-cli";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Unsupported version")]
|
||||
pub struct UnsupportedVersion;
|
||||
@@ -44,6 +47,7 @@ pub struct AcpConnection {
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
command: AgentServerCommand,
|
||||
agent_capabilities: acp::AgentCapabilities,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
@@ -286,6 +290,7 @@ impl AcpConnection {
|
||||
.read_text_file(true)
|
||||
.write_text_file(true))
|
||||
.terminal(true)
|
||||
.auth(acp::AuthCapabilities::new().terminal(true))
|
||||
// Experimental: Allow for rendering terminal output from the agents
|
||||
.meta(acp::Meta::from_iter([
|
||||
("terminal_output".into(), true.into()),
|
||||
@@ -335,7 +340,7 @@ impl AcpConnection {
|
||||
});
|
||||
let meta = acp::Meta::from_iter([("terminal-auth".to_string(), value)]);
|
||||
vec![acp::AuthMethod::Agent(
|
||||
acp::AuthMethodAgent::new("spawn-gemini-cli", "Login")
|
||||
acp::AuthMethodAgent::new(GEMINI_TERMINAL_AUTH_METHOD_ID, "Login")
|
||||
.description("Login with your Google or Vertex AI account")
|
||||
.meta(meta),
|
||||
)]
|
||||
@@ -345,6 +350,7 @@ impl AcpConnection {
|
||||
Ok(Self {
|
||||
id: agent_id,
|
||||
auth_methods,
|
||||
command,
|
||||
connection,
|
||||
display_name,
|
||||
telemetry_id,
|
||||
@@ -468,6 +474,64 @@ impl Drop for AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_auth_task_id(agent_id: &AgentId, method_id: &acp::AuthMethodId) -> String {
|
||||
format!("external-agent-{}-{}-login", agent_id.0, method_id.0)
|
||||
}
|
||||
|
||||
fn terminal_auth_task(
|
||||
command: &AgentServerCommand,
|
||||
agent_id: &AgentId,
|
||||
method: &acp::AuthMethodTerminal,
|
||||
) -> SpawnInTerminal {
|
||||
let mut args = command.args.clone();
|
||||
args.extend(method.args.clone());
|
||||
|
||||
let mut env = command.env.clone().unwrap_or_default();
|
||||
env.extend(method.env.clone());
|
||||
|
||||
acp_thread::build_terminal_auth_task(
|
||||
terminal_auth_task_id(agent_id, &method.id),
|
||||
method.name.clone(),
|
||||
command.path.to_string_lossy().into_owned(),
|
||||
args,
|
||||
env,
|
||||
)
|
||||
}
|
||||
|
||||
/// Used to support the _meta method prior to stabilization
|
||||
fn meta_terminal_auth_task(
|
||||
agent_id: &AgentId,
|
||||
method_id: &acp::AuthMethodId,
|
||||
method: &acp::AuthMethod,
|
||||
) -> Option<SpawnInTerminal> {
|
||||
#[derive(Deserialize)]
|
||||
struct MetaTerminalAuth {
|
||||
label: String,
|
||||
command: String,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
let meta = match method {
|
||||
acp::AuthMethod::EnvVar(env_var) => env_var.meta.as_ref(),
|
||||
acp::AuthMethod::Terminal(terminal) => terminal.meta.as_ref(),
|
||||
acp::AuthMethod::Agent(agent) => agent.meta.as_ref(),
|
||||
_ => None,
|
||||
}?;
|
||||
let terminal_auth =
|
||||
serde_json::from_value::<MetaTerminalAuth>(meta.get("terminal-auth")?.clone()).ok()?;
|
||||
|
||||
Some(acp_thread::build_terminal_auth_task(
|
||||
terminal_auth_task_id(agent_id, method_id),
|
||||
terminal_auth.label.clone(),
|
||||
terminal_auth.command,
|
||||
terminal_auth.args,
|
||||
terminal_auth.env,
|
||||
))
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
fn agent_id(&self) -> AgentId {
|
||||
self.id.clone()
|
||||
@@ -813,6 +877,24 @@ impl AgentConnection for AcpConnection {
|
||||
&self.auth_methods
|
||||
}
|
||||
|
||||
fn terminal_auth_task(
|
||||
&self,
|
||||
method_id: &acp::AuthMethodId,
|
||||
cx: &App,
|
||||
) -> Option<SpawnInTerminal> {
|
||||
let method = self
|
||||
.auth_methods
|
||||
.iter()
|
||||
.find(|method| method.id() == method_id)?;
|
||||
|
||||
match method {
|
||||
acp::AuthMethod::Terminal(terminal) if cx.has_flag::<AcpBetaFeatureFlag>() => {
|
||||
Some(terminal_auth_task(&self.command, &self.id, terminal))
|
||||
}
|
||||
_ => meta_terminal_auth_task(&self.id, method_id, method),
|
||||
}
|
||||
}
|
||||
|
||||
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let conn = self.connection.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
@@ -979,6 +1061,149 @@ fn map_acp_error(err: acp::Error) -> anyhow::Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn terminal_auth_task_reuses_command_and_merges_args_and_env() {
|
||||
let command = AgentServerCommand {
|
||||
path: "/path/to/agent".into(),
|
||||
args: vec!["--acp".into(), "--verbose".into()],
|
||||
env: Some(HashMap::from_iter([
|
||||
("BASE".into(), "1".into()),
|
||||
("SHARED".into(), "base".into()),
|
||||
])),
|
||||
};
|
||||
let method = acp::AuthMethodTerminal::new("login", "Login")
|
||||
.args(vec!["/auth".into()])
|
||||
.env(std::collections::HashMap::from_iter([
|
||||
("EXTRA".into(), "2".into()),
|
||||
("SHARED".into(), "override".into()),
|
||||
]));
|
||||
|
||||
let terminal_auth_task = terminal_auth_task(&command, &AgentId::new("test-agent"), &method);
|
||||
|
||||
assert_eq!(
|
||||
terminal_auth_task.command.as_deref(),
|
||||
Some("/path/to/agent")
|
||||
);
|
||||
assert_eq!(terminal_auth_task.args, vec!["--acp", "--verbose", "/auth"]);
|
||||
assert_eq!(
|
||||
terminal_auth_task.env,
|
||||
HashMap::from_iter([
|
||||
("BASE".into(), "1".into()),
|
||||
("SHARED".into(), "override".into()),
|
||||
("EXTRA".into(), "2".into()),
|
||||
])
|
||||
);
|
||||
assert_eq!(terminal_auth_task.label, "Login");
|
||||
assert_eq!(terminal_auth_task.command_label, "Login");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_terminal_auth_task_parses_meta_and_retries_session() {
|
||||
let method_id = acp::AuthMethodId::new("legacy-login");
|
||||
let method = acp::AuthMethod::Agent(
|
||||
acp::AuthMethodAgent::new(method_id.clone(), "Login").meta(acp::Meta::from_iter([(
|
||||
"terminal-auth".to_string(),
|
||||
serde_json::json!({
|
||||
"label": "legacy /auth",
|
||||
"command": "legacy-agent",
|
||||
"args": ["auth", "--interactive"],
|
||||
"env": {
|
||||
"AUTH_MODE": "interactive",
|
||||
},
|
||||
}),
|
||||
)])),
|
||||
);
|
||||
|
||||
let terminal_auth_task =
|
||||
meta_terminal_auth_task(&AgentId::new("test-agent"), &method_id, &method)
|
||||
.expect("expected legacy terminal auth task");
|
||||
|
||||
assert_eq!(
|
||||
terminal_auth_task.id.0,
|
||||
"external-agent-test-agent-legacy-login-login"
|
||||
);
|
||||
assert_eq!(terminal_auth_task.command.as_deref(), Some("legacy-agent"));
|
||||
assert_eq!(terminal_auth_task.args, vec!["auth", "--interactive"]);
|
||||
assert_eq!(
|
||||
terminal_auth_task.env,
|
||||
HashMap::from_iter([("AUTH_MODE".into(), "interactive".into())])
|
||||
);
|
||||
assert_eq!(terminal_auth_task.label, "legacy /auth");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_terminal_auth_task_returns_none_for_invalid_meta() {
|
||||
let method_id = acp::AuthMethodId::new("legacy-login");
|
||||
let method = acp::AuthMethod::Agent(
|
||||
acp::AuthMethodAgent::new(method_id.clone(), "Login").meta(acp::Meta::from_iter([(
|
||||
"terminal-auth".to_string(),
|
||||
serde_json::json!({
|
||||
"label": "legacy /auth",
|
||||
}),
|
||||
)])),
|
||||
);
|
||||
|
||||
assert!(
|
||||
meta_terminal_auth_task(&AgentId::new("test-agent"), &method_id, &method).is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_class_terminal_auth_takes_precedence_over_legacy_meta() {
|
||||
let method_id = acp::AuthMethodId::new("login");
|
||||
let method = acp::AuthMethod::Terminal(
|
||||
acp::AuthMethodTerminal::new(method_id, "Login")
|
||||
.args(vec!["/auth".into()])
|
||||
.env(std::collections::HashMap::from_iter([(
|
||||
"AUTH_MODE".into(),
|
||||
"first-class".into(),
|
||||
)]))
|
||||
.meta(acp::Meta::from_iter([(
|
||||
"terminal-auth".to_string(),
|
||||
serde_json::json!({
|
||||
"label": "legacy /auth",
|
||||
"command": "legacy-agent",
|
||||
"args": ["legacy-auth"],
|
||||
"env": {
|
||||
"AUTH_MODE": "legacy",
|
||||
},
|
||||
}),
|
||||
)])),
|
||||
);
|
||||
|
||||
let command = AgentServerCommand {
|
||||
path: "/path/to/agent".into(),
|
||||
args: vec!["--acp".into()],
|
||||
env: Some(HashMap::from_iter([("BASE".into(), "1".into())])),
|
||||
};
|
||||
|
||||
let terminal_auth_task = match &method {
|
||||
acp::AuthMethod::Terminal(terminal) => {
|
||||
terminal_auth_task(&command, &AgentId::new("test-agent"), terminal)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
terminal_auth_task.command.as_deref(),
|
||||
Some("/path/to/agent")
|
||||
);
|
||||
assert_eq!(terminal_auth_task.args, vec!["--acp", "/auth"]);
|
||||
assert_eq!(
|
||||
terminal_auth_task.env,
|
||||
HashMap::from_iter([
|
||||
("BASE".into(), "1".into()),
|
||||
("AUTH_MODE".into(), "first-class".into()),
|
||||
])
|
||||
);
|
||||
assert_eq!(terminal_auth_task.label, "Login");
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_servers_for_project(project: &Entity<Project>, cx: &App) -> Vec<acp::McpServer> {
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let is_local = project.read(cx).is_local();
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui::{App, AppContext, Entity, Task};
|
||||
use settings::SettingsStore;
|
||||
use std::{any::Any, rc::Rc, sync::Arc};
|
||||
|
||||
pub use acp::AcpConnection;
|
||||
pub use acp::{AcpConnection, GEMINI_TERMINAL_AUTH_METHOD_ID};
|
||||
|
||||
pub struct AgentServerDelegate {
|
||||
store: Entity<AgentServerStore>,
|
||||
|
||||
@@ -8,9 +8,9 @@ use acp_thread::{AgentConnection, Plan};
|
||||
use action_log::{ActionLog, ActionLogTelemetry, DiffStats};
|
||||
use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::AgentServer;
|
||||
#[cfg(test)]
|
||||
use agent_servers::AgentServerDelegate;
|
||||
use agent_servers::{AgentServer, GEMINI_TERMINAL_AUTH_METHOD_ID};
|
||||
use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use anyhow::{Result, anyhow};
|
||||
use arrayvec::ArrayVec;
|
||||
@@ -1475,6 +1475,9 @@ impl ConversationView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(connected) = self.as_connected_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -1491,119 +1494,65 @@ impl ConversationView {
|
||||
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
// Check for the experimental "terminal-auth" _meta field
|
||||
let auth_method = connection.auth_methods().iter().find(|m| m.id() == &method);
|
||||
if let Some(login) = connection.terminal_auth_task(&method, cx) {
|
||||
configuration_view.take();
|
||||
pending_auth_method.replace(method.clone());
|
||||
|
||||
if let Some(terminal_auth) = auth_method
|
||||
.and_then(|a| match a {
|
||||
acp::AuthMethod::EnvVar(env_var) => env_var.meta.as_ref(),
|
||||
acp::AuthMethod::Terminal(terminal) => terminal.meta.as_ref(),
|
||||
acp::AuthMethod::Agent(agent) => agent.meta.as_ref(),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|m| m.get("terminal-auth"))
|
||||
{
|
||||
// Extract terminal auth details from meta
|
||||
if let (Some(command), Some(label)) = (
|
||||
terminal_auth.get("command").and_then(|v| v.as_str()),
|
||||
terminal_auth.get("label").and_then(|v| v.as_str()),
|
||||
) {
|
||||
let args = terminal_auth
|
||||
.get("args")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let project = self.project.clone();
|
||||
let authenticate = Self::spawn_external_agent_login(
|
||||
login,
|
||||
workspace,
|
||||
project,
|
||||
method.clone(),
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
|
||||
let env = terminal_auth
|
||||
.get("env")
|
||||
.and_then(|v| v.as_object())
|
||||
.map(|obj| {
|
||||
obj.iter()
|
||||
.filter_map(|(k, v)| v.as_str().map(|val| (k.clone(), val.to_string())))
|
||||
.collect::<HashMap<String, String>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build SpawnInTerminal from _meta
|
||||
let login = task::SpawnInTerminal {
|
||||
id: task::TaskId(format!("external-agent-{}-login", label)),
|
||||
full_label: label.to_string(),
|
||||
label: label.to_string(),
|
||||
command: Some(command.to_string()),
|
||||
args,
|
||||
command_label: label.to_string(),
|
||||
env,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
hide: task::HideStrategy::Always,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
configuration_view.take();
|
||||
pending_auth_method.replace(method.clone());
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
let project = self.project.clone();
|
||||
let authenticate = Self::spawn_external_agent_login(
|
||||
login,
|
||||
workspace,
|
||||
project,
|
||||
method.clone(),
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
self.auth_task = Some(cx.spawn_in(window, {
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
|
||||
match &result {
|
||||
Ok(_) => telemetry::event!(
|
||||
"Authenticate Agent Succeeded",
|
||||
agent = agent_telemetry_id
|
||||
),
|
||||
Err(_) => {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Failed",
|
||||
agent = agent_telemetry_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if let Err(err) = result {
|
||||
if let Some(ConnectedServerState {
|
||||
auth_state:
|
||||
AuthState::Unauthenticated {
|
||||
pending_auth_method,
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = this.as_connected_mut()
|
||||
{
|
||||
pending_auth_method.take();
|
||||
}
|
||||
if let Some(active) = this.active_thread() {
|
||||
active.update(cx, |active, cx| {
|
||||
active.handle_thread_error(err, cx);
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.reset(window, cx);
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
.ok();
|
||||
match &result {
|
||||
Ok(_) => telemetry::event!(
|
||||
"Authenticate Agent Succeeded",
|
||||
agent = agent_telemetry_id
|
||||
),
|
||||
Err(_) => {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Failed",
|
||||
agent = agent_telemetry_id,
|
||||
)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if let Err(err) = result {
|
||||
if let Some(ConnectedServerState {
|
||||
auth_state:
|
||||
AuthState::Unauthenticated {
|
||||
pending_auth_method,
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = this.as_connected_mut()
|
||||
{
|
||||
pending_auth_method.take();
|
||||
}
|
||||
if let Some(active) = this.active_thread() {
|
||||
active.update(cx, |active, cx| {
|
||||
active.handle_thread_error(err, cx);
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.reset(window, cx);
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
configuration_view.take();
|
||||
@@ -1726,7 +1675,7 @@ impl ConversationView {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return Task::ready(Ok(()));
|
||||
return Task::ready(Err(anyhow!("Terminal panel is unavailable")));
|
||||
};
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
@@ -1734,17 +1683,14 @@ impl ConversationView {
|
||||
if let Some(cmd) = &task.command {
|
||||
// Have "node" command use Zed's managed Node runtime by default
|
||||
if cmd == "node" {
|
||||
let resolved_node_runtime = project
|
||||
.update(cx, |project, cx| {
|
||||
let agent_server_store = project.agent_server_store().clone();
|
||||
agent_server_store.update(cx, |store, cx| {
|
||||
store.node_runtime().map(|node_runtime| {
|
||||
cx.background_spawn(async move {
|
||||
node_runtime.binary_path().await
|
||||
})
|
||||
})
|
||||
let resolved_node_runtime = project.update(cx, |project, cx| {
|
||||
let agent_server_store = project.agent_server_store().clone();
|
||||
agent_server_store.update(cx, |store, cx| {
|
||||
store.node_runtime().map(|node_runtime| {
|
||||
cx.background_spawn(async move { node_runtime.binary_path().await })
|
||||
})
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(resolve_task) = resolved_node_runtime {
|
||||
if let Ok(node_path) = resolve_task.await {
|
||||
@@ -1756,14 +1702,8 @@ impl ConversationView {
|
||||
task.shell = task::Shell::WithArguments {
|
||||
program: task.command.take().expect("login command should be set"),
|
||||
args: std::mem::take(&mut task.args),
|
||||
title_override: None
|
||||
title_override: None,
|
||||
};
|
||||
task.full_label = task.label.clone();
|
||||
task.id = task::TaskId(format!("external-agent-{}-login", task.label));
|
||||
task.command_label = task.label.clone();
|
||||
task.use_new_terminal = true;
|
||||
task.allow_concurrent_runs = true;
|
||||
task.hide = task::HideStrategy::Always;
|
||||
|
||||
let terminal = terminal_panel
|
||||
.update_in(cx, |terminal_panel, window, cx| {
|
||||
@@ -1772,7 +1712,7 @@ impl ConversationView {
|
||||
.await?;
|
||||
|
||||
let success_patterns = match method.0.as_ref() {
|
||||
"claude-login" | "spawn-gemini-cli" => vec![
|
||||
"claude-login" | GEMINI_TERMINAL_AUTH_METHOD_ID => vec![
|
||||
"Login successful".to_string(),
|
||||
"Type your message".to_string(),
|
||||
],
|
||||
@@ -1806,7 +1746,9 @@ impl ConversationView {
|
||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||
let content =
|
||||
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
|
||||
if success_patterns.iter().any(|pattern| content.contains(pattern))
|
||||
if success_patterns
|
||||
.iter()
|
||||
.any(|pattern| content.contains(pattern))
|
||||
{
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
@@ -1823,8 +1765,23 @@ impl ConversationView {
|
||||
}
|
||||
}
|
||||
_ = exit_status => {
|
||||
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server()) && login.label.contains("gemini") {
|
||||
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), method, true, window, cx))?.await
|
||||
if !previous_attempt
|
||||
&& project.read_with(cx, |project, _| project.is_via_remote_server())
|
||||
&& method.0.as_ref() == GEMINI_TERMINAL_AUTH_METHOD_ID
|
||||
{
|
||||
return cx
|
||||
.update(|window, cx| {
|
||||
Self::spawn_external_agent_login(
|
||||
login,
|
||||
workspace,
|
||||
project.clone(),
|
||||
method,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
return Err(anyhow!("exited before logging in"));
|
||||
}
|
||||
|
||||
@@ -1374,13 +1374,8 @@ impl ExternalAgentServer for LocalRegistryNpxAgent {
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut exec_args = Vec::new();
|
||||
exec_args.push("--yes".to_string());
|
||||
exec_args.push(package.to_string());
|
||||
if !args.is_empty() {
|
||||
exec_args.push("--".to_string());
|
||||
exec_args.extend(args);
|
||||
}
|
||||
let mut exec_args = vec!["--yes".to_string(), "--".to_string(), package.to_string()];
|
||||
exec_args.extend(args);
|
||||
|
||||
let npm_command = node_runtime
|
||||
.npm_command(
|
||||
|
||||
Reference in new issue
Block a user