Merge branch 'main' into thread-view-papercuts
This commit is contained in:
commit
55b3f3720e
22 changed files with 477 additions and 116 deletions
|
@ -1629,6 +1629,9 @@
|
||||||
"allowed": true
|
"allowed": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Kotlin": {
|
||||||
|
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
|
||||||
|
},
|
||||||
"LaTeX": {
|
"LaTeX": {
|
||||||
"formatter": "language_server",
|
"formatter": "language_server",
|
||||||
"language_servers": ["texlab", "..."],
|
"language_servers": ["texlab", "..."],
|
||||||
|
|
|
@ -756,6 +756,8 @@ pub struct AcpThread {
|
||||||
connection: Rc<dyn AgentConnection>,
|
connection: Rc<dyn AgentConnection>,
|
||||||
session_id: acp::SessionId,
|
session_id: acp::SessionId,
|
||||||
token_usage: Option<TokenUsage>,
|
token_usage: Option<TokenUsage>,
|
||||||
|
prompt_capabilities: acp::PromptCapabilities,
|
||||||
|
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -770,6 +772,7 @@ pub enum AcpThreadEvent {
|
||||||
Stopped,
|
Stopped,
|
||||||
Error,
|
Error,
|
||||||
LoadError(LoadError),
|
LoadError(LoadError),
|
||||||
|
PromptCapabilitiesUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||||
|
@ -821,7 +824,20 @@ impl AcpThread {
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
action_log: Entity<ActionLog>,
|
action_log: Entity<ActionLog>,
|
||||||
session_id: acp::SessionId,
|
session_id: acp::SessionId,
|
||||||
|
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let prompt_capabilities = *prompt_capabilities_rx.borrow();
|
||||||
|
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
|
||||||
|
loop {
|
||||||
|
let caps = prompt_capabilities_rx.recv().await?;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.prompt_capabilities = caps;
|
||||||
|
cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
action_log,
|
action_log,
|
||||||
shared_buffers: Default::default(),
|
shared_buffers: Default::default(),
|
||||||
|
@ -833,9 +849,15 @@ impl AcpThread {
|
||||||
connection,
|
connection,
|
||||||
session_id,
|
session_id,
|
||||||
token_usage: None,
|
token_usage: None,
|
||||||
|
prompt_capabilities,
|
||||||
|
_observe_prompt_capabilities: task,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||||
|
self.prompt_capabilities
|
||||||
|
}
|
||||||
|
|
||||||
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
||||||
&self.connection
|
&self.connection
|
||||||
}
|
}
|
||||||
|
@ -2599,13 +2621,19 @@ mod tests {
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
let thread = cx.new(|_cx| {
|
let thread = cx.new(|cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
"Test",
|
"Test",
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
watch::Receiver::constant(acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: true,
|
||||||
|
embedded_context: true,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
self.sessions.lock().insert(session_id, thread.downgrade());
|
self.sessions.lock().insert(session_id, thread.downgrade());
|
||||||
|
@ -2639,14 +2667,6 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: true,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||||
let sessions = self.sessions.lock();
|
let sessions = self.sessions.lock();
|
||||||
let thread = sessions.get(session_id).unwrap().clone();
|
let thread = sessions.get(session_id).unwrap().clone();
|
||||||
|
|
|
@ -38,8 +38,6 @@ pub trait AgentConnection {
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<acp::PromptResponse>>;
|
) -> Task<Result<acp::PromptResponse>>;
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities;
|
|
||||||
|
|
||||||
fn resume(
|
fn resume(
|
||||||
&self,
|
&self,
|
||||||
_session_id: &acp::SessionId,
|
_session_id: &acp::SessionId,
|
||||||
|
@ -329,13 +327,19 @@ mod test_support {
|
||||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||||
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
|
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||||
let thread = cx.new(|_cx| {
|
let thread = cx.new(|cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
"Test",
|
"Test",
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
watch::Receiver::constant(acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: true,
|
||||||
|
embedded_context: true,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
self.sessions.lock().insert(
|
self.sessions.lock().insert(
|
||||||
|
@ -348,14 +352,6 @@ mod test_support {
|
||||||
Task::ready(Ok(thread))
|
Task::ready(Ok(thread))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: true,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn authenticate(
|
fn authenticate(
|
||||||
&self,
|
&self,
|
||||||
_method_id: acp::AuthMethodId,
|
_method_id: acp::AuthMethodId,
|
||||||
|
|
|
@ -240,13 +240,16 @@ impl NativeAgent {
|
||||||
let title = thread.title();
|
let title = thread.title();
|
||||||
let project = thread.project.clone();
|
let project = thread.project.clone();
|
||||||
let action_log = thread.action_log.clone();
|
let action_log = thread.action_log.clone();
|
||||||
let acp_thread = cx.new(|_cx| {
|
let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();
|
||||||
|
let acp_thread = cx.new(|cx| {
|
||||||
acp_thread::AcpThread::new(
|
acp_thread::AcpThread::new(
|
||||||
title,
|
title,
|
||||||
connection,
|
connection,
|
||||||
project.clone(),
|
project.clone(),
|
||||||
action_log.clone(),
|
action_log.clone(),
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
prompt_capabilities_rx,
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
|
@ -925,14 +928,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: false,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resume(
|
fn resume(
|
||||||
&self,
|
&self,
|
||||||
session_id: &acp::SessionId,
|
session_id: &acp::SessionId,
|
||||||
|
|
|
@ -22,6 +22,10 @@ impl NativeAgentServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentServer for NativeAgentServer {
|
impl AgentServer for NativeAgentServer {
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"zed"
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Zed Agent".into()
|
"Zed Agent".into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -575,11 +575,22 @@ pub struct Thread {
|
||||||
templates: Arc<Templates>,
|
templates: Arc<Templates>,
|
||||||
model: Option<Arc<dyn LanguageModel>>,
|
model: Option<Arc<dyn LanguageModel>>,
|
||||||
summarization_model: Option<Arc<dyn LanguageModel>>,
|
summarization_model: Option<Arc<dyn LanguageModel>>,
|
||||||
|
prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
|
||||||
|
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||||
pub(crate) project: Entity<Project>,
|
pub(crate) project: Entity<Project>,
|
||||||
pub(crate) action_log: Entity<ActionLog>,
|
pub(crate) action_log: Entity<ActionLog>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Thread {
|
impl Thread {
|
||||||
|
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
|
||||||
|
let image = model.map_or(true, |model| model.supports_images());
|
||||||
|
acp::PromptCapabilities {
|
||||||
|
image,
|
||||||
|
audio: false,
|
||||||
|
embedded_context: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
project_context: Entity<ProjectContext>,
|
project_context: Entity<ProjectContext>,
|
||||||
|
@ -590,6 +601,8 @@ impl Thread {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
||||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||||
|
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
||||||
|
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
||||||
Self {
|
Self {
|
||||||
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
|
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
|
||||||
prompt_id: PromptId::new(),
|
prompt_id: PromptId::new(),
|
||||||
|
@ -617,6 +630,8 @@ impl Thread {
|
||||||
templates,
|
templates,
|
||||||
model,
|
model,
|
||||||
summarization_model: None,
|
summarization_model: None,
|
||||||
|
prompt_capabilities_tx,
|
||||||
|
prompt_capabilities_rx,
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
}
|
}
|
||||||
|
@ -750,6 +765,8 @@ impl Thread {
|
||||||
.or_else(|| registry.default_model())
|
.or_else(|| registry.default_model())
|
||||||
.map(|model| model.model)
|
.map(|model| model.model)
|
||||||
});
|
});
|
||||||
|
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
||||||
|
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
@ -779,6 +796,8 @@ impl Thread {
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
updated_at: db_thread.updated_at,
|
updated_at: db_thread.updated_at,
|
||||||
|
prompt_capabilities_tx,
|
||||||
|
prompt_capabilities_rx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -946,10 +965,12 @@ impl Thread {
|
||||||
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
|
||||||
let old_usage = self.latest_token_usage();
|
let old_usage = self.latest_token_usage();
|
||||||
self.model = Some(model);
|
self.model = Some(model);
|
||||||
|
let new_caps = Self::prompt_capabilities(self.model.as_deref());
|
||||||
let new_usage = self.latest_token_usage();
|
let new_usage = self.latest_token_usage();
|
||||||
if old_usage != new_usage {
|
if old_usage != new_usage {
|
||||||
cx.emit(TokenUsageUpdated(new_usage));
|
cx.emit(TokenUsageUpdated(new_usage));
|
||||||
}
|
}
|
||||||
|
self.prompt_capabilities_tx.send(new_caps).log_err();
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -185,13 +185,16 @@ impl AgentConnection for AcpConnection {
|
||||||
|
|
||||||
let session_id = response.session_id;
|
let session_id = response.session_id;
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||||
let thread = cx.new(|_cx| {
|
let thread = cx.new(|cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
self.server_name.clone(),
|
self.server_name.clone(),
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
|
||||||
|
watch::Receiver::constant(self.prompt_capabilities),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -279,10 +282,6 @@ impl AgentConnection for AcpConnection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
self.prompt_capabilities
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
|
||||||
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
|
||||||
session.suppress_abort_err = true;
|
session.suppress_abort_err = true;
|
||||||
|
|
|
@ -36,6 +36,7 @@ pub trait AgentServer: Send {
|
||||||
fn name(&self) -> SharedString;
|
fn name(&self) -> SharedString;
|
||||||
fn empty_state_headline(&self) -> SharedString;
|
fn empty_state_headline(&self) -> SharedString;
|
||||||
fn empty_state_message(&self) -> SharedString;
|
fn empty_state_message(&self) -> SharedString;
|
||||||
|
fn telemetry_id(&self) -> &'static str;
|
||||||
|
|
||||||
fn connect(
|
fn connect(
|
||||||
&self,
|
&self,
|
||||||
|
@ -97,7 +98,7 @@ pub struct AgentServerCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentServerCommand {
|
impl AgentServerCommand {
|
||||||
pub(crate) async fn resolve(
|
pub async fn resolve(
|
||||||
path_bin_name: &'static str,
|
path_bin_name: &'static str,
|
||||||
extra_args: &[&'static str],
|
extra_args: &[&'static str],
|
||||||
fallback_path: Option<&Path>,
|
fallback_path: Option<&Path>,
|
||||||
|
|
|
@ -43,6 +43,10 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
|
||||||
pub struct ClaudeCode;
|
pub struct ClaudeCode;
|
||||||
|
|
||||||
impl AgentServer for ClaudeCode {
|
impl AgentServer for ClaudeCode {
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"claude-code"
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Claude Code".into()
|
"Claude Code".into()
|
||||||
}
|
}
|
||||||
|
@ -249,13 +253,19 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||||
});
|
});
|
||||||
|
|
||||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||||
let thread = cx.new(|_cx| {
|
let thread = cx.new(|cx| {
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
"Claude Code",
|
"Claude Code",
|
||||||
self.clone(),
|
self.clone(),
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
|
watch::Receiver::constant(acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: false,
|
||||||
|
embedded_context: true,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -319,14 +329,6 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||||
cx.foreground_executor().spawn(async move { end_rx.await? })
|
cx.foreground_executor().spawn(async move { end_rx.await? })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: false,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
|
||||||
let sessions = self.sessions.borrow();
|
let sessions = self.sessions.borrow();
|
||||||
let Some(session) = sessions.get(session_id) else {
|
let Some(session) = sessions.get(session_id) else {
|
||||||
|
|
|
@ -22,6 +22,10 @@ impl CustomAgentServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::AgentServer for CustomAgentServer {
|
impl crate::AgentServer for CustomAgentServer {
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"custom"
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
self.name.clone()
|
self.name.clone()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,10 @@ pub struct Gemini;
|
||||||
const ACP_ARG: &str = "--experimental-acp";
|
const ACP_ARG: &str = "--experimental-acp";
|
||||||
|
|
||||||
impl AgentServer for Gemini {
|
impl AgentServer for Gemini {
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"gemini-cli"
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> SharedString {
|
fn name(&self) -> SharedString {
|
||||||
"Gemini CLI".into()
|
"Gemini CLI".into()
|
||||||
}
|
}
|
||||||
|
@ -53,7 +57,7 @@ impl AgentServer for Gemini {
|
||||||
return Err(LoadError::NotInstalled {
|
return Err(LoadError::NotInstalled {
|
||||||
error_message: "Failed to find Gemini CLI binary".into(),
|
error_message: "Failed to find Gemini CLI binary".into(),
|
||||||
install_message: "Install Gemini CLI".into(),
|
install_message: "Install Gemini CLI".into(),
|
||||||
install_command: "npm install -g @google/gemini-cli@preview".into()
|
install_command: Self::install_command().into(),
|
||||||
}.into());
|
}.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,7 +92,7 @@ impl AgentServer for Gemini {
|
||||||
current_version
|
current_version
|
||||||
).into(),
|
).into(),
|
||||||
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
upgrade_message: "Upgrade Gemini CLI to latest".into(),
|
||||||
upgrade_command: "npm install -g @google/gemini-cli@preview".into(),
|
upgrade_command: Self::upgrade_command().into(),
|
||||||
}.into())
|
}.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,6 +105,20 @@ impl AgentServer for Gemini {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Gemini {
|
||||||
|
pub fn binary_name() -> &'static str {
|
||||||
|
"gemini"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_command() -> &'static str {
|
||||||
|
"npm install -g @google/gemini-cli@preview"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upgrade_command() -> &'static str {
|
||||||
|
"npm install -g @google/gemini-cli@preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -373,7 +373,7 @@ impl MessageEditor {
|
||||||
|
|
||||||
if Img::extensions().contains(&extension) && !extension.contains("svg") {
|
if Img::extensions().contains(&extension) && !extension.contains("svg") {
|
||||||
if !self.prompt_capabilities.get().image {
|
if !self.prompt_capabilities.get().image {
|
||||||
return Task::ready(Err(anyhow!("This agent does not support images yet")));
|
return Task::ready(Err(anyhow!("This model does not support images yet")));
|
||||||
}
|
}
|
||||||
let task = self
|
let task = self
|
||||||
.project
|
.project
|
||||||
|
|
|
@ -475,7 +475,7 @@ impl AcpThreadView {
|
||||||
let action_log = thread.read(cx).action_log().clone();
|
let action_log = thread.read(cx).action_log().clone();
|
||||||
|
|
||||||
this.prompt_capabilities
|
this.prompt_capabilities
|
||||||
.set(connection.prompt_capabilities());
|
.set(thread.read(cx).prompt_capabilities());
|
||||||
|
|
||||||
let count = thread.read(cx).entries().len();
|
let count = thread.read(cx).entries().len();
|
||||||
this.list_state.splice(0..0, count);
|
this.list_state.splice(0..0, count);
|
||||||
|
@ -893,6 +893,8 @@ impl AcpThreadView {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let agent_telemetry_id = self.agent.telemetry_id();
|
||||||
|
|
||||||
self.thread_error.take();
|
self.thread_error.take();
|
||||||
self.editing_message.take();
|
self.editing_message.take();
|
||||||
self.thread_feedback.clear();
|
self.thread_feedback.clear();
|
||||||
|
@ -937,6 +939,9 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
|
telemetry::event!("Agent Message Sent", agent = agent_telemetry_id);
|
||||||
|
|
||||||
thread.send(contents, cx)
|
thread.send(contents, cx)
|
||||||
})?;
|
})?;
|
||||||
send.await
|
send.await
|
||||||
|
@ -1164,6 +1169,10 @@ impl AcpThreadView {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AcpThreadEvent::PromptCapabilitiesUpdated => {
|
||||||
|
self.prompt_capabilities
|
||||||
|
.set(thread.read(cx).prompt_capabilities());
|
||||||
|
}
|
||||||
AcpThreadEvent::TokenUsageUpdated => {}
|
AcpThreadEvent::TokenUsageUpdated => {}
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -1243,30 +1252,44 @@ impl AcpThreadView {
|
||||||
pending_auth_method.replace(method.clone());
|
pending_auth_method.replace(method.clone());
|
||||||
let authenticate = connection.authenticate(method, cx);
|
let authenticate = connection.authenticate(method, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
self.auth_task = Some(cx.spawn_in(window, {
|
self.auth_task =
|
||||||
let project = self.project.clone();
|
Some(cx.spawn_in(window, {
|
||||||
let agent = self.agent.clone();
|
let project = self.project.clone();
|
||||||
async move |this, cx| {
|
let agent = self.agent.clone();
|
||||||
let result = authenticate.await;
|
async move |this, cx| {
|
||||||
|
let result = authenticate.await;
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
match &result {
|
||||||
if let Err(err) = result {
|
Ok(_) => telemetry::event!(
|
||||||
this.handle_thread_error(err, cx);
|
"Authenticate Agent Succeeded",
|
||||||
} else {
|
agent = agent.telemetry_id()
|
||||||
this.thread_state = Self::initial_state(
|
),
|
||||||
agent,
|
Err(_) => {
|
||||||
None,
|
telemetry::event!(
|
||||||
this.workspace.clone(),
|
"Authenticate Agent Failed",
|
||||||
project.clone(),
|
agent = agent.telemetry_id(),
|
||||||
window,
|
)
|
||||||
cx,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
this.auth_task.take()
|
|
||||||
})
|
this.update_in(cx, |this, window, cx| {
|
||||||
.ok();
|
if let Err(err) = result {
|
||||||
}
|
this.handle_thread_error(err, cx);
|
||||||
}));
|
} else {
|
||||||
|
this.thread_state = Self::initial_state(
|
||||||
|
agent,
|
||||||
|
None,
|
||||||
|
this.workspace.clone(),
|
||||||
|
project.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
this.auth_task.take()
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authorize_tool_call(
|
fn authorize_tool_call(
|
||||||
|
@ -2725,6 +2748,12 @@ impl AcpThreadView {
|
||||||
.on_click({
|
.on_click({
|
||||||
let method_id = method.id.clone();
|
let method_id = method.id.clone();
|
||||||
cx.listener(move |this, _, window, cx| {
|
cx.listener(move |this, _, window, cx| {
|
||||||
|
telemetry::event!(
|
||||||
|
"Authenticate Agent Started",
|
||||||
|
agent = this.agent.telemetry_id(),
|
||||||
|
method = method_id
|
||||||
|
);
|
||||||
|
|
||||||
this.authenticate(method_id.clone(), window, cx)
|
this.authenticate(method_id.clone(), window, cx)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -2753,6 +2782,8 @@ impl AcpThreadView {
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id());
|
||||||
|
|
||||||
let task = this
|
let task = this
|
||||||
.workspace
|
.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
|
@ -2760,7 +2791,7 @@ impl AcpThreadView {
|
||||||
let cwd = project.first_project_directory(cx);
|
let cwd = project.first_project_directory(cx);
|
||||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||||
let spawn_in_terminal = task::SpawnInTerminal {
|
let spawn_in_terminal = task::SpawnInTerminal {
|
||||||
id: task::TaskId("install".to_string()),
|
id: task::TaskId(install_command.clone()),
|
||||||
full_label: install_command.clone(),
|
full_label: install_command.clone(),
|
||||||
label: install_command.clone(),
|
label: install_command.clone(),
|
||||||
command: Some(install_command.clone()),
|
command: Some(install_command.clone()),
|
||||||
|
@ -2810,6 +2841,8 @@ impl AcpThreadView {
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id());
|
||||||
|
|
||||||
let task = this
|
let task = this
|
||||||
.workspace
|
.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
|
@ -2817,7 +2850,7 @@ impl AcpThreadView {
|
||||||
let cwd = project.first_project_directory(cx);
|
let cwd = project.first_project_directory(cx);
|
||||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||||
let spawn_in_terminal = task::SpawnInTerminal {
|
let spawn_in_terminal = task::SpawnInTerminal {
|
||||||
id: task::TaskId("upgrade".to_string()),
|
id: task::TaskId(upgrade_command.to_string()),
|
||||||
full_label: upgrade_command.clone(),
|
full_label: upgrade_command.clone(),
|
||||||
label: upgrade_command.clone(),
|
label: upgrade_command.clone(),
|
||||||
command: Some(upgrade_command.clone()),
|
command: Some(upgrade_command.clone()),
|
||||||
|
@ -3660,6 +3693,8 @@ impl AcpThreadView {
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
telemetry::event!("Follow Agent Selected", following = !following);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
@ -5287,6 +5322,10 @@ pub(crate) mod tests {
|
||||||
where
|
where
|
||||||
C: 'static + AgentConnection + Send + Clone,
|
C: 'static + AgentConnection + Send + Clone,
|
||||||
{
|
{
|
||||||
|
fn telemetry_id(&self) -> &'static str {
|
||||||
|
"test"
|
||||||
|
}
|
||||||
|
|
||||||
fn logo(&self) -> ui::IconName {
|
fn logo(&self) -> ui::IconName {
|
||||||
ui::IconName::Ai
|
ui::IconName::Ai
|
||||||
}
|
}
|
||||||
|
@ -5335,6 +5374,12 @@ pub(crate) mod tests {
|
||||||
project,
|
project,
|
||||||
action_log,
|
action_log,
|
||||||
SessionId("test".into()),
|
SessionId("test".into()),
|
||||||
|
watch::Receiver::constant(acp::PromptCapabilities {
|
||||||
|
image: true,
|
||||||
|
audio: true,
|
||||||
|
embedded_context: true,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
)
|
)
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
@ -5343,14 +5388,6 @@ pub(crate) mod tests {
|
||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
|
||||||
acp::PromptCapabilities {
|
|
||||||
image: true,
|
|
||||||
audio: true,
|
|
||||||
embedded_context: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn authenticate(
|
fn authenticate(
|
||||||
&self,
|
&self,
|
||||||
_method_id: acp::AuthMethodId,
|
_method_id: acp::AuthMethodId,
|
||||||
|
|
|
@ -5,6 +5,7 @@ mod tool_picker;
|
||||||
|
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||||
use cloud_llm_client::Plan;
|
use cloud_llm_client::Plan;
|
||||||
|
@ -15,7 +16,7 @@ use extension_host::ExtensionStore;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
|
||||||
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||||
};
|
};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
|
@ -23,10 +24,11 @@ use language_model::{
|
||||||
};
|
};
|
||||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||||
use project::{
|
use project::{
|
||||||
|
Project,
|
||||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||||
project_settings::{ContextServerSettings, ProjectSettings},
|
project_settings::{ContextServerSettings, ProjectSettings},
|
||||||
};
|
};
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, SettingsStore, update_settings_file};
|
||||||
use ui::{
|
use ui::{
|
||||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||||
|
@ -39,7 +41,7 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer,
|
AddContextServer, ExternalAgent, NewExternalAgentThread,
|
||||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,6 +49,7 @@ pub struct AgentConfiguration {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||||
context_server_store: Entity<ContextServerStore>,
|
context_server_store: Entity<ContextServerStore>,
|
||||||
|
@ -56,6 +59,8 @@ pub struct AgentConfiguration {
|
||||||
_registry_subscription: Subscription,
|
_registry_subscription: Subscription,
|
||||||
scroll_handle: ScrollHandle,
|
scroll_handle: ScrollHandle,
|
||||||
scrollbar_state: ScrollbarState,
|
scrollbar_state: ScrollbarState,
|
||||||
|
gemini_is_installed: bool,
|
||||||
|
_check_for_gemini: Task<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentConfiguration {
|
impl AgentConfiguration {
|
||||||
|
@ -65,6 +70,7 @@ impl AgentConfiguration {
|
||||||
tools: Entity<ToolWorkingSet>,
|
tools: Entity<ToolWorkingSet>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
project: WeakEntity<Project>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -89,6 +95,11 @@ impl AgentConfiguration {
|
||||||
|
|
||||||
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
||||||
.detach();
|
.detach();
|
||||||
|
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
|
||||||
|
this.check_for_gemini(cx);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
let scroll_handle = ScrollHandle::new();
|
let scroll_handle = ScrollHandle::new();
|
||||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||||
|
@ -97,6 +108,7 @@ impl AgentConfiguration {
|
||||||
fs,
|
fs,
|
||||||
language_registry,
|
language_registry,
|
||||||
workspace,
|
workspace,
|
||||||
|
project,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
configuration_views_by_provider: HashMap::default(),
|
configuration_views_by_provider: HashMap::default(),
|
||||||
context_server_store,
|
context_server_store,
|
||||||
|
@ -106,8 +118,11 @@ impl AgentConfiguration {
|
||||||
_registry_subscription: registry_subscription,
|
_registry_subscription: registry_subscription,
|
||||||
scroll_handle,
|
scroll_handle,
|
||||||
scrollbar_state,
|
scrollbar_state,
|
||||||
|
gemini_is_installed: false,
|
||||||
|
_check_for_gemini: Task::ready(()),
|
||||||
};
|
};
|
||||||
this.build_provider_configuration_views(window, cx);
|
this.build_provider_configuration_views(window, cx);
|
||||||
|
this.check_for_gemini(cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +152,34 @@ impl AgentConfiguration {
|
||||||
self.configuration_views_by_provider
|
self.configuration_views_by_provider
|
||||||
.insert(provider.id(), configuration_view);
|
.insert(provider.id(), configuration_view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let project = self.project.clone();
|
||||||
|
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||||
|
self._check_for_gemini = cx.spawn({
|
||||||
|
async move |this, cx| {
|
||||||
|
let Some(project) = project.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let gemini_is_installed = AgentServerCommand::resolve(
|
||||||
|
Gemini::binary_name(),
|
||||||
|
&[],
|
||||||
|
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
|
||||||
|
None,
|
||||||
|
settings.gemini,
|
||||||
|
&project,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_some();
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.gemini_is_installed = gemini_is_installed;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for AgentConfiguration {
|
impl Focusable for AgentConfiguration {
|
||||||
|
@ -211,7 +254,6 @@ impl AgentConfiguration {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(provider_id_string.clone())
|
.id(provider_id_string.clone())
|
||||||
.cursor_pointer()
|
|
||||||
.px_2()
|
.px_2()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
@ -231,10 +273,7 @@ impl AgentConfiguration {
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(Label::new(provider_name.clone()))
|
||||||
Label::new(provider_name.clone())
|
|
||||||
.size(LabelSize::Large),
|
|
||||||
)
|
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if is_zed_provider && is_signed_in {
|
if is_zed_provider && is_signed_in {
|
||||||
this.child(
|
this.child(
|
||||||
|
@ -279,7 +318,7 @@ impl AgentConfiguration {
|
||||||
"Start New Thread",
|
"Start New Thread",
|
||||||
)
|
)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Thread)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
|
@ -378,7 +417,7 @@ impl AgentConfiguration {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Label::new("Add at least one provider to use AI-powered features.")
|
Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -519,6 +558,14 @@ impl AgentConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||||
|
cx.theme().colors().background.opacity(0.25)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
|
||||||
|
cx.theme().colors().border.opacity(0.6)
|
||||||
|
}
|
||||||
|
|
||||||
fn render_context_servers_section(
|
fn render_context_servers_section(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
@ -536,7 +583,12 @@ impl AgentConfiguration {
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||||
.child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)),
|
.child(
|
||||||
|
Label::new(
|
||||||
|
"All context servers connected through the Model Context Protocol.",
|
||||||
|
)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.children(
|
.children(
|
||||||
context_server_ids.into_iter().map(|context_server_id| {
|
context_server_ids.into_iter().map(|context_server_id| {
|
||||||
|
@ -546,7 +598,7 @@ impl AgentConfiguration {
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.gap_2()
|
.gap_1p5()
|
||||||
.child(
|
.child(
|
||||||
h_flex().w_full().child(
|
h_flex().w_full().child(
|
||||||
Button::new("add-context-server", "Add Custom Server")
|
Button::new("add-context-server", "Add Custom Server")
|
||||||
|
@ -637,8 +689,6 @@ impl AgentConfiguration {
|
||||||
.map_or([].as_slice(), |tools| tools.as_slice());
|
.map_or([].as_slice(), |tools| tools.as_slice());
|
||||||
let tool_count = tools.len();
|
let tool_count = tools.len();
|
||||||
|
|
||||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
|
||||||
|
|
||||||
let (source_icon, source_tooltip) = if is_from_extension {
|
let (source_icon, source_tooltip) = if is_from_extension {
|
||||||
(
|
(
|
||||||
IconName::ZedMcpExtension,
|
IconName::ZedMcpExtension,
|
||||||
|
@ -781,8 +831,8 @@ impl AgentConfiguration {
|
||||||
.id(item_id.clone())
|
.id(item_id.clone())
|
||||||
.border_1()
|
.border_1()
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.border_color(border_color)
|
.border_color(self.card_item_border_color(cx))
|
||||||
.bg(cx.theme().colors().background.opacity(0.2))
|
.bg(self.card_item_bg_color(cx))
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -790,7 +840,11 @@ impl AgentConfiguration {
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.when(
|
.when(
|
||||||
error.is_some() || are_tools_expanded && tool_count >= 1,
|
error.is_some() || are_tools_expanded && tool_count >= 1,
|
||||||
|element| element.border_b_1().border_color(border_color),
|
|element| {
|
||||||
|
element
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(self.card_item_border_color(cx))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -972,6 +1026,166 @@ impl AgentConfiguration {
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||||
|
let user_defined_agents = settings
|
||||||
|
.custom
|
||||||
|
.iter()
|
||||||
|
.map(|(name, settings)| {
|
||||||
|
self.render_agent_server(
|
||||||
|
IconName::Ai,
|
||||||
|
name.clone(),
|
||||||
|
ExternalAgent::Custom {
|
||||||
|
name: name.clone(),
|
||||||
|
settings: settings.clone(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.p(DynamicSpacing::Base16.rems(cx))
|
||||||
|
.pr(DynamicSpacing::Base20.rems(cx))
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(Headline::new("External Agents"))
|
||||||
|
.child(
|
||||||
|
Label::new(
|
||||||
|
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
|
||||||
|
)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(self.render_agent_server(
|
||||||
|
IconName::AiGemini,
|
||||||
|
"Gemini CLI",
|
||||||
|
ExternalAgent::Gemini,
|
||||||
|
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
// TODO add CC
|
||||||
|
.children(user_defined_agents),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_agent_server(
|
||||||
|
&self,
|
||||||
|
icon: IconName,
|
||||||
|
name: impl Into<SharedString>,
|
||||||
|
agent: ExternalAgent,
|
||||||
|
install_command: Option<SharedString>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let name = name.into();
|
||||||
|
h_flex()
|
||||||
|
.p_1()
|
||||||
|
.pl_2()
|
||||||
|
.gap_1p5()
|
||||||
|
.justify_between()
|
||||||
|
.border_1()
|
||||||
|
.rounded_md()
|
||||||
|
.border_color(self.card_item_border_color(cx))
|
||||||
|
.bg(self.card_item_bg_color(cx))
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||||
|
.child(Label::new(name.clone())),
|
||||||
|
)
|
||||||
|
.map(|this| {
|
||||||
|
if let Some(install_command) = install_command {
|
||||||
|
this.child(
|
||||||
|
Button::new(
|
||||||
|
SharedString::from(format!("install_external_agent-{name}")),
|
||||||
|
"Install Agent",
|
||||||
|
)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.tooltip(Tooltip::text(install_command.clone()))
|
||||||
|
.on_click(cx.listener(
|
||||||
|
move |this, _, window, cx| {
|
||||||
|
let Some(project) = this.project.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(workspace) = this.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let cwd = project.read(cx).first_project_directory(cx);
|
||||||
|
let shell =
|
||||||
|
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||||
|
let spawn_in_terminal = task::SpawnInTerminal {
|
||||||
|
id: task::TaskId(install_command.to_string()),
|
||||||
|
full_label: install_command.to_string(),
|
||||||
|
label: install_command.to_string(),
|
||||||
|
command: Some(install_command.to_string()),
|
||||||
|
args: Vec::new(),
|
||||||
|
command_label: install_command.to_string(),
|
||||||
|
cwd,
|
||||||
|
env: Default::default(),
|
||||||
|
use_new_terminal: true,
|
||||||
|
allow_concurrent_runs: true,
|
||||||
|
reveal: Default::default(),
|
||||||
|
reveal_target: Default::default(),
|
||||||
|
hide: Default::default(),
|
||||||
|
shell,
|
||||||
|
show_summary: true,
|
||||||
|
show_command: true,
|
||||||
|
show_rerun: false,
|
||||||
|
};
|
||||||
|
let task = workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
||||||
|
});
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
task.await;
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.check_for_gemini(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.child(
|
||||||
|
h_flex().gap_1().child(
|
||||||
|
Button::new(
|
||||||
|
SharedString::from(format!("start_acp_thread-{name}")),
|
||||||
|
"Start New Thread",
|
||||||
|
)
|
||||||
|
.label_size(LabelSize::Small)
|
||||||
|
.icon(IconName::Thread)
|
||||||
|
.icon_position(IconPosition::Start)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
window.dispatch_action(
|
||||||
|
NewExternalAgentThread {
|
||||||
|
agent: Some(agent.clone()),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for AgentConfiguration {
|
impl Render for AgentConfiguration {
|
||||||
|
@ -991,6 +1205,7 @@ impl Render for AgentConfiguration {
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.child(self.render_general_settings_section(cx))
|
.child(self.render_general_settings_section(cx))
|
||||||
|
.child(self.render_agent_servers_section(cx))
|
||||||
.child(self.render_context_servers_section(window, cx))
|
.child(self.render_context_servers_section(window, cx))
|
||||||
.child(self.render_provider_configuration_section(cx)),
|
.child(self.render_provider_configuration_section(cx)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1529,6 +1529,7 @@ impl AgentDiff {
|
||||||
| AcpThreadEvent::TokenUsageUpdated
|
| AcpThreadEvent::TokenUsageUpdated
|
||||||
| AcpThreadEvent::EntriesRemoved(_)
|
| AcpThreadEvent::EntriesRemoved(_)
|
||||||
| AcpThreadEvent::ToolAuthorizationRequired
|
| AcpThreadEvent::ToolAuthorizationRequired
|
||||||
|
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||||
| AcpThreadEvent::Retry(_) => {}
|
| AcpThreadEvent::Retry(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,6 +241,7 @@ enum WhichFontSize {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO unify this with ExternalAgent
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum AgentType {
|
pub enum AgentType {
|
||||||
#[default]
|
#[default]
|
||||||
|
@ -1025,6 +1026,8 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
telemetry::event!("Agent Thread Started", agent = "zed-text");
|
||||||
|
|
||||||
let context = self
|
let context = self
|
||||||
.context_store
|
.context_store
|
||||||
.update(cx, |context_store, cx| context_store.create(cx));
|
.update(cx, |context_store, cx| context_store.create(cx));
|
||||||
|
@ -1117,6 +1120,8 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
|
||||||
|
|
||||||
let server = ext_agent.server(fs, history);
|
let server = ext_agent.server(fs, history);
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
@ -1474,6 +1479,7 @@ impl AgentPanel {
|
||||||
tools,
|
tools,
|
||||||
self.language_registry.clone(),
|
self.language_registry.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
|
self.project.downgrade(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -2325,6 +2331,8 @@ impl AgentPanel {
|
||||||
.menu({
|
.menu({
|
||||||
let menu = self.assistant_navigation_menu.clone();
|
let menu = self.assistant_navigation_menu.clone();
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
|
telemetry::event!("View Thread History Clicked");
|
||||||
|
|
||||||
if let Some(menu) = menu.as_ref() {
|
if let Some(menu) = menu.as_ref() {
|
||||||
menu.update(cx, |_, cx| {
|
menu.update(cx, |_, cx| {
|
||||||
cx.defer_in(window, |menu, window, cx| {
|
cx.defer_in(window, |menu, window, cx| {
|
||||||
|
@ -2503,6 +2511,8 @@ impl AgentPanel {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
|
||||||
move |window, cx| {
|
move |window, cx| {
|
||||||
|
telemetry::event!("New Thread Clicked");
|
||||||
|
|
||||||
let active_thread = active_thread.clone();
|
let active_thread = active_thread.clone();
|
||||||
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||||
menu = menu
|
menu = menu
|
||||||
|
|
|
@ -160,6 +160,7 @@ pub struct NewNativeAgentThreadFromSummary {
|
||||||
from_session_id: agent_client_protocol::SessionId,
|
from_session_id: agent_client_protocol::SessionId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO unify this with AgentType
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
enum ExternalAgent {
|
enum ExternalAgent {
|
||||||
|
@ -174,6 +175,15 @@ enum ExternalAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExternalAgent {
|
impl ExternalAgent {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::NativeAgent => "zed",
|
||||||
|
Self::Gemini => "gemini-cli",
|
||||||
|
Self::ClaudeCode => "claude-code",
|
||||||
|
Self::Custom { .. } => "custom",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn server(
|
pub fn server(
|
||||||
&self,
|
&self,
|
||||||
fs: Arc<dyn fs::Fs>,
|
fs: Arc<dyn fs::Fs>,
|
||||||
|
|
|
@ -361,6 +361,7 @@ impl TextThreadEditor {
|
||||||
if self.sending_disabled(cx) {
|
if self.sending_disabled(cx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
telemetry::event!("Agent Message Sent", agent = "zed-text");
|
||||||
self.send_to_model(window, cx);
|
self.send_to_model(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}
|
||||||
|
|
||||||
#[derive(IntoElement, RegisterComponent)]
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
pub struct AiUpsellCard {
|
pub struct AiUpsellCard {
|
||||||
pub sign_in_status: SignInStatus,
|
sign_in_status: SignInStatus,
|
||||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||||
pub account_too_young: bool,
|
account_too_young: bool,
|
||||||
pub user_plan: Option<Plan>,
|
user_plan: Option<Plan>,
|
||||||
pub tab_index: Option<isize>,
|
tab_index: Option<isize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AiUpsellCard {
|
impl AiUpsellCard {
|
||||||
|
@ -43,6 +43,11 @@ impl AiUpsellCard {
|
||||||
tab_index: None,
|
tab_index: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
|
||||||
|
self.tab_index = tab_index;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for AiUpsellCard {
|
impl RenderOnce for AiUpsellCard {
|
||||||
|
|
|
@ -283,17 +283,13 @@ pub(crate) fn render_ai_setup_page(
|
||||||
v_flex()
|
v_flex()
|
||||||
.mt_2()
|
.mt_2()
|
||||||
.gap_6()
|
.gap_6()
|
||||||
.child({
|
.child(
|
||||||
let mut ai_upsell_card =
|
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
|
||||||
AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx);
|
.tab_index(Some({
|
||||||
|
tab_index += 1;
|
||||||
ai_upsell_card.tab_index = Some({
|
tab_index - 1
|
||||||
tab_index += 1;
|
})),
|
||||||
tab_index - 1
|
)
|
||||||
});
|
|
||||||
|
|
||||||
ai_upsell_card
|
|
||||||
})
|
|
||||||
.child(render_llm_provider_section(
|
.child(render_llm_provider_section(
|
||||||
&mut tab_index,
|
&mut tab_index,
|
||||||
workspace,
|
workspace,
|
||||||
|
|
|
@ -162,6 +162,19 @@ impl<T> Receiver<T> {
|
||||||
pending_waker_id: None,
|
pending_waker_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`Receiver`] holding an initial value that will never change.
|
||||||
|
pub fn constant(value: T) -> Self {
|
||||||
|
let state = Arc::new(RwLock::new(State {
|
||||||
|
value,
|
||||||
|
wakers: BTreeMap::new(),
|
||||||
|
next_waker_id: WakerId::default(),
|
||||||
|
version: 0,
|
||||||
|
closed: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Self { state, version: 0 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone> Receiver<T> {
|
impl<T: Clone> Receiver<T> {
|
||||||
|
|
|
@ -6622,15 +6622,25 @@ impl Render for Workspace {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.children(self.zoomed.as_ref().and_then(|view| {
|
.children(self.zoomed.as_ref().and_then(|view| {
|
||||||
Some(div()
|
let zoomed_view = view.upgrade()?;
|
||||||
|
let div = div()
|
||||||
.occlude()
|
.occlude()
|
||||||
.absolute()
|
.absolute()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.border_color(colors.border)
|
.border_color(colors.border)
|
||||||
.bg(colors.background)
|
.bg(colors.background)
|
||||||
.child(view.upgrade()?)
|
.child(zoomed_view)
|
||||||
.inset_0()
|
.inset_0()
|
||||||
.shadow_lg())
|
.shadow_lg();
|
||||||
|
|
||||||
|
Some(match self.zoomed_position {
|
||||||
|
Some(DockPosition::Left) => div.right_2().border_r_1(),
|
||||||
|
Some(DockPosition::Right) => div.left_2().border_l_1(),
|
||||||
|
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
|
||||||
|
None => {
|
||||||
|
div.top_2().bottom_2().left_2().right_2().border_1()
|
||||||
|
}
|
||||||
|
})
|
||||||
}))
|
}))
|
||||||
.children(self.render_notifications(window, cx)),
|
.children(self.render_notifications(window, cx)),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue