Merge branch 'main' into thread-view-papercuts

This commit is contained in:
Ben Brandt 2025-08-25 11:55:57 -07:00
commit 55b3f3720e
No known key found for this signature in database
GPG key ID: D4618C5D3B500571
22 changed files with 477 additions and 116 deletions

View file

@ -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", "..."],

View file

@ -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();

View file

@ -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,

View file

@ -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,

View file

@ -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()
} }

View file

@ -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()
} }

View file

@ -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;

View file

@ -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>,

View file

@ -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 {

View file

@ -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()
} }

View file

@ -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::*;

View file

@ -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

View file

@ -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,

View file

@ -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)),
) )

View file

@ -1529,6 +1529,7 @@ impl AgentDiff {
| AcpThreadEvent::TokenUsageUpdated | AcpThreadEvent::TokenUsageUpdated
| AcpThreadEvent::EntriesRemoved(_) | AcpThreadEvent::EntriesRemoved(_)
| AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::Retry(_) => {} | AcpThreadEvent::Retry(_) => {}
} }
} }

View file

@ -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

View file

@ -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>,

View file

@ -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);
} }

View file

@ -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 {

View file

@ -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,

View file

@ -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> {

View file

@ -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)),
) )