diff --git a/Cargo.lock b/Cargo.lock
index 4f2749c913..e1f06fdd85 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13538,6 +13538,7 @@ dependencies = [
"smol",
"sysinfo",
"telemetry_events",
+ "thiserror 2.0.12",
"toml 0.8.20",
"unindent",
"util",
diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg
index bca13f8d56..aba193930b 100644
--- a/assets/icons/copy.svg
+++ b/assets/icons/copy.svg
@@ -1 +1,4 @@
-
+
diff --git a/assets/settings/default.json b/assets/settings/default.json
index ac26952c7f..f0b9e11e57 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -162,6 +162,12 @@
// 2. Always quit the application
// "on_last_window_closed": "quit_app",
"on_last_window_closed": "platform_default",
+ // Whether to show padding for zoomed panels.
+ // When enabled, zoomed center panels (e.g. code editor) will have padding all around,
+ // while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
+ //
+ // Default: true
+ "zoomed_padding": true,
// Whether to use the system provided dialogs for Open and Save As.
// When set to false, Zed will use the built-in keyboard-first pickers.
"use_system_path_prompts": true,
@@ -1629,6 +1635,9 @@
"allowed": true
}
},
+ "Kotlin": {
+ "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
+ },
"LaTeX": {
"formatter": "language_server",
"language_servers": ["texlab", "..."],
diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs
index 029d175054..4ded647a74 100644
--- a/crates/acp_thread/src/acp_thread.rs
+++ b/crates/acp_thread/src/acp_thread.rs
@@ -183,16 +183,15 @@ impl ToolCall {
language_registry: Arc,
cx: &mut App,
) -> Self {
+ let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
+ first_line.to_owned() + "…"
+ } else {
+ tool_call.title
+ };
Self {
id: tool_call.id,
- label: cx.new(|cx| {
- Markdown::new(
- tool_call.title.into(),
- Some(language_registry.clone()),
- None,
- cx,
- )
- }),
+ label: cx
+ .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
kind: tool_call.kind,
content: tool_call
.content
@@ -233,7 +232,11 @@ impl ToolCall {
if let Some(title) = title {
self.label.update(cx, |label, cx| {
- label.replace(title, cx);
+ if let Some((first_line, _)) = title.split_once("\n") {
+ label.replace(first_line.to_owned() + "…", cx)
+ } else {
+ label.replace(title, cx);
+ }
});
}
@@ -756,6 +759,8 @@ pub struct AcpThread {
connection: Rc,
session_id: acp::SessionId,
token_usage: Option,
+ prompt_capabilities: acp::PromptCapabilities,
+ _observe_prompt_capabilities: Task>,
}
#[derive(Debug)]
@@ -770,11 +775,12 @@ pub enum AcpThreadEvent {
Stopped,
Error,
LoadError(LoadError),
+ PromptCapabilitiesUpdated,
}
impl EventEmitter for AcpThread {}
-#[derive(PartialEq, Eq)]
+#[derive(PartialEq, Eq, Debug)]
pub enum ThreadStatus {
Idle,
WaitingForToolConfirmation,
@@ -821,7 +827,20 @@ impl AcpThread {
project: Entity,
action_log: Entity,
session_id: acp::SessionId,
+ mut prompt_capabilities_rx: watch::Receiver,
+ cx: &mut Context,
) -> 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 {
action_log,
shared_buffers: Default::default(),
@@ -833,9 +852,15 @@ impl AcpThread {
connection,
session_id,
token_usage: None,
+ prompt_capabilities,
+ _observe_prompt_capabilities: task,
}
}
+ pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+ self.prompt_capabilities
+ }
+
pub fn connection(&self) -> &Rc {
&self.connection
}
@@ -1373,6 +1398,10 @@ impl AcpThread {
})
}
+ pub fn can_resume(&self, cx: &App) -> bool {
+ self.connection.resume(&self.session_id, cx).is_some()
+ }
+
pub fn resume(&mut self, cx: &mut Context) -> BoxFuture<'static, Result<()>> {
self.run_turn(cx, async move |this, cx| {
this.update(cx, |this, cx| {
@@ -2595,13 +2624,19 @@ mod tests {
.into(),
);
let action_log = cx.new(|_| ActionLog::new(project.clone()));
- let thread = cx.new(|_cx| {
+ let thread = cx.new(|cx| {
AcpThread::new(
"Test",
self.clone(),
project,
action_log,
session_id.clone(),
+ watch::Receiver::constant(acp::PromptCapabilities {
+ image: true,
+ audio: true,
+ embedded_context: true,
+ }),
+ cx,
)
});
self.sessions.lock().insert(session_id, thread.downgrade());
@@ -2635,14 +2670,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) {
let sessions = self.sessions.lock();
let thread = sessions.get(session_id).unwrap().clone();
@@ -2659,7 +2686,7 @@ mod tests {
fn truncate(
&self,
session_id: &acp::SessionId,
- _cx: &mut App,
+ _cx: &App,
) -> Option> {
Some(Rc::new(FakeAgentSessionEditor {
_session_id: session_id.clone(),
diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs
index 91e46dbac1..af229b7545 100644
--- a/crates/acp_thread/src/connection.rs
+++ b/crates/acp_thread/src/connection.rs
@@ -38,12 +38,10 @@ pub trait AgentConnection {
cx: &mut App,
) -> Task>;
- fn prompt_capabilities(&self) -> acp::PromptCapabilities;
-
fn resume(
&self,
_session_id: &acp::SessionId,
- _cx: &mut App,
+ _cx: &App,
) -> Option> {
None
}
@@ -53,7 +51,7 @@ pub trait AgentConnection {
fn truncate(
&self,
_session_id: &acp::SessionId,
- _cx: &mut App,
+ _cx: &App,
) -> Option> {
None
}
@@ -61,7 +59,7 @@ pub trait AgentConnection {
fn set_title(
&self,
_session_id: &acp::SessionId,
- _cx: &mut App,
+ _cx: &App,
) -> Option> {
None
}
@@ -329,13 +327,19 @@ mod test_support {
) -> Task>> {
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
- let thread = cx.new(|_cx| {
+ let thread = cx.new(|cx| {
AcpThread::new(
"Test",
self.clone(),
project,
action_log,
session_id.clone(),
+ watch::Receiver::constant(acp::PromptCapabilities {
+ image: true,
+ audio: true,
+ embedded_context: true,
+ }),
+ cx,
)
});
self.sessions.lock().insert(
@@ -348,14 +352,6 @@ mod test_support {
Task::ready(Ok(thread))
}
- fn prompt_capabilities(&self) -> acp::PromptCapabilities {
- acp::PromptCapabilities {
- image: true,
- audio: true,
- embedded_context: true,
- }
- }
-
fn authenticate(
&self,
_method_id: acp::AuthMethodId,
@@ -439,7 +435,7 @@ mod test_support {
fn truncate(
&self,
_session_id: &agent_client_protocol::SessionId,
- _cx: &mut App,
+ _cx: &App,
) -> Option> {
Some(Rc::new(StubAgentSessionEditor))
}
diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs
index ee12b04cde..e20a040e9d 100644
--- a/crates/acp_tools/src/acp_tools.rs
+++ b/crates/acp_tools/src/acp_tools.rs
@@ -21,12 +21,12 @@ use ui::prelude::*;
use util::ResultExt as _;
use workspace::{Item, Workspace};
-actions!(acp, [OpenDebugTools]);
+actions!(dev, [OpenAcpLogs]);
pub fn init(cx: &mut App) {
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context| {
- workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| {
+ workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
let acp_tools =
Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs
index 4eaf87e218..ecfaea4b49 100644
--- a/crates/agent2/src/agent.rs
+++ b/crates/agent2/src/agent.rs
@@ -180,7 +180,7 @@ impl NativeAgent {
fs: Arc,
cx: &mut AsyncApp,
) -> Result> {
- log::info!("Creating new NativeAgent");
+ log::debug!("Creating new NativeAgent");
let project_context = cx
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
@@ -240,13 +240,16 @@ impl NativeAgent {
let title = thread.title();
let project = thread.project.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(
title,
connection,
project.clone(),
action_log.clone(),
session_id.clone(),
+ prompt_capabilities_rx,
+ cx,
)
});
let subscriptions = vec![
@@ -756,7 +759,7 @@ impl NativeAgentConnection {
}
}
- log::info!("Response stream completed");
+ log::debug!("Response stream completed");
anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
})
@@ -781,7 +784,7 @@ impl AgentModelSelector for NativeAgentConnection {
model_id: acp_thread::AgentModelId,
cx: &mut App,
) -> Task> {
- log::info!("Setting model for session {}: {}", session_id, model_id);
+ log::debug!("Setting model for session {}: {}", session_id, model_id);
let Some(thread) = self
.0
.read(cx)
@@ -852,7 +855,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
cx: &mut App,
) -> Task>> {
let agent = self.0.clone();
- log::info!("Creating new thread for project at: {:?}", cwd);
+ log::debug!("Creating new thread for project at: {:?}", cwd);
cx.spawn(async move |cx| {
log::debug!("Starting thread creation in async context");
@@ -917,7 +920,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
.into_iter()
.map(Into::into)
.collect::>();
- log::info!("Converted prompt to message: {} chars", content.len());
+ log::debug!("Converted prompt to message: {} chars", content.len());
log::debug!("Message id: {:?}", id);
log::debug!("Message content: {:?}", content);
@@ -925,18 +928,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
})
}
- fn prompt_capabilities(&self) -> acp::PromptCapabilities {
- acp::PromptCapabilities {
- image: true,
- audio: false,
- embedded_context: true,
- }
- }
-
fn resume(
&self,
session_id: &acp::SessionId,
- _cx: &mut App,
+ _cx: &App,
) -> Option> {
Some(Rc::new(NativeAgentSessionResume {
connection: self.clone(),
@@ -956,9 +951,9 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
fn truncate(
&self,
session_id: &agent_client_protocol::SessionId,
- cx: &mut App,
+ cx: &App,
) -> Option> {
- self.0.update(cx, |agent, _cx| {
+ self.0.read_with(cx, |agent, _cx| {
agent.sessions.get(session_id).map(|session| {
Rc::new(NativeAgentSessionEditor {
thread: session.thread.clone(),
@@ -971,7 +966,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
fn set_title(
&self,
session_id: &acp::SessionId,
- _cx: &mut App,
+ _cx: &App,
) -> Option> {
Some(Rc::new(NativeAgentSessionSetTitle {
connection: self.clone(),
diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent2/src/native_agent_server.rs
index 12d3c79d1b..9ff98ccd18 100644
--- a/crates/agent2/src/native_agent_server.rs
+++ b/crates/agent2/src/native_agent_server.rs
@@ -22,6 +22,10 @@ impl NativeAgentServer {
}
impl AgentServer for NativeAgentServer {
+ fn telemetry_id(&self) -> &'static str {
+ "zed"
+ }
+
fn name(&self) -> SharedString {
"Zed Agent".into()
}
@@ -44,7 +48,7 @@ impl AgentServer for NativeAgentServer {
project: &Entity,
cx: &mut App,
) -> Task>> {
- log::info!(
+ log::debug!(
"NativeAgentServer::connect called for path: {:?}",
_root_dir
);
@@ -63,7 +67,7 @@ impl AgentServer for NativeAgentServer {
// Create the connection wrapper
let connection = NativeAgentConnection(agent);
- log::info!("NativeAgentServer connection established successfully");
+ log::debug!("NativeAgentServer connection established successfully");
Ok(Rc::new(connection) as Rc)
})
diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs
index 60b3198081..864fbf8b10 100644
--- a/crates/agent2/src/tests/mod.rs
+++ b/crates/agent2/src/tests/mod.rs
@@ -4,6 +4,8 @@ use agent_client_protocol::{self as acp};
use agent_settings::AgentProfileId;
use anyhow::Result;
use client::{Client, UserStore};
+use cloud_llm_client::CompletionIntent;
+use collections::IndexMap;
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
use fs::{FakeFs, Fs};
use futures::{
@@ -672,15 +674,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
"}
)
});
-
- // Ensure we error if calling resume when tool use limit was *not* reached.
- let error = thread
- .update(cx, |thread, cx| thread.resume(cx))
- .unwrap_err();
- assert_eq!(
- error.to_string(),
- "can only resume after tool use limit is reached"
- )
}
#[gpui::test]
@@ -1692,6 +1685,7 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
}
#[gpui::test]
+#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_title_generation(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
@@ -1737,6 +1731,81 @@ async fn test_title_generation(cx: &mut TestAppContext) {
thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world"));
}
+#[gpui::test]
+async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
+ let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+ let fake_model = model.as_fake();
+
+ let _events = thread
+ .update(cx, |thread, cx| {
+ thread.add_tool(ToolRequiringPermission);
+ thread.add_tool(EchoTool);
+ thread.send(UserMessageId::new(), ["Hey!"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ let permission_tool_use = LanguageModelToolUse {
+ id: "tool_id_1".into(),
+ name: ToolRequiringPermission::name().into(),
+ raw_input: "{}".into(),
+ input: json!({}),
+ is_input_complete: true,
+ };
+ let echo_tool_use = LanguageModelToolUse {
+ id: "tool_id_2".into(),
+ name: EchoTool::name().into(),
+ raw_input: json!({"text": "test"}).to_string(),
+ input: json!({"text": "test"}),
+ is_input_complete: true,
+ };
+ fake_model.send_last_completion_stream_text_chunk("Hi!");
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
+ permission_tool_use,
+ ));
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
+ echo_tool_use.clone(),
+ ));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // Ensure pending tools are skipped when building a request.
+ let request = thread
+ .read_with(cx, |thread, cx| {
+ thread.build_completion_request(CompletionIntent::EditFile, cx)
+ })
+ .unwrap();
+ assert_eq!(
+ request.messages[1..],
+ vec![
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Hey!".into()],
+ cache: true
+ },
+ LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: vec![
+ MessageContent::Text("Hi!".into()),
+ MessageContent::ToolUse(echo_tool_use.clone())
+ ],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec![MessageContent::ToolResult(LanguageModelToolResult {
+ tool_use_id: echo_tool_use.id.clone(),
+ tool_name: echo_tool_use.name,
+ is_error: false,
+ content: "test".into(),
+ output: Some("test".into())
+ })],
+ cache: false
+ },
+ ],
+ );
+}
+
#[gpui::test]
async fn test_agent_connection(cx: &mut TestAppContext) {
cx.update(settings::init);
@@ -2029,6 +2098,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
.unwrap();
cx.run_until_parked();
+ fake_model.send_last_completion_stream_text_chunk("Hey,");
fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
provider: LanguageModelProviderName::new("Anthropic"),
retry_after: Some(Duration::from_secs(3)),
@@ -2038,8 +2108,9 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
cx.executor().advance_clock(Duration::from_secs(3));
cx.run_until_parked();
- fake_model.send_last_completion_stream_text_chunk("Hey!");
+ fake_model.send_last_completion_stream_text_chunk("there!");
fake_model.end_last_completion_stream();
+ cx.run_until_parked();
let mut retry_events = Vec::new();
while let Some(Ok(event)) = events.next().await {
@@ -2067,12 +2138,94 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
## Assistant
- Hey!
+ Hey,
+
+ [resume]
+
+ ## Assistant
+
+ there!
"}
)
});
}
+#[gpui::test]
+async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
+ let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
+ let fake_model = model.as_fake();
+
+ let events = thread
+ .update(cx, |thread, cx| {
+ thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
+ thread.add_tool(EchoTool);
+ thread.send(UserMessageId::new(), ["Call the echo tool!"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ let tool_use_1 = LanguageModelToolUse {
+ id: "tool_1".into(),
+ name: EchoTool::name().into(),
+ raw_input: json!({"text": "test"}).to_string(),
+ input: json!({"text": "test"}),
+ is_input_complete: true,
+ };
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
+ tool_use_1.clone(),
+ ));
+ fake_model.send_last_completion_stream_error(LanguageModelCompletionError::ServerOverloaded {
+ provider: LanguageModelProviderName::new("Anthropic"),
+ retry_after: Some(Duration::from_secs(3)),
+ });
+ fake_model.end_last_completion_stream();
+
+ cx.executor().advance_clock(Duration::from_secs(3));
+ let completion = fake_model.pending_completions().pop().unwrap();
+ assert_eq!(
+ completion.messages[1..],
+ vec![
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Call the echo tool!".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec![language_model::MessageContent::ToolResult(
+ LanguageModelToolResult {
+ tool_use_id: tool_use_1.id.clone(),
+ tool_name: tool_use_1.name.clone(),
+ is_error: false,
+ content: "test".into(),
+ output: Some("test".into())
+ }
+ )],
+ cache: true
+ },
+ ]
+ );
+
+ fake_model.send_last_completion_stream_text_chunk("Done");
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+ events.collect::>().await;
+ thread.read_with(cx, |thread, _cx| {
+ assert_eq!(
+ thread.last_message(),
+ Some(Message::Agent(AgentMessage {
+ content: vec![AgentMessageContent::Text("Done".into())],
+ tool_results: IndexMap::default()
+ }))
+ );
+ })
+}
+
#[gpui::test]
async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await;
diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs
index 6d616f73fc..1b1c014b79 100644
--- a/crates/agent2/src/thread.rs
+++ b/crates/agent2/src/thread.rs
@@ -123,7 +123,7 @@ impl Message {
match self {
Message::User(message) => message.to_markdown(),
Message::Agent(message) => message.to_markdown(),
- Message::Resume => "[resumed after tool use limit was reached]".into(),
+ Message::Resume => "[resume]\n".into(),
}
}
@@ -448,24 +448,33 @@ impl AgentMessage {
cache: false,
};
for chunk in &self.content {
- let chunk = match chunk {
+ match chunk {
AgentMessageContent::Text(text) => {
- language_model::MessageContent::Text(text.clone())
+ assistant_message
+ .content
+ .push(language_model::MessageContent::Text(text.clone()));
}
AgentMessageContent::Thinking { text, signature } => {
- language_model::MessageContent::Thinking {
- text: text.clone(),
- signature: signature.clone(),
- }
+ assistant_message
+ .content
+ .push(language_model::MessageContent::Thinking {
+ text: text.clone(),
+ signature: signature.clone(),
+ });
}
AgentMessageContent::RedactedThinking(value) => {
- language_model::MessageContent::RedactedThinking(value.clone())
+ assistant_message.content.push(
+ language_model::MessageContent::RedactedThinking(value.clone()),
+ );
}
- AgentMessageContent::ToolUse(value) => {
- language_model::MessageContent::ToolUse(value.clone())
+ AgentMessageContent::ToolUse(tool_use) => {
+ if self.tool_results.contains_key(&tool_use.id) {
+ assistant_message
+ .content
+ .push(language_model::MessageContent::ToolUse(tool_use.clone()));
+ }
}
};
- assistant_message.content.push(chunk);
}
let mut user_message = LanguageModelRequestMessage {
@@ -566,11 +575,22 @@ pub struct Thread {
templates: Arc,
model: Option>,
summarization_model: Option>,
+ prompt_capabilities_tx: watch::Sender,
+ pub(crate) prompt_capabilities_rx: watch::Receiver,
pub(crate) project: Entity,
pub(crate) action_log: Entity,
}
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(
project: Entity,
project_context: Entity,
@@ -581,6 +601,8 @@ impl Thread {
) -> Self {
let profile_id = AgentSettings::get_global(cx).default_profile.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 {
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
prompt_id: PromptId::new(),
@@ -608,6 +630,8 @@ impl Thread {
templates,
model,
summarization_model: None,
+ prompt_capabilities_tx,
+ prompt_capabilities_rx,
project,
action_log,
}
@@ -741,6 +765,8 @@ impl Thread {
.or_else(|| registry.default_model())
.map(|model| model.model)
});
+ let (prompt_capabilities_tx, prompt_capabilities_rx) =
+ watch::channel(Self::prompt_capabilities(model.as_deref()));
Self {
id,
@@ -770,6 +796,8 @@ impl Thread {
project,
action_log,
updated_at: db_thread.updated_at,
+ prompt_capabilities_tx,
+ prompt_capabilities_rx,
}
}
@@ -937,10 +965,12 @@ impl Thread {
pub fn set_model(&mut self, model: Arc, cx: &mut Context) {
let old_usage = self.latest_token_usage();
self.model = Some(model);
+ let new_caps = Self::prompt_capabilities(self.model.as_deref());
let new_usage = self.latest_token_usage();
if old_usage != new_usage {
cx.emit(TokenUsageUpdated(new_usage));
}
+ self.prompt_capabilities_tx.send(new_caps).log_err();
cx.notify()
}
@@ -1076,15 +1106,10 @@ impl Thread {
&mut self,
cx: &mut Context,
) -> Result>> {
- anyhow::ensure!(
- self.tool_use_limit_reached,
- "can only resume after tool use limit is reached"
- );
-
self.messages.push(Message::Resume);
cx.notify();
- log::info!("Total messages in thread: {}", self.messages.len());
+ log::debug!("Total messages in thread: {}", self.messages.len());
self.run_turn(cx)
}
@@ -1102,7 +1127,7 @@ impl Thread {
{
let model = self.model().context("No language model configured")?;
- log::info!("Thread::send called with model: {:?}", model.name());
+ log::info!("Thread::send called with model: {}", model.name().0);
self.advance_prompt_id();
let content = content.into_iter().map(Into::into).collect::>();
@@ -1112,7 +1137,7 @@ impl Thread {
.push(Message::User(UserMessage { id, content }));
cx.notify();
- log::info!("Total messages in thread: {}", self.messages.len());
+ log::debug!("Total messages in thread: {}", self.messages.len());
self.run_turn(cx)
}
@@ -1136,44 +1161,14 @@ impl Thread {
event_stream: event_stream.clone(),
tools: self.enabled_tools(profile, &model, cx),
_task: cx.spawn(async move |this, cx| {
- log::info!("Starting agent turn execution");
+ log::debug!("Starting agent turn execution");
- let turn_result: Result<()> = async {
- let mut intent = CompletionIntent::UserPrompt;
- loop {
- Self::stream_completion(&this, &model, intent, &event_stream, cx).await?;
-
- let mut end_turn = true;
- this.update(cx, |this, cx| {
- // Generate title if needed.
- if this.title.is_none() && this.pending_title_generation.is_none() {
- this.generate_title(cx);
- }
-
- // End the turn if the model didn't use tools.
- let message = this.pending_message.as_ref();
- end_turn =
- message.map_or(true, |message| message.tool_results.is_empty());
- this.flush_pending_message(cx);
- })?;
-
- if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
- log::info!("Tool use limit reached, completing turn");
- return Err(language_model::ToolUseLimitReachedError.into());
- } else if end_turn {
- log::info!("No tool uses found, completing turn");
- return Ok(());
- } else {
- intent = CompletionIntent::ToolResults;
- }
- }
- }
- .await;
+ let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await;
_ = this.update(cx, |this, cx| this.flush_pending_message(cx));
match turn_result {
Ok(()) => {
- log::info!("Turn execution completed");
+ log::debug!("Turn execution completed");
event_stream.send_stop(acp::StopReason::EndTurn);
}
Err(error) => {
@@ -1199,20 +1194,18 @@ impl Thread {
Ok(events_rx)
}
- async fn stream_completion(
+ async fn run_turn_internal(
this: &WeakEntity,
- model: &Arc,
- completion_intent: CompletionIntent,
+ model: Arc,
event_stream: &ThreadEventStream,
cx: &mut AsyncApp,
) -> Result<()> {
- log::debug!("Stream completion started successfully");
- let request = this.update(cx, |this, cx| {
- this.build_completion_request(completion_intent, cx)
- })??;
+ let mut attempt = 0;
+ let mut intent = CompletionIntent::UserPrompt;
+ loop {
+ let request =
+ this.update(cx, |this, cx| this.build_completion_request(intent, cx))??;
- let mut attempt = None;
- 'retry: loop {
telemetry::event!(
"Agent Thread Completion",
thread_id = this.read_with(cx, |this, _| this.id.to_string())?,
@@ -1222,75 +1215,31 @@ impl Thread {
attempt
);
- log::info!(
- "Calling model.stream_completion, attempt {}",
- attempt.unwrap_or(0)
- );
+ log::debug!("Calling model.stream_completion, attempt {}", attempt);
let mut events = model
- .stream_completion(request.clone(), cx)
+ .stream_completion(request, cx)
.await
.map_err(|error| anyhow!(error))?;
let mut tool_results = FuturesUnordered::new();
-
+ let mut error = None;
while let Some(event) = events.next().await {
+ log::trace!("Received completion event: {:?}", event);
match event {
Ok(event) => {
- log::trace!("Received completion event: {:?}", event);
tool_results.extend(this.update(cx, |this, cx| {
- this.handle_streamed_completion_event(event, event_stream, cx)
+ this.handle_completion_event(event, event_stream, cx)
})??);
}
- Err(error) => {
- let completion_mode =
- this.read_with(cx, |thread, _cx| thread.completion_mode())?;
- if completion_mode == CompletionMode::Normal {
- return Err(anyhow!(error))?;
- }
-
- let Some(strategy) = Self::retry_strategy_for(&error) else {
- return Err(anyhow!(error))?;
- };
-
- let max_attempts = match &strategy {
- RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
- RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
- };
-
- let attempt = attempt.get_or_insert(0u8);
-
- *attempt += 1;
-
- let attempt = *attempt;
- if attempt > max_attempts {
- return Err(anyhow!(error))?;
- }
-
- let delay = match &strategy {
- RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
- let delay_secs =
- initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
- Duration::from_secs(delay_secs)
- }
- RetryStrategy::Fixed { delay, .. } => *delay,
- };
- log::debug!("Retry attempt {attempt} with delay {delay:?}");
-
- event_stream.send_retry(acp_thread::RetryStatus {
- last_error: error.to_string().into(),
- attempt: attempt as usize,
- max_attempts: max_attempts as usize,
- started_at: Instant::now(),
- duration: delay,
- });
-
- cx.background_executor().timer(delay).await;
- continue 'retry;
+ Err(err) => {
+ error = Some(err);
+ break;
}
}
}
+ let end_turn = tool_results.is_empty();
while let Some(tool_result) = tool_results.next().await {
- log::info!("Tool finished {:?}", tool_result);
+ log::debug!("Tool finished {:?}", tool_result);
event_stream.update_tool_call_fields(
&tool_result.tool_use_id,
@@ -1311,31 +1260,83 @@ impl Thread {
})?;
}
- return Ok(());
+ this.update(cx, |this, cx| {
+ this.flush_pending_message(cx);
+ if this.title.is_none() && this.pending_title_generation.is_none() {
+ this.generate_title(cx);
+ }
+ })?;
+
+ if let Some(error) = error {
+ attempt += 1;
+ let retry =
+ this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
+ let timer = cx.background_executor().timer(retry.duration);
+ event_stream.send_retry(retry);
+ timer.await;
+ this.update(cx, |this, _cx| {
+ if let Some(Message::Agent(message)) = this.messages.last() {
+ if message.tool_results.is_empty() {
+ intent = CompletionIntent::UserPrompt;
+ this.messages.push(Message::Resume);
+ }
+ }
+ })?;
+ } else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
+ return Err(language_model::ToolUseLimitReachedError.into());
+ } else if end_turn {
+ return Ok(());
+ } else {
+ intent = CompletionIntent::ToolResults;
+ attempt = 0;
+ }
}
}
- pub fn build_system_message(&self, cx: &App) -> LanguageModelRequestMessage {
- log::debug!("Building system message");
- let prompt = SystemPromptTemplate {
- project: self.project_context.read(cx),
- available_tools: self.tools.keys().cloned().collect(),
+ fn handle_completion_error(
+ &mut self,
+ error: LanguageModelCompletionError,
+ attempt: u8,
+ ) -> Result {
+ if self.completion_mode == CompletionMode::Normal {
+ return Err(anyhow!(error));
}
- .render(&self.templates)
- .context("failed to build system prompt")
- .expect("Invalid template");
- log::debug!("System message built");
- LanguageModelRequestMessage {
- role: Role::System,
- content: vec![prompt.into()],
- cache: true,
+
+ let Some(strategy) = Self::retry_strategy_for(&error) else {
+ return Err(anyhow!(error));
+ };
+
+ let max_attempts = match &strategy {
+ RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
+ RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
+ };
+
+ if attempt > max_attempts {
+ return Err(anyhow!(error));
}
+
+ let delay = match &strategy {
+ RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
+ let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
+ Duration::from_secs(delay_secs)
+ }
+ RetryStrategy::Fixed { delay, .. } => *delay,
+ };
+ log::debug!("Retry attempt {attempt} with delay {delay:?}");
+
+ Ok(acp_thread::RetryStatus {
+ last_error: error.to_string().into(),
+ attempt: attempt as usize,
+ max_attempts: max_attempts as usize,
+ started_at: Instant::now(),
+ duration: delay,
+ })
}
/// A helper method that's called on every streamed completion event.
/// Returns an optional tool result task, which the main agentic loop will
/// send back to the model when it resolves.
- fn handle_streamed_completion_event(
+ fn handle_completion_event(
&mut self,
event: LanguageModelCompletionEvent,
event_stream: &ThreadEventStream,
@@ -1530,7 +1531,7 @@ impl Thread {
});
let supports_images = self.model().is_some_and(|model| model.supports_images());
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
- log::info!("Running tool {}", tool_use.name);
+ log::debug!("Running tool {}", tool_use.name);
Some(cx.foreground_executor().spawn(async move {
let tool_result = tool_result.await.and_then(|output| {
if let LanguageModelToolResultContent::Image(_) = &output.llm_output
@@ -1642,7 +1643,7 @@ impl Thread {
summary.extend(lines.next());
}
- log::info!("Setting summary: {}", summary);
+ log::debug!("Setting summary: {}", summary);
let summary = SharedString::from(summary);
this.update(cx, |this, cx| {
@@ -1659,7 +1660,7 @@ impl Thread {
return;
};
- log::info!(
+ log::debug!(
"Generating title with model: {:?}",
self.summarization_model.as_ref().map(|model| model.name())
);
@@ -1745,6 +1746,10 @@ impl Thread {
return;
};
+ if message.content.is_empty() {
+ return;
+ }
+
for content in &message.content {
let AgentMessageContent::ToolUse(tool_use) = content else {
continue;
@@ -1773,7 +1778,7 @@ impl Thread {
pub(crate) fn build_completion_request(
&self,
completion_intent: CompletionIntent,
- cx: &mut App,
+ cx: &App,
) -> Result {
let model = self.model().context("No language model configured")?;
let tools = if let Some(turn) = self.running_turn.as_ref() {
@@ -1797,8 +1802,8 @@ impl Thread {
log::debug!("Completion mode: {:?}", self.completion_mode);
let messages = self.build_request_messages(cx);
- log::info!("Request will include {} messages", messages.len());
- log::info!("Request includes {} tools", tools.len());
+ log::debug!("Request will include {} messages", messages.len());
+ log::debug!("Request includes {} tools", tools.len());
let request = LanguageModelRequest {
thread_id: Some(self.id.to_string()),
@@ -1894,21 +1899,29 @@ impl Thread {
"Building request messages from {} thread messages",
self.messages.len()
);
- let mut messages = vec![self.build_system_message(cx)];
+
+ let system_prompt = SystemPromptTemplate {
+ project: self.project_context.read(cx),
+ available_tools: self.tools.keys().cloned().collect(),
+ }
+ .render(&self.templates)
+ .context("failed to build system prompt")
+ .expect("Invalid template");
+ let mut messages = vec![LanguageModelRequestMessage {
+ role: Role::System,
+ content: vec![system_prompt.into()],
+ cache: false,
+ }];
for message in &self.messages {
messages.extend(message.to_request());
}
- if let Some(message) = self.pending_message.as_ref() {
- messages.extend(message.to_request());
+ if let Some(last_message) = messages.last_mut() {
+ last_message.cache = true;
}
- if let Some(last_user_message) = messages
- .iter_mut()
- .rev()
- .find(|message| message.role == Role::User)
- {
- last_user_message.cache = true;
+ if let Some(message) = self.pending_message.as_ref() {
+ messages.extend(message.to_request());
}
messages
diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent2/src/tools/fetch_tool.rs
index 0313c4e4c2..dd97271a79 100644
--- a/crates/agent2/src/tools/fetch_tool.rs
+++ b/crates/agent2/src/tools/fetch_tool.rs
@@ -136,12 +136,17 @@ impl AgentTool for FetchTool {
fn run(
self: Arc,
input: Self::Input,
- _event_stream: ToolCallEventStream,
+ event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task> {
+ let authorize = event_stream.authorize(input.url.clone(), cx);
+
let text = cx.background_spawn({
let http_client = self.http_client.clone();
- async move { Self::build_message(http_client, &input.url).await }
+ async move {
+ authorize.await?;
+ Self::build_message(http_client, &input.url).await
+ }
});
cx.foreground_executor().spawn(async move {
diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent2/src/tools/find_path_tool.rs
index 5b35c40f85..384bd56e77 100644
--- a/crates/agent2/src/tools/find_path_tool.rs
+++ b/crates/agent2/src/tools/find_path_tool.rs
@@ -165,16 +165,17 @@ fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task) -> SharedString {
- if let Ok(input) = input {
- let path = &input.path;
- match (input.start_line, input.end_line) {
- (Some(start), Some(end)) => {
- format!(
- "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
- path, start, end, path, start, end
- )
- }
- (Some(start), None) => {
- format!(
- "[Read file `{}` (from line {})](@selection:{}:({}-{}))",
- path, start, path, start, start
- )
- }
- _ => format!("[Read file `{}`](@file:{})", path, path),
- }
- .into()
- } else {
- "Read file".into()
- }
+ input
+ .ok()
+ .as_ref()
+ .and_then(|input| Path::new(&input.path).file_name())
+ .map(|file_name| file_name.to_string_lossy().to_string().into())
+ .unwrap_or_default()
}
fn run(
@@ -258,6 +244,19 @@ impl AgentTool for ReadFileTool {
}]),
..Default::default()
});
+ if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
+ let markdown = MarkdownCodeBlock {
+ tag: &input.path,
+ text,
+ }
+ .to_string();
+ event_stream.update_fields(ToolCallUpdateFields {
+ content: Some(vec![acp::ToolCallContent::Content {
+ content: markdown.into(),
+ }]),
+ ..Default::default()
+ })
+ }
}
})?;
diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs
index c9c938c6c0..9080fc1ab0 100644
--- a/crates/agent_servers/src/acp.rs
+++ b/crates/agent_servers/src/acp.rs
@@ -185,13 +185,16 @@ impl AgentConnection for AcpConnection {
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
- let thread = cx.new(|_cx| {
+ let thread = cx.new(|cx| {
AcpThread::new(
self.server_name.clone(),
self.clone(),
project,
action_log,
session_id.clone(),
+ // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
+ watch::Receiver::constant(self.prompt_capabilities),
+ cx,
)
})?;
@@ -263,7 +266,9 @@ impl AgentConnection for AcpConnection {
match serde_json::from_value(data.clone()) {
Ok(ErrorDetails { details }) => {
- if suppress_abort_err && details.contains("This operation was aborted")
+ if suppress_abort_err
+ && (details.contains("This operation was aborted")
+ || details.contains("The user aborted a request"))
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
@@ -279,10 +284,6 @@ impl AgentConnection for AcpConnection {
})
}
- fn prompt_capabilities(&self) -> acp::PromptCapabilities {
- self.prompt_capabilities
- }
-
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
session.suppress_abort_err = true;
diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs
index fa59201338..7c7e124ca7 100644
--- a/crates/agent_servers/src/agent_servers.rs
+++ b/crates/agent_servers/src/agent_servers.rs
@@ -36,6 +36,7 @@ pub trait AgentServer: Send {
fn name(&self) -> SharedString;
fn empty_state_headline(&self) -> SharedString;
fn empty_state_message(&self) -> SharedString;
+ fn telemetry_id(&self) -> &'static str;
fn connect(
&self,
@@ -97,7 +98,7 @@ pub struct AgentServerCommand {
}
impl AgentServerCommand {
- pub(crate) async fn resolve(
+ pub async fn resolve(
path_bin_name: &'static str,
extra_args: &[&'static str],
fallback_path: Option<&Path>,
diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs
index 048563103f..250e564526 100644
--- a/crates/agent_servers/src/claude.rs
+++ b/crates/agent_servers/src/claude.rs
@@ -43,6 +43,10 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
pub struct ClaudeCode;
impl AgentServer for ClaudeCode {
+ fn telemetry_id(&self) -> &'static str {
+ "claude-code"
+ }
+
fn name(&self) -> SharedString {
"Claude Code".into()
}
@@ -249,13 +253,19 @@ impl AgentConnection for ClaudeAgentConnection {
});
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
- let thread = cx.new(|_cx| {
+ let thread = cx.new(|cx| {
AcpThread::new(
"Claude Code",
self.clone(),
project,
action_log,
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? })
}
- 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) {
let sessions = self.sessions.borrow();
let Some(session) = sessions.get(session_id) else {
diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs
index e544c4f21f..72823026d7 100644
--- a/crates/agent_servers/src/custom.rs
+++ b/crates/agent_servers/src/custom.rs
@@ -22,6 +22,10 @@ impl CustomAgentServer {
}
impl crate::AgentServer for CustomAgentServer {
+ fn telemetry_id(&self) -> &'static str {
+ "custom"
+ }
+
fn name(&self) -> SharedString {
self.name.clone()
}
diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs
index 9ebcee745c..5d6a70fa64 100644
--- a/crates/agent_servers/src/gemini.rs
+++ b/crates/agent_servers/src/gemini.rs
@@ -17,6 +17,10 @@ pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp";
impl AgentServer for Gemini {
+ fn telemetry_id(&self) -> &'static str {
+ "gemini-cli"
+ }
+
fn name(&self) -> SharedString {
"Gemini CLI".into()
}
@@ -53,7 +57,7 @@ impl AgentServer for Gemini {
return Err(LoadError::NotInstalled {
error_message: "Failed to find Gemini CLI binary".into(),
install_message: "Install Gemini CLI".into(),
- install_command: "npm install -g @google/gemini-cli@preview".into()
+ install_command: Self::install_command().into(),
}.into());
};
@@ -88,7 +92,7 @@ impl AgentServer for Gemini {
current_version
).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())
}
}
@@ -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)]
pub(crate) mod tests {
use super::*;
diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs
index 70faa0ed27..12ae893c31 100644
--- a/crates/agent_ui/src/acp/message_editor.rs
+++ b/crates/agent_ui/src/acp/message_editor.rs
@@ -373,7 +373,7 @@ impl MessageEditor {
if Img::extensions().contains(&extension) && !extension.contains("svg") {
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
.project
diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs
index 5d852f0ddc..a49dae25b3 100644
--- a/crates/agent_ui/src/acp/thread_history.rs
+++ b/crates/agent_ui/src/acp/thread_history.rs
@@ -462,7 +462,7 @@ impl AcpThreadHistory {
cx.notify();
}))
- .end_slot::(if hovered || selected {
+ .end_slot::(if hovered {
Some(
IconButton::new("delete", IconName::Trash)
.shape(IconButtonShape::Square)
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index 9caa4bad8c..837ce6f90a 100644
--- a/crates/agent_ui/src/acp/thread_view.rs
+++ b/crates/agent_ui/src/acp/thread_view.rs
@@ -35,6 +35,7 @@ use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::{Settings as _, SettingsStore};
use std::cell::Cell;
+use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use std::{collections::BTreeMap, rc::Rc, time::Duration};
@@ -274,6 +275,7 @@ pub struct AcpThreadView {
edits_expanded: bool,
plan_expanded: bool,
editor_expanded: bool,
+ should_be_following: bool,
editing_message: Option,
prompt_capabilities: Rc>,
is_loading_contents: bool,
@@ -385,6 +387,7 @@ impl AcpThreadView {
edits_expanded: false,
plan_expanded: false,
editor_expanded: false,
+ should_be_following: false,
history_store,
hovered_recent_history_item: None,
prompt_capabilities,
@@ -472,7 +475,7 @@ impl AcpThreadView {
let action_log = thread.read(cx).action_log().clone();
this.prompt_capabilities
- .set(connection.prompt_capabilities());
+ .set(thread.read(cx).prompt_capabilities());
let count = thread.read(cx).entries().len();
this.list_state.splice(0..0, count);
@@ -820,6 +823,9 @@ impl AcpThreadView {
let Some(thread) = self.thread() else {
return;
};
+ if !thread.read(cx).can_resume(cx) {
+ return;
+ }
let task = thread.update(cx, |thread, cx| thread.resume(cx));
cx.spawn(async move |this, cx| {
@@ -887,6 +893,8 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context,
) {
+ let agent_telemetry_id = self.agent.telemetry_id();
+
self.thread_error.take();
self.editing_message.take();
self.thread_feedback.clear();
@@ -894,6 +902,13 @@ impl AcpThreadView {
let Some(thread) = self.thread().cloned() else {
return;
};
+ if self.should_be_following {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ workspace.follow(CollaboratorId::Agent, window, cx);
+ })
+ .ok();
+ }
self.is_loading_contents = true;
let guard = cx.new(|_| ());
@@ -924,6 +939,9 @@ impl AcpThreadView {
}
});
drop(guard);
+
+ telemetry::event!("Agent Message Sent", agent = agent_telemetry_id);
+
thread.send(contents, cx)
})?;
send.await
@@ -935,6 +953,16 @@ impl AcpThreadView {
this.handle_thread_error(err, cx);
})
.ok();
+ } else {
+ this.update(cx, |this, cx| {
+ this.should_be_following = this
+ .workspace
+ .update(cx, |workspace, _| {
+ workspace.is_being_followed(CollaboratorId::Agent)
+ })
+ .unwrap_or_default();
+ })
+ .ok();
}
})
.detach();
@@ -1141,6 +1169,10 @@ impl AcpThreadView {
});
}
}
+ AcpThreadEvent::PromptCapabilitiesUpdated => {
+ self.prompt_capabilities
+ .set(thread.read(cx).prompt_capabilities());
+ }
AcpThreadEvent::TokenUsageUpdated => {}
}
cx.notify();
@@ -1220,30 +1252,44 @@ impl AcpThreadView {
pending_auth_method.replace(method.clone());
let authenticate = connection.authenticate(method, cx);
cx.notify();
- self.auth_task = Some(cx.spawn_in(window, {
- let project = self.project.clone();
- let agent = self.agent.clone();
- async move |this, cx| {
- let result = authenticate.await;
+ self.auth_task =
+ Some(cx.spawn_in(window, {
+ let project = self.project.clone();
+ let agent = self.agent.clone();
+ async move |this, cx| {
+ let result = authenticate.await;
- this.update_in(cx, |this, window, cx| {
- 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,
- )
+ match &result {
+ Ok(_) => telemetry::event!(
+ "Authenticate Agent Succeeded",
+ agent = agent.telemetry_id()
+ ),
+ Err(_) => {
+ telemetry::event!(
+ "Authenticate Agent Failed",
+ agent = agent.telemetry_id(),
+ )
+ }
}
- this.auth_task.take()
- })
- .ok();
- }
- }));
+
+ this.update_in(cx, |this, window, cx| {
+ 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(
@@ -1251,6 +1297,7 @@ impl AcpThreadView {
tool_call_id: acp::ToolCallId,
option_id: acp::PermissionOptionId,
option_kind: acp::PermissionOptionKind,
+ window: &mut Window,
cx: &mut Context,
) {
let Some(thread) = self.thread() else {
@@ -1259,6 +1306,13 @@ impl AcpThreadView {
thread.update(cx, |thread, cx| {
thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
});
+ if self.should_be_following {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ workspace.follow(CollaboratorId::Agent, window, cx);
+ })
+ .ok();
+ }
cx.notify();
}
@@ -1302,11 +1356,24 @@ impl AcpThreadView {
None
};
+ let has_checkpoint_button = message
+ .checkpoint
+ .as_ref()
+ .is_some_and(|checkpoint| checkpoint.show);
+
let agent_name = self.agent.name();
v_flex()
.id(("user_message", entry_ix))
- .pt_2()
+ .map(|this| {
+ if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() {
+ this.pt_4()
+ } else if rules_item.is_some() {
+ this.pt_3()
+ } else {
+ this.pt_2()
+ }
+ })
.pb_4()
.px_2()
.gap_1p5()
@@ -1315,6 +1382,7 @@ impl AcpThreadView {
.children(message.id.clone().and_then(|message_id| {
message.checkpoint.as_ref()?.show.then(|| {
h_flex()
+ .px_3()
.gap_2()
.child(Divider::horizontal())
.child(
@@ -1484,17 +1552,14 @@ impl AcpThreadView {
return primary;
};
- let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
- let primary = if entry_ix == total_entries - 1 && !is_generating {
+ let primary = if entry_ix == total_entries - 1 {
v_flex()
.w_full()
.child(primary)
- .child(self.render_thread_controls(cx))
+ .child(self.render_thread_controls(&thread, cx))
.when_some(
self.thread_feedback.comments_editor.clone(),
- |this, editor| {
- this.child(Self::render_feedback_feedback_editor(editor, window, cx))
- },
+ |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
)
.into_any_element()
} else {
@@ -1633,15 +1698,16 @@ impl AcpThreadView {
.into_any_element()
}
- fn render_tool_call_icon(
+ fn render_tool_call(
&self,
- group_name: SharedString,
entry_ix: usize,
- is_collapsible: bool,
- is_open: bool,
tool_call: &ToolCall,
+ window: &Window,
cx: &Context,
) -> Div {
+ let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
+ let card_header_id = SharedString::from("inner-tool-call-header");
+
let tool_icon =
if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 {
FileIcons::get_icon(&tool_call.locations[0].path, cx)
@@ -1649,7 +1715,7 @@ impl AcpThreadView {
.unwrap_or(Icon::new(IconName::ToolPencil))
} else {
Icon::new(match tool_call.kind {
- acp::ToolKind::Read => IconName::ToolRead,
+ acp::ToolKind::Read => IconName::ToolSearch,
acp::ToolKind::Edit => IconName::ToolPencil,
acp::ToolKind::Delete => IconName::ToolDeleteFile,
acp::ToolKind::Move => IconName::ArrowRightLeft,
@@ -1663,59 +1729,6 @@ impl AcpThreadView {
.size(IconSize::Small)
.color(Color::Muted);
- let base_container = h_flex().flex_shrink_0().size_4().justify_center();
-
- if is_collapsible {
- base_container
- .child(
- div()
- .group_hover(&group_name, |s| s.invisible().w_0())
- .child(tool_icon),
- )
- .child(
- h_flex()
- .absolute()
- .inset_0()
- .invisible()
- .justify_center()
- .group_hover(&group_name, |s| s.visible())
- .child(
- Disclosure::new(("expand", entry_ix), is_open)
- .opened_icon(IconName::ChevronUp)
- .closed_icon(IconName::ChevronRight)
- .on_click(cx.listener({
- let id = tool_call.id.clone();
- move |this: &mut Self, _, _, cx: &mut Context| {
- if is_open {
- this.expanded_tool_calls.remove(&id);
- } else {
- this.expanded_tool_calls.insert(id.clone());
- }
- cx.notify();
- }
- })),
- ),
- )
- } else {
- base_container.child(tool_icon)
- }
- }
-
- fn render_tool_call(
- &self,
- entry_ix: usize,
- tool_call: &ToolCall,
- window: &Window,
- cx: &Context,
- ) -> Div {
- let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
- let card_header_id = SharedString::from("inner-tool-call-header");
-
- let in_progress = match &tool_call.status {
- ToolCallStatus::InProgress => true,
- _ => false,
- };
-
let failed_or_canceled = match &tool_call.status {
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
_ => false,
@@ -1725,6 +1738,7 @@ impl AcpThreadView {
tool_call.status,
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
);
+
let needs_confirmation = matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
@@ -1742,7 +1756,7 @@ impl AcpThreadView {
.absolute()
.top_0()
.right_0()
- .w_16()
+ .w_12()
.h_full()
.bg(linear_gradient(
90.,
@@ -1814,6 +1828,7 @@ impl AcpThreadView {
.child(
h_flex()
.id(header_id)
+ .group(&card_header_id)
.relative()
.w_full()
.max_w_full()
@@ -1831,19 +1846,11 @@ impl AcpThreadView {
})
.child(
h_flex()
- .group(&card_header_id)
.relative()
.w_full()
.h(window.line_height() - px(2.))
.text_size(self.tool_name_font_size())
- .child(self.render_tool_call_icon(
- card_header_id,
- entry_ix,
- is_collapsible,
- is_open,
- tool_call,
- cx,
- ))
+ .child(tool_icon)
.child(if tool_call.locations.len() == 1 {
let name = tool_call.locations[0]
.path
@@ -1871,13 +1878,13 @@ impl AcpThreadView {
})
.child(name)
.tooltip(Tooltip::text("Jump to File"))
+ .cursor(gpui::CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
}))
.into_any_element()
} else {
h_flex()
- .id("non-card-label-container")
.relative()
.w_full()
.max_w_full()
@@ -1888,47 +1895,39 @@ impl AcpThreadView {
default_markdown_style(false, true, window, cx),
)))
.child(gradient_overlay(gradient_color))
- .on_click(cx.listener({
- let id = tool_call.id.clone();
- move |this: &mut Self, _, _, cx: &mut Context| {
- if is_open {
- this.expanded_tool_calls.remove(&id);
- } else {
- this.expanded_tool_calls.insert(id.clone());
- }
- cx.notify();
- }
- }))
.into_any()
}),
)
- .when(in_progress && use_card_layout, |this| {
- this.child(
- div().absolute().right_2().child(
- Icon::new(IconName::ArrowCircle)
- .color(Color::Muted)
- .size(IconSize::Small)
- .with_animation(
- "running",
- Animation::new(Duration::from_secs(3)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(percentage(
- delta,
- )))
- },
- ),
- ),
- )
- })
- .when(failed_or_canceled, |this| {
- this.child(
- div().absolute().right_2().child(
- Icon::new(IconName::Close)
- .color(Color::Error)
- .size(IconSize::Small),
- ),
- )
- }),
+ .child(
+ h_flex()
+ .gap_px()
+ .when(is_collapsible, |this| {
+ this.child(
+ Disclosure::new(("expand", entry_ix), is_open)
+ .opened_icon(IconName::ChevronUp)
+ .closed_icon(IconName::ChevronDown)
+ .visible_on_hover(&card_header_id)
+ .on_click(cx.listener({
+ let id = tool_call.id.clone();
+ move |this: &mut Self, _, _, cx: &mut Context| {
+ if is_open {
+ this.expanded_tool_calls.remove(&id);
+ } else {
+ this.expanded_tool_calls.insert(id.clone());
+ }
+ cx.notify();
+ }
+ })),
+ )
+ })
+ .when(failed_or_canceled, |this| {
+ this.child(
+ Icon::new(IconName::Close)
+ .color(Color::Error)
+ .size(IconSize::Small),
+ )
+ }),
+ ),
)
.children(tool_output_display)
}
@@ -1998,9 +1997,27 @@ impl AcpThreadView {
cx: &Context,
) -> AnyElement {
let uri: SharedString = resource_link.uri.clone().into();
+ let is_file = resource_link.uri.strip_prefix("file://");
- let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") {
- path.to_string().into()
+ let label: SharedString = if let Some(abs_path) = is_file {
+ if let Some(project_path) = self
+ .project
+ .read(cx)
+ .project_path_for_absolute_path(&Path::new(abs_path), cx)
+ && let Some(worktree) = self
+ .project
+ .read(cx)
+ .worktree_for_id(project_path.worktree_id, cx)
+ {
+ worktree
+ .read(cx)
+ .full_path(&project_path.path)
+ .to_string_lossy()
+ .to_string()
+ .into()
+ } else {
+ abs_path.to_string().into()
+ }
} else {
uri.clone()
};
@@ -2017,10 +2034,12 @@ impl AcpThreadView {
Button::new(button_id, label)
.label_size(LabelSize::Small)
.color(Color::Muted)
- .icon(IconName::ArrowUpRight)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
.truncate(true)
+ .when(is_file.is_none(), |this| {
+ this.icon(IconName::ArrowUpRight)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ })
.on_click(cx.listener({
let workspace = self.workspace.clone();
move |_, _, window, cx: &mut Context| {
@@ -2079,11 +2098,12 @@ impl AcpThreadView {
let tool_call_id = tool_call_id.clone();
let option_id = option.id.clone();
let option_kind = option.kind;
- move |this, _, _, cx| {
+ move |this, _, window, cx| {
this.authorize_tool_call(
tool_call_id.clone(),
option_id.clone(),
option_kind,
+ window,
cx,
);
}
@@ -2460,7 +2480,6 @@ impl AcpThreadView {
Some(
h_flex()
.px_2p5()
- .pb_1()
.child(
Icon::new(IconName::Attach)
.size(IconSize::XSmall)
@@ -2476,8 +2495,7 @@ impl AcpThreadView {
Label::new(user_rules_text)
.size(LabelSize::XSmall)
.color(Color::Muted)
- .truncate()
- .buffer_font(cx),
+ .truncate(),
)
.hover(|s| s.bg(cx.theme().colors().element_hover))
.tooltip(Tooltip::text("View User Rules"))
@@ -2491,7 +2509,13 @@ impl AcpThreadView {
}),
)
})
- .when(has_both, |this| this.child(Divider::vertical()))
+ .when(has_both, |this| {
+ this.child(
+ Label::new("•")
+ .size(LabelSize::XSmall)
+ .color(Color::Disabled),
+ )
+ })
.when_some(rules_file_text, |parent, rules_file_text| {
parent.child(
h_flex()
@@ -2500,8 +2524,7 @@ impl AcpThreadView {
.child(
Label::new(rules_file_text)
.size(LabelSize::XSmall)
- .color(Color::Muted)
- .buffer_font(cx),
+ .color(Color::Muted),
)
.hover(|s| s.bg(cx.theme().colors().element_hover))
.tooltip(Tooltip::text("View Project Rules"))
@@ -2725,6 +2748,12 @@ impl AcpThreadView {
.on_click({
let method_id = method.id.clone();
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)
})
})
@@ -2753,6 +2782,8 @@ impl AcpThreadView {
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(move |this, _, window, cx| {
+ telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id());
+
let task = this
.workspace
.update(cx, |workspace, cx| {
@@ -2760,7 +2791,7 @@ impl AcpThreadView {
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
- id: task::TaskId("install".to_string()),
+ id: task::TaskId(install_command.clone()),
full_label: install_command.clone(),
label: install_command.clone(),
command: Some(install_command.clone()),
@@ -2810,6 +2841,8 @@ impl AcpThreadView {
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(move |this, _, window, cx| {
+ telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id());
+
let task = this
.workspace
.update(cx, |workspace, cx| {
@@ -2817,7 +2850,7 @@ impl AcpThreadView {
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
- id: task::TaskId("upgrade".to_string()),
+ id: task::TaskId(upgrade_command.to_string()),
full_label: upgrade_command.clone(),
label: upgrade_command.clone(),
command: Some(upgrade_command.clone()),
@@ -3078,13 +3111,13 @@ impl AcpThreadView {
h_flex()
.p_1()
.justify_between()
+ .flex_wrap()
.when(expanded, |this| {
this.border_b_1().border_color(cx.theme().colors().border)
})
.child(
h_flex()
.id("edits-container")
- .w_full()
.gap_1()
.child(Disclosure::new("edits-disclosure", expanded))
.map(|this| {
@@ -3633,13 +3666,53 @@ impl AcpThreadView {
}
}
+ fn is_following(&self, cx: &App) -> bool {
+ match self.thread().map(|thread| thread.read(cx).status()) {
+ Some(ThreadStatus::Generating) => self
+ .workspace
+ .read_with(cx, |workspace, _| {
+ workspace.is_being_followed(CollaboratorId::Agent)
+ })
+ .unwrap_or(false),
+ _ => self.should_be_following,
+ }
+ }
+
+ fn toggle_following(&mut self, window: &mut Window, cx: &mut Context) {
+ let following = self.is_following(cx);
+
+ self.should_be_following = !following;
+ if self.thread().map(|thread| thread.read(cx).status()) == Some(ThreadStatus::Generating) {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ if following {
+ workspace.unfollow(CollaboratorId::Agent, window, cx);
+ } else {
+ workspace.follow(CollaboratorId::Agent, window, cx);
+ }
+ })
+ .ok();
+ }
+
+ telemetry::event!("Follow Agent Selected", following = !following);
+ }
+
fn render_follow_toggle(&self, cx: &mut Context) -> impl IntoElement {
- let following = self
- .workspace
- .read_with(cx, |workspace, _| {
- workspace.is_being_followed(CollaboratorId::Agent)
- })
- .unwrap_or(false);
+ let following = self.is_following(cx);
+
+ let tooltip_label = if following {
+ if self.agent.name() == "Zed Agent" {
+ format!("Stop Following the {}", self.agent.name())
+ } else {
+ format!("Stop Following {}", self.agent.name())
+ }
+ } else {
+ if self.agent.name() == "Zed Agent" {
+ format!("Follow the {}", self.agent.name())
+ } else {
+ format!("Follow {}", self.agent.name())
+ }
+ };
IconButton::new("follow-agent", IconName::Crosshair)
.icon_size(IconSize::Small)
@@ -3648,10 +3721,10 @@ impl AcpThreadView {
.selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
.tooltip(move |window, cx| {
if following {
- Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
+ Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx)
} else {
Tooltip::with_meta(
- "Follow Agent",
+ tooltip_label.clone(),
Some(&Follow),
"Track the agent's location as it reads and edits files.",
window,
@@ -3660,15 +3733,7 @@ impl AcpThreadView {
}
})
.on_click(cx.listener(move |this, _, window, cx| {
- this.workspace
- .update(cx, |workspace, cx| {
- if following {
- workspace.unfollow(CollaboratorId::Agent, window, cx);
- } else {
- workspace.follow(CollaboratorId::Agent, window, cx);
- }
- })
- .ok();
+ this.toggle_following(window, cx);
}))
}
@@ -4080,7 +4145,20 @@ impl AcpThreadView {
}
}
- fn render_thread_controls(&self, cx: &Context) -> impl IntoElement {
+ fn render_thread_controls(
+ &self,
+ thread: &Entity,
+ cx: &Context,
+ ) -> impl IntoElement {
+ let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
+ if is_generating {
+ return h_flex().id("thread-controls-container").ml_1().child(
+ div()
+ .py_2()
+ .px(rems_from_px(22.))
+ .child(SpinnerLabel::new().size(LabelSize::Small)),
+ );
+ }
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
@@ -4177,13 +4255,8 @@ impl AcpThreadView {
container.child(open_as_markdown).child(scroll_to_top)
}
- fn render_feedback_feedback_editor(
- editor: Entity,
- window: &mut Window,
- cx: &Context,
- ) -> Div {
- let focus_handle = editor.focus_handle(cx);
- v_flex()
+ fn render_feedback_feedback_editor(editor: Entity, cx: &Context) -> Div {
+ h_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
this.thread_feedback.dismiss_comments();
@@ -4192,43 +4265,31 @@ impl AcpThreadView {
.on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
this.submit_feedback_message(cx);
}))
- .mb_2()
- .mx_4()
.p_2()
+ .mb_2()
+ .mx_5()
+ .gap_1()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
- .child(editor)
+ .child(div().w_full().child(editor))
.child(
h_flex()
- .gap_1()
- .justify_end()
.child(
- Button::new("dismiss-feedback-message", "Cancel")
- .label_size(LabelSize::Small)
- .key_binding(
- KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
- .map(|kb| kb.size(rems_from_px(10.))),
- )
+ IconButton::new("dismiss-feedback-message", IconName::Close)
+ .icon_color(Color::Error)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread_feedback.dismiss_comments();
cx.notify();
})),
)
.child(
- Button::new("submit-feedback-message", "Share Feedback")
- .style(ButtonStyle::Tinted(ui::TintColor::Accent))
- .label_size(LabelSize::Small)
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Confirm,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(10.))),
- )
+ IconButton::new("submit-feedback-message", IconName::Return)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
.on_click(cx.listener(move |this, _, _window, cx| {
this.submit_feedback_message(cx);
})),
@@ -4469,12 +4530,53 @@ impl AcpThreadView {
}
fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
+ let can_resume = self
+ .thread()
+ .map_or(false, |thread| thread.read(cx).can_resume(cx));
+
+ let can_enable_burn_mode = self.as_native_thread(cx).map_or(false, |thread| {
+ let thread = thread.read(cx);
+ let supports_burn_mode = thread
+ .model()
+ .map_or(false, |model| model.supports_burn_mode());
+ supports_burn_mode && thread.completion_mode() == CompletionMode::Normal
+ });
+
Callout::new()
.severity(Severity::Error)
.title("Error")
.icon(IconName::XCircle)
.description(error.clone())
- .actions_slot(self.create_copy_button(error.to_string()))
+ .actions_slot(
+ h_flex()
+ .gap_0p5()
+ .when(can_resume && can_enable_burn_mode, |this| {
+ this.child(
+ Button::new("enable-burn-mode-and-retry", "Enable Burn Mode and Retry")
+ .icon(IconName::ZedBurnMode)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.toggle_burn_mode(&ToggleBurnMode, window, cx);
+ this.resume_chat(cx);
+ })),
+ )
+ })
+ .when(can_resume, |this| {
+ this.child(
+ Button::new("retry", "Retry")
+ .icon(IconName::RotateCw)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::Small)
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener(|this, _, _window, cx| {
+ this.resume_chat(cx);
+ })),
+ )
+ })
+ .child(self.create_copy_button(error.to_string())),
+ )
.dismiss_action(self.dismiss_error_button(cx))
}
@@ -4662,6 +4764,24 @@ impl AcpThreadView {
}))
}
+ pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context) {
+ let agent = self.agent.clone();
+ let ThreadState::Ready { thread, .. } = &self.thread_state else {
+ return;
+ };
+
+ let connection = thread.read(cx).connection().clone();
+ let err = AuthRequired {
+ description: None,
+ provider_id: None,
+ };
+ self.clear_thread_error(cx);
+ let this = cx.weak_entity();
+ window.defer(cx, |window, cx| {
+ Self::handle_auth_required(this, err, agent, connection, window, cx);
+ })
+ }
+
fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement {
Button::new("upgrade", "Upgrade")
.label_size(LabelSize::Small)
@@ -4762,45 +4882,30 @@ impl Render for AcpThreadView {
.items_center()
.justify_end()
.child(self.render_load_error(e, cx)),
- ThreadState::Ready { thread, .. } => {
- let thread_clone = thread.clone();
-
- v_flex().flex_1().map(|this| {
- if has_messages {
- this.child(
- list(
- self.list_state.clone(),
- cx.processor(|this, index: usize, window, cx| {
- let Some((entry, len)) = this.thread().and_then(|thread| {
- let entries = &thread.read(cx).entries();
- Some((entries.get(index)?, entries.len()))
- }) else {
- return Empty.into_any();
- };
- this.render_entry(index, len, entry, window, cx)
- }),
- )
- .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
- .flex_grow()
- .into_any(),
+ ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
+ if has_messages {
+ this.child(
+ list(
+ self.list_state.clone(),
+ cx.processor(|this, index: usize, window, cx| {
+ let Some((entry, len)) = this.thread().and_then(|thread| {
+ let entries = &thread.read(cx).entries();
+ Some((entries.get(index)?, entries.len()))
+ }) else {
+ return Empty.into_any();
+ };
+ this.render_entry(index, len, entry, window, cx)
+ }),
)
- .child(self.render_vertical_scrollbar(cx))
- .children(
- match thread_clone.read(cx).status() {
- ThreadStatus::Idle
- | ThreadStatus::WaitingForToolConfirmation => None,
- ThreadStatus::Generating => div()
- .py_2()
- .px(rems_from_px(22.))
- .child(SpinnerLabel::new().size(LabelSize::Small))
- .into(),
- },
- )
- } else {
- this.child(self.render_recent_history(window, cx))
- }
- })
- }
+ .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
+ .flex_grow()
+ .into_any(),
+ )
+ .child(self.render_vertical_scrollbar(cx))
+ } else {
+ this.child(self.render_recent_history(window, cx))
+ }
+ }),
})
// The activity bar is intentionally rendered outside of the ThreadState::Ready match
// above so that the scrollbar doesn't render behind it. The current setup allows
@@ -5217,6 +5322,10 @@ pub(crate) mod tests {
where
C: 'static + AgentConnection + Send + Clone,
{
+ fn telemetry_id(&self) -> &'static str {
+ "test"
+ }
+
fn logo(&self) -> ui::IconName {
ui::IconName::Ai
}
@@ -5265,6 +5374,12 @@ pub(crate) mod tests {
project,
action_log,
SessionId("test".into()),
+ watch::Receiver::constant(acp::PromptCapabilities {
+ image: true,
+ audio: true,
+ embedded_context: true,
+ }),
+ cx,
)
})))
}
@@ -5273,14 +5388,6 @@ pub(crate) mod tests {
&[]
}
- fn prompt_capabilities(&self) -> acp::PromptCapabilities {
- acp::PromptCapabilities {
- image: true,
- audio: true,
- embedded_context: true,
- }
- }
-
fn authenticate(
&self,
_method_id: acp::AuthMethodId,
diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs
index f33f0ba032..52fb7eed4b 100644
--- a/crates/agent_ui/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -5,6 +5,7 @@ mod tool_picker;
use std::{sync::Arc, time::Duration};
+use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
use agent_settings::AgentSettings;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan;
@@ -15,7 +16,7 @@ use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
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_model::{
@@ -23,10 +24,11 @@ use language_model::{
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
+ Project,
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
-use settings::{Settings, update_settings_file};
+use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
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;
use crate::{
- AddContextServer,
+ AddContextServer, ExternalAgent, NewExternalAgentThread,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
};
@@ -47,6 +49,7 @@ pub struct AgentConfiguration {
fs: Arc,
language_registry: Arc,
workspace: WeakEntity,
+ project: WeakEntity,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap,
context_server_store: Entity,
@@ -56,6 +59,8 @@ pub struct AgentConfiguration {
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
+ gemini_is_installed: bool,
+ _check_for_gemini: Task<()>,
}
impl AgentConfiguration {
@@ -65,6 +70,7 @@ impl AgentConfiguration {
tools: Entity,
language_registry: Arc,
workspace: WeakEntity,
+ project: WeakEntity,
window: &mut Window,
cx: &mut Context,
) -> Self {
@@ -89,6 +95,11 @@ impl AgentConfiguration {
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
.detach();
+ cx.observe_global_in::(window, |this, _, cx| {
+ this.check_for_gemini(cx);
+ cx.notify();
+ })
+ .detach();
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
@@ -97,6 +108,7 @@ impl AgentConfiguration {
fs,
language_registry,
workspace,
+ project,
focus_handle,
configuration_views_by_provider: HashMap::default(),
context_server_store,
@@ -106,8 +118,11 @@ impl AgentConfiguration {
_registry_subscription: registry_subscription,
scroll_handle,
scrollbar_state,
+ gemini_is_installed: false,
+ _check_for_gemini: Task::ready(()),
};
this.build_provider_configuration_views(window, cx);
+ this.check_for_gemini(cx);
this
}
@@ -137,6 +152,34 @@ impl AgentConfiguration {
self.configuration_views_by_provider
.insert(provider.id(), configuration_view);
}
+
+ fn check_for_gemini(&mut self, cx: &mut Context) {
+ 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 {
@@ -211,7 +254,6 @@ impl AgentConfiguration {
.child(
h_flex()
.id(provider_id_string.clone())
- .cursor_pointer()
.px_2()
.py_0p5()
.w_full()
@@ -231,10 +273,7 @@ impl AgentConfiguration {
h_flex()
.w_full()
.gap_1()
- .child(
- Label::new(provider_name.clone())
- .size(LabelSize::Large),
- )
+ .child(Label::new(provider_name.clone()))
.map(|this| {
if is_zed_provider && is_signed_in {
this.child(
@@ -279,7 +318,7 @@ impl AgentConfiguration {
"Start New Thread",
)
.icon_position(IconPosition::Start)
- .icon(IconName::Plus)
+ .icon(IconName::Thread)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
@@ -378,7 +417,7 @@ impl AgentConfiguration {
),
)
.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),
),
),
@@ -519,6 +558,14 @@ impl AgentConfiguration {
}
}
+ fn card_item_bg_color(&self, cx: &mut Context) -> Hsla {
+ cx.theme().colors().background.opacity(0.25)
+ }
+
+ fn card_item_border_color(&self, cx: &mut Context) -> Hsla {
+ cx.theme().colors().border.opacity(0.6)
+ }
+
fn render_context_servers_section(
&mut self,
window: &mut Window,
@@ -536,7 +583,12 @@ impl AgentConfiguration {
v_flex()
.gap_0p5()
.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(
context_server_ids.into_iter().map(|context_server_id| {
@@ -546,7 +598,7 @@ impl AgentConfiguration {
.child(
h_flex()
.justify_between()
- .gap_2()
+ .gap_1p5()
.child(
h_flex().w_full().child(
Button::new("add-context-server", "Add Custom Server")
@@ -637,8 +689,6 @@ impl AgentConfiguration {
.map_or([].as_slice(), |tools| tools.as_slice());
let tool_count = tools.len();
- let border_color = cx.theme().colors().border.opacity(0.6);
-
let (source_icon, source_tooltip) = if is_from_extension {
(
IconName::ZedMcpExtension,
@@ -781,8 +831,8 @@ impl AgentConfiguration {
.id(item_id.clone())
.border_1()
.rounded_md()
- .border_color(border_color)
- .bg(cx.theme().colors().background.opacity(0.2))
+ .border_color(self.card_item_border_color(cx))
+ .bg(self.card_item_bg_color(cx))
.overflow_hidden()
.child(
h_flex()
@@ -790,7 +840,11 @@ impl AgentConfiguration {
.justify_between()
.when(
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(
h_flex()
@@ -972,6 +1026,166 @@ impl AgentConfiguration {
))
})
}
+
+ fn render_agent_servers_section(&mut self, cx: &mut Context) -> 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::>();
+
+ 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,
+ agent: ExternalAgent,
+ install_command: Option,
+ cx: &mut Context,
+ ) -> 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 {
@@ -991,6 +1205,7 @@ impl Render for AgentConfiguration {
.size_full()
.overflow_y_scroll()
.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_provider_configuration_section(cx)),
)
diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs
index e07424987c..1e1ff95178 100644
--- a/crates/agent_ui/src/agent_diff.rs
+++ b/crates/agent_ui/src/agent_diff.rs
@@ -1529,6 +1529,7 @@ impl AgentDiff {
| AcpThreadEvent::TokenUsageUpdated
| AcpThreadEvent::EntriesRemoved(_)
| AcpThreadEvent::ToolAuthorizationRequired
+ | AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::Retry(_) => {}
}
}
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index 50f9fc6a45..269aec3365 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -9,6 +9,8 @@ use agent_servers::AgentServerSettings;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
+use zed_actions::OpenBrowser;
+use zed_actions::agent::ReauthenticateAgent;
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread;
@@ -240,6 +242,7 @@ enum WhichFontSize {
None,
}
+// TODO unify this with ExternalAgent
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType {
#[default]
@@ -1024,6 +1027,8 @@ impl AgentPanel {
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) {
+ telemetry::event!("Agent Thread Started", agent = "zed-text");
+
let context = self
.context_store
.update(cx, |context_store, cx| context_store.create(cx));
@@ -1116,6 +1121,8 @@ impl AgentPanel {
}
};
+ telemetry::event!("Agent Thread Started", agent = ext_agent.name());
+
let server = ext_agent.server(fs, history);
this.update_in(cx, |this, window, cx| {
@@ -1473,6 +1480,7 @@ impl AgentPanel {
tools,
self.language_registry.clone(),
self.workspace.clone(),
+ self.project.downgrade(),
window,
cx,
)
@@ -2204,6 +2212,8 @@ impl AgentPanel {
"Enable Full Screen"
};
+ let selected_agent = self.selected_agent.clone();
+
PopoverMenu::new("agent-options-menu")
.trigger_with_tooltip(
IconButton::new("agent-options-menu", IconName::Ellipsis)
@@ -2283,6 +2293,11 @@ impl AgentPanel {
.action("Settings", Box::new(OpenSettings))
.separator()
.action(full_screen_label, Box::new(ToggleZoom));
+
+ if selected_agent == AgentType::Gemini {
+ menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
+ }
+
menu
}))
}
@@ -2317,6 +2332,8 @@ impl AgentPanel {
.menu({
let menu = self.assistant_navigation_menu.clone();
move |window, cx| {
+ telemetry::event!("View Thread History Clicked");
+
if let Some(menu) = menu.as_ref() {
menu.update(cx, |_, cx| {
cx.defer_in(window, |menu, window, cx| {
@@ -2495,6 +2512,8 @@ impl AgentPanel {
let workspace = self.workspace.clone();
move |window, cx| {
+ telemetry::event!("New Thread Clicked");
+
let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
menu = menu
@@ -2671,6 +2690,15 @@ impl AgentPanel {
}
menu
+ })
+ .when(cx.has_flag::(), |menu| {
+ menu.separator().link(
+ "Add Your Own Agent",
+ OpenBrowser {
+ url: "https://agentclientprotocol.com/".into(),
+ }
+ .boxed_clone(),
+ )
});
menu
}))
@@ -3751,6 +3779,11 @@ impl Render for AgentPanel {
}
}))
.on_action(cx.listener(Self::toggle_burn_mode))
+ .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
+ if let Some(thread_view) = this.active_thread_view() {
+ thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
+ }
+ }))
.child(self.render_toolbar(window, cx))
.children(self.render_onboarding(window, cx))
.map(|parent| match &self.active_view {
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index 40f6c6a2bb..110c432df3 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -160,6 +160,7 @@ pub struct NewNativeAgentThreadFromSummary {
from_session_id: agent_client_protocol::SessionId,
}
+// TODO unify this with AgentType
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum ExternalAgent {
@@ -174,6 +175,15 @@ enum 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(
&self,
fs: Arc,
diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs
index edb672a872..e9e7eba4b6 100644
--- a/crates/agent_ui/src/text_thread_editor.rs
+++ b/crates/agent_ui/src/text_thread_editor.rs
@@ -361,6 +361,7 @@ impl TextThreadEditor {
if self.sending_disabled(cx) {
return;
}
+ telemetry::event!("Agent Message Sent", agent = "zed-text");
self.send_to_model(window, cx);
}
diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs
index e9639ca075..106dcb0aef 100644
--- a/crates/ai_onboarding/src/ai_upsell_card.rs
+++ b/crates/ai_onboarding/src/ai_upsell_card.rs
@@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}
#[derive(IntoElement, RegisterComponent)]
pub struct AiUpsellCard {
- pub sign_in_status: SignInStatus,
- pub sign_in: Arc,
- pub account_too_young: bool,
- pub user_plan: Option,
- pub tab_index: Option,
+ sign_in_status: SignInStatus,
+ sign_in: Arc,
+ account_too_young: bool,
+ user_plan: Option,
+ tab_index: Option,
}
impl AiUpsellCard {
@@ -43,6 +43,11 @@ impl AiUpsellCard {
tab_index: None,
}
}
+
+ pub fn tab_index(mut self, tab_index: Option) -> Self {
+ self.tab_index = tab_index;
+ self
+ }
}
impl RenderOnce for AiUpsellCard {
diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs
index 79e205f205..cc22c9fc09 100644
--- a/crates/assistant_tools/src/fetch_tool.rs
+++ b/crates/assistant_tools/src/fetch_tool.rs
@@ -118,7 +118,7 @@ impl Tool for FetchTool {
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool {
- false
+ true
}
fn may_perform_edits(&self) -> bool {
diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs
index ac2c7a32ab..d1451132ae 100644
--- a/crates/assistant_tools/src/find_path_tool.rs
+++ b/crates/assistant_tools/src/find_path_tool.rs
@@ -435,8 +435,8 @@ mod test {
assert_eq!(
matches,
&[
- PathBuf::from("root/apple/banana/carrot"),
- PathBuf::from("root/apple/bandana/carbonara")
+ PathBuf::from(path!("root/apple/banana/carrot")),
+ PathBuf::from(path!("root/apple/bandana/carbonara"))
]
);
@@ -447,8 +447,8 @@ mod test {
assert_eq!(
matches,
&[
- PathBuf::from("root/apple/banana/carrot"),
- PathBuf::from("root/apple/bandana/carbonara")
+ PathBuf::from(path!("root/apple/banana/carrot")),
+ PathBuf::from(path!("root/apple/bandana/carbonara"))
]
);
}
diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs
index 766ee3b161..a6e984fca6 100644
--- a/crates/assistant_tools/src/read_file_tool.rs
+++ b/crates/assistant_tools/src/read_file_tool.rs
@@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
}
fn icon(&self) -> IconName {
- IconName::ToolRead
+ IconName::ToolSearch
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result {
diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs
index 32582ba941..4f3580da07 100644
--- a/crates/editor/src/element.rs
+++ b/crates/editor/src/element.rs
@@ -74,7 +74,7 @@ use std::{
fmt::{self, Write},
iter, mem,
ops::{Deref, Range},
- path::Path,
+ path::{self, Path},
rc::Rc,
sync::Arc,
time::{Duration, Instant},
@@ -90,8 +90,8 @@ use unicode_segmentation::UnicodeSegmentation;
use util::post_inc;
use util::{RangeExt, ResultExt, debug_panic};
use workspace::{
- CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item,
- notifications::NotifyTaskExt,
+ CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace,
+ item::Item, notifications::NotifyTaskExt,
};
/// Determines what kinds of highlights should be applied to a lines background.
@@ -3603,176 +3603,187 @@ impl EditorElement {
let focus_handle = editor.focus_handle(cx);
let colors = cx.theme().colors();
- let header =
- div()
- .p_1()
- .w_full()
- .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
- .child(
- h_flex()
- .size_full()
- .gap_2()
- .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
- .pl_0p5()
- .pr_5()
- .rounded_sm()
- .when(is_sticky, |el| el.shadow_md())
- .border_1()
- .map(|div| {
- let border_color = if is_selected
- && is_folded
- && focus_handle.contains_focused(window, cx)
- {
- colors.border_focused
- } else {
- colors.border
- };
- div.border_color(border_color)
- })
- .bg(colors.editor_subheader_background)
- .hover(|style| style.bg(colors.element_hover))
- .map(|header| {
- let editor = self.editor.clone();
- let buffer_id = for_excerpt.buffer_id;
- let toggle_chevron_icon =
- FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
- header.child(
- div()
- .hover(|style| style.bg(colors.element_selected))
- .rounded_xs()
- .child(
- ButtonLike::new("toggle-buffer-fold")
- .style(ui::ButtonStyle::Transparent)
- .height(px(28.).into())
- .width(px(28.))
- .children(toggle_chevron_icon)
- .tooltip({
- let focus_handle = focus_handle.clone();
- move |window, cx| {
- Tooltip::with_meta_in(
- "Toggle Excerpt Fold",
- Some(&ToggleFold),
- "Alt+click to toggle all",
- &focus_handle,
+ let header = div()
+ .p_1()
+ .w_full()
+ .h(FILE_HEADER_HEIGHT as f32 * window.line_height())
+ .child(
+ h_flex()
+ .size_full()
+ .gap_2()
+ .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
+ .pl_0p5()
+ .pr_5()
+ .rounded_sm()
+ .when(is_sticky, |el| el.shadow_md())
+ .border_1()
+ .map(|div| {
+ let border_color = if is_selected
+ && is_folded
+ && focus_handle.contains_focused(window, cx)
+ {
+ colors.border_focused
+ } else {
+ colors.border
+ };
+ div.border_color(border_color)
+ })
+ .bg(colors.editor_subheader_background)
+ .hover(|style| style.bg(colors.element_hover))
+ .map(|header| {
+ let editor = self.editor.clone();
+ let buffer_id = for_excerpt.buffer_id;
+ let toggle_chevron_icon =
+ FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
+ header.child(
+ div()
+ .hover(|style| style.bg(colors.element_selected))
+ .rounded_xs()
+ .child(
+ ButtonLike::new("toggle-buffer-fold")
+ .style(ui::ButtonStyle::Transparent)
+ .height(px(28.).into())
+ .width(px(28.))
+ .children(toggle_chevron_icon)
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ Tooltip::with_meta_in(
+ "Toggle Excerpt Fold",
+ Some(&ToggleFold),
+ "Alt+click to toggle all",
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click(move |event, window, cx| {
+ if event.modifiers().alt {
+ // Alt+click toggles all buffers
+ editor.update(cx, |editor, cx| {
+ editor.toggle_fold_all(
+ &ToggleFoldAll,
window,
cx,
- )
- }
- })
- .on_click(move |event, window, cx| {
- if event.modifiers().alt {
- // Alt+click toggles all buffers
+ );
+ });
+ } else {
+ // Regular click toggles single buffer
+ if is_folded {
editor.update(cx, |editor, cx| {
- editor.toggle_fold_all(
- &ToggleFoldAll,
- window,
- cx,
- );
+ editor.unfold_buffer(buffer_id, cx);
});
} else {
- // Regular click toggles single buffer
- if is_folded {
- editor.update(cx, |editor, cx| {
- editor.unfold_buffer(buffer_id, cx);
- });
- } else {
- editor.update(cx, |editor, cx| {
- editor.fold_buffer(buffer_id, cx);
- });
- }
+ editor.update(cx, |editor, cx| {
+ editor.fold_buffer(buffer_id, cx);
+ });
}
- }),
- ),
- )
- })
- .children(
- editor
- .addons
- .values()
- .filter_map(|addon| {
- addon.render_buffer_header_controls(for_excerpt, window, cx)
- })
- .take(1),
+ }
+ }),
+ ),
)
- .child(
- h_flex()
- .size(Pixels(12.0))
- .justify_center()
- .children(indicator),
- )
- .child(
- h_flex()
- .cursor_pointer()
- .id("path header block")
- .size_full()
- .justify_between()
- .overflow_hidden()
- .child(
- h_flex()
- .gap_2()
- .child(
- Label::new(
- filename
- .map(SharedString::from)
- .unwrap_or_else(|| "untitled".into()),
- )
- .single_line()
- .when_some(file_status, |el, status| {
- el.color(if status.is_conflicted() {
- Color::Conflict
- } else if status.is_modified() {
- Color::Modified
- } else if status.is_deleted() {
- Color::Disabled
- } else {
- Color::Created
- })
- .when(status.is_deleted(), |el| el.strikethrough())
- }),
- )
- .when_some(parent_path, |then, path| {
- then.child(div().child(path).text_color(
- if file_status.is_some_and(FileStatus::is_deleted) {
- colors.text_disabled
- } else {
- colors.text_muted
+ })
+ .children(
+ editor
+ .addons
+ .values()
+ .filter_map(|addon| {
+ addon.render_buffer_header_controls(for_excerpt, window, cx)
+ })
+ .take(1),
+ )
+ .child(
+ h_flex()
+ .size(Pixels(12.0))
+ .justify_center()
+ .children(indicator),
+ )
+ .child(
+ h_flex()
+ .cursor_pointer()
+ .id("path header block")
+ .size_full()
+ .justify_between()
+ .overflow_hidden()
+ .child(
+ h_flex()
+ .gap_2()
+ .map(|path_header| {
+ let filename = filename
+ .map(SharedString::from)
+ .unwrap_or_else(|| "untitled".into());
+
+ path_header
+ .when(ItemSettings::get_global(cx).file_icons, |el| {
+ let path = path::Path::new(filename.as_str());
+ let icon = FileIcons::get_icon(path, cx)
+ .unwrap_or_default();
+ let icon =
+ Icon::from_path(icon).color(Color::Muted);
+ el.child(icon)
+ })
+ .child(Label::new(filename).single_line().when_some(
+ file_status,
+ |el, status| {
+ el.color(if status.is_conflicted() {
+ Color::Conflict
+ } else if status.is_modified() {
+ Color::Modified
+ } else if status.is_deleted() {
+ Color::Disabled
+ } else {
+ Color::Created
+ })
+ .when(status.is_deleted(), |el| {
+ el.strikethrough()
+ })
},
))
- }),
- )
- .when(
- can_open_excerpts && is_selected && relative_path.is_some(),
- |el| {
- el.child(
- h_flex()
- .id("jump-to-file-button")
- .gap_2p5()
- .child(Label::new("Jump To File"))
- .children(
- KeyBinding::for_action_in(
- &OpenExcerpts,
- &focus_handle,
- window,
- cx,
- )
- .map(|binding| binding.into_any_element()),
- ),
- )
- },
- )
- .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
- .on_click(window.listener_for(&self.editor, {
- move |editor, e: &ClickEvent, window, cx| {
- editor.open_excerpts_common(
- Some(jump_data.clone()),
- e.modifiers().secondary(),
- window,
- cx,
- );
- }
- })),
- ),
- );
+ })
+ .when_some(parent_path, |then, path| {
+ then.child(div().child(path).text_color(
+ if file_status.is_some_and(FileStatus::is_deleted) {
+ colors.text_disabled
+ } else {
+ colors.text_muted
+ },
+ ))
+ }),
+ )
+ .when(
+ can_open_excerpts && is_selected && relative_path.is_some(),
+ |el| {
+ el.child(
+ h_flex()
+ .id("jump-to-file-button")
+ .gap_2p5()
+ .child(Label::new("Jump To File"))
+ .children(
+ KeyBinding::for_action_in(
+ &OpenExcerpts,
+ &focus_handle,
+ window,
+ cx,
+ )
+ .map(|binding| binding.into_any_element()),
+ ),
+ )
+ },
+ )
+ .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
+ .on_click(window.listener_for(&self.editor, {
+ move |editor, e: &ClickEvent, window, cx| {
+ editor.open_excerpts_common(
+ Some(jump_data.clone()),
+ e.modifiers().secondary(),
+ window,
+ cx,
+ );
+ }
+ })),
+ ),
+ );
let file = for_excerpt.buffer.file().cloned();
let editor = self.editor.clone();
diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs
index 641e8a97ed..b7110190fd 100644
--- a/crates/editor/src/items.rs
+++ b/crates/editor/src/items.rs
@@ -1404,7 +1404,7 @@ impl ProjectItem for Editor {
}
fn for_broken_project_item(
- abs_path: PathBuf,
+ abs_path: &Path,
is_local: bool,
e: &anyhow::Error,
window: &mut Window,
diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs
index 8aaaa04729..7512152324 100644
--- a/crates/file_finder/src/file_finder.rs
+++ b/crates/file_finder/src/file_finder.rs
@@ -1401,13 +1401,16 @@ impl PickerDelegate for FileFinderDelegate {
#[cfg(windows)]
let raw_query = raw_query.trim().to_owned().replace("/", "\\");
#[cfg(not(windows))]
- let raw_query = raw_query.trim().to_owned();
+ let raw_query = raw_query.trim();
- let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query {
+ let raw_query = raw_query.trim_end_matches(':').to_owned();
+ let path = path_position.path.to_str();
+ let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
+ let file_query_end = if path_trimmed == raw_query {
None
} else {
// Safe to unwrap as we won't get here when the unwrap in if fails
- Some(path_position.path.to_str().unwrap().len())
+ Some(path.unwrap().len())
};
let query = FileSearchQuery {
diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs
index 8203d1b1fd..cd0f203d6a 100644
--- a/crates/file_finder/src/file_finder_tests.rs
+++ b/crates/file_finder/src/file_finder_tests.rs
@@ -218,6 +218,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
" ndan ",
" band ",
"a bandana",
+ "bandana:",
] {
picker
.update_in(cx, |picker, window, cx| {
@@ -252,6 +253,53 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
}
}
+#[gpui::test]
+async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/root"),
+ json!({
+ "a": {
+ "foo:bar.rs": "",
+ "foo.rs": "",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
+
+ let (picker, _, cx) = build_find_picker(project, cx);
+
+ // 'foo:' matches both files
+ cx.simulate_input("foo:");
+ picker.update(cx, |picker, _| {
+ assert_eq!(picker.delegate.matches.len(), 3);
+ assert_match_at_position(picker, 0, "foo.rs");
+ assert_match_at_position(picker, 1, "foo:bar.rs");
+ });
+
+ // 'foo:b' matches one of the files
+ cx.simulate_input("b");
+ picker.update(cx, |picker, _| {
+ assert_eq!(picker.delegate.matches.len(), 2);
+ assert_match_at_position(picker, 0, "foo:bar.rs");
+ });
+
+ cx.dispatch_action(editor::actions::Backspace);
+
+ // 'foo:1' matches both files, specifying which row to jump to
+ cx.simulate_input("1");
+ picker.update(cx, |picker, _| {
+ assert_eq!(picker.delegate.matches.len(), 3);
+ assert_match_at_position(picker, 0, "foo.rs");
+ assert_match_at_position(picker, 1, "foo:bar.rs");
+ });
+}
+
#[gpui::test]
async fn test_unicode_paths(cx: &mut TestAppContext) {
let app_state = init_test(cx);
diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs
index e5b9c020d5..f554dea128 100644
--- a/crates/gpui/src/platform/windows/dispatcher.rs
+++ b/crates/gpui/src/platform/windows/dispatcher.rs
@@ -9,10 +9,8 @@ use parking::Parker;
use parking_lot::Mutex;
use util::ResultExt;
use windows::{
- Foundation::TimeSpan,
System::Threading::{
- ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions,
- WorkItemPriority,
+ ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority,
},
Win32::{
Foundation::{LPARAM, WPARAM},
@@ -56,12 +54,7 @@ impl WindowsDispatcher {
Ok(())
})
};
- ThreadPool::RunWithPriorityAndOptionsAsync(
- &handler,
- WorkItemPriority::High,
- WorkItemOptions::TimeSliced,
- )
- .log_err();
+ ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err();
}
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
@@ -72,12 +65,7 @@ impl WindowsDispatcher {
Ok(())
})
};
- let delay = TimeSpan {
- // A time period expressed in 100-nanosecond units.
- // 10,000,000 ticks per second
- Duration: (duration.as_nanos() / 100) as i64,
- };
- ThreadPoolTimer::CreateTimer(&handler, delay).log_err();
+ ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err();
}
}
diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs
index b106110c33..4ddc2b3018 100644
--- a/crates/language/src/buffer.rs
+++ b/crates/language/src/buffer.rs
@@ -1569,11 +1569,21 @@ impl Buffer {
self.send_operation(op, true, cx);
}
- pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> {
- let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else {
- return None;
- };
- Some(&self.diagnostics[idx].1)
+ pub fn buffer_diagnostics(
+ &self,
+ for_server: Option,
+ ) -> Vec<&DiagnosticEntry> {
+ match for_server {
+ Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) {
+ Ok(idx) => self.diagnostics[idx].1.iter().collect(),
+ Err(_) => Vec::new(),
+ },
+ None => self
+ .diagnostics
+ .iter()
+ .flat_map(|(_, diagnostic_set)| diagnostic_set.iter())
+ .collect(),
+ }
}
fn request_autoindent(&mut self, cx: &mut Context) {
diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs
index 43c0365291..d5206c1f26 100644
--- a/crates/language_tools/src/lsp_log.rs
+++ b/crates/language_tools/src/lsp_log.rs
@@ -1743,6 +1743,5 @@ pub enum Event {
}
impl EventEmitter for LogStore {}
-impl EventEmitter for LspLogView {}
impl EventEmitter for LspLogView {}
impl EventEmitter for LspLogView {}
diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm
index 7baba5f227..dbec1937b1 100644
--- a/crates/languages/src/javascript/injections.scm
+++ b/crates/languages/src/javascript/injections.scm
@@ -11,6 +11,21 @@
(#set! injection.language "css"))
)
+(call_expression
+ function: (member_expression
+ object: (identifier) @_obj (#eq? @_obj "styled")
+ property: (property_identifier))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
+(call_expression
+ function: (call_expression
+ function: (identifier) @_name (#eq? @_name "styled"))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
(call_expression
function: (identifier) @_name (#eq? @_name "html")
arguments: (template_string) @injection.content
diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm
index 9c02fbedaa..1c46061827 100644
--- a/crates/languages/src/rust/highlights.scm
+++ b/crates/languages/src/rust/highlights.scm
@@ -6,9 +6,6 @@
(self) @variable.special
(field_identifier) @property
-(shorthand_field_initializer
- (identifier) @property)
-
(trait_item name: (type_identifier) @type.interface)
(impl_item trait: (type_identifier) @type.interface)
(abstract_type trait: (type_identifier) @type.interface)
@@ -41,20 +38,11 @@
(identifier) @function.special
(scoped_identifier
name: (identifier) @function.special)
- ]
- "!" @function.special)
+ ])
(macro_definition
name: (identifier) @function.special.definition)
-(mod_item
- name: (identifier) @module)
-
-(visibility_modifier [
- (crate) @keyword
- (super) @keyword
-])
-
; Identifier conventions
; Assume uppercase names are types/enum-constructors
@@ -127,7 +115,9 @@
"where"
"while"
"yield"
+ (crate)
(mutable_specifier)
+ (super)
] @keyword
[
@@ -199,7 +189,6 @@
operator: "/" @operator
(lifetime) @lifetime
-(lifetime (identifier) @lifetime)
(parameter (identifier) @variable.parameter)
diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm
index 48da80995b..9eec01cc89 100644
--- a/crates/languages/src/tsx/injections.scm
+++ b/crates/languages/src/tsx/injections.scm
@@ -11,6 +11,21 @@
(#set! injection.language "css"))
)
+(call_expression
+ function: (member_expression
+ object: (identifier) @_obj (#eq? @_obj "styled")
+ property: (property_identifier))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
+(call_expression
+ function: (call_expression
+ function: (identifier) @_name (#eq? @_name "styled"))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
(call_expression
function: (identifier) @_name (#eq? @_name "html")
arguments: (template_string (string_fragment) @injection.content
diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm
index 7affdc5b75..1ca1e9ad59 100644
--- a/crates/languages/src/typescript/injections.scm
+++ b/crates/languages/src/typescript/injections.scm
@@ -15,6 +15,21 @@
(#set! injection.language "css"))
)
+(call_expression
+ function: (member_expression
+ object: (identifier) @_obj (#eq? @_obj "styled")
+ property: (property_identifier))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
+(call_expression
+ function: (call_expression
+ function: (identifier) @_name (#eq? @_name "styled"))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
(call_expression
function: (identifier) @_name (#eq? @_name "html")
arguments: (template_string) @injection.content
diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs
index 39a438c512..f16da45d79 100644
--- a/crates/markdown/src/markdown.rs
+++ b/crates/markdown/src/markdown.rs
@@ -1085,10 +1085,10 @@ impl Element for MarkdownElement {
);
el.child(
h_flex()
- .w_5()
+ .w_4()
.absolute()
- .top_1()
- .right_1()
+ .top_1p5()
+ .right_1p5()
.justify_end()
.child(codeblock),
)
@@ -1115,11 +1115,12 @@ impl Element for MarkdownElement {
cx,
);
el.child(
- div()
+ h_flex()
+ .w_4()
.absolute()
.top_0()
.right_0()
- .w_5()
+ .justify_end()
.visible_on_hover("code_block")
.child(codeblock),
)
diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs
index 672bcf1cd9..54c49bc72a 100644
--- a/crates/onboarding/src/ai_setup_page.rs
+++ b/crates/onboarding/src/ai_setup_page.rs
@@ -283,17 +283,13 @@ pub(crate) fn render_ai_setup_page(
v_flex()
.mt_2()
.gap_6()
- .child({
- let mut ai_upsell_card =
- AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx);
-
- ai_upsell_card.tab_index = Some({
- tab_index += 1;
- tab_index - 1
- });
-
- ai_upsell_card
- })
+ .child(
+ AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx)
+ .tab_index(Some({
+ tab_index += 1;
+ tab_index - 1
+ })),
+ )
.child(render_llm_provider_section(
&mut tab_index,
workspace,
diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs
index acf6ec434a..08be82b830 100644
--- a/crates/open_ai/src/open_ai.rs
+++ b/crates/open_ai/src/open_ai.rs
@@ -446,7 +446,6 @@ pub enum ResponseStreamResult {
#[derive(Serialize, Deserialize, Debug)]
pub struct ResponseStreamEvent {
- pub model: String,
pub choices: Vec,
pub usage: Option,
}
diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs
index d2958dce01..deebaedd74 100644
--- a/crates/project/src/lsp_store.rs
+++ b/crates/project/src/lsp_store.rs
@@ -7588,19 +7588,16 @@ impl LspStore {
let snapshot = buffer_handle.read(cx).snapshot();
let buffer = buffer_handle.read(cx);
let reused_diagnostics = buffer
- .get_diagnostics(server_id)
- .into_iter()
- .flat_map(|diag| {
- diag.iter()
- .filter(|v| merge(buffer, &v.diagnostic, cx))
- .map(|v| {
- let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
- let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
- DiagnosticEntry {
- range: start..end,
- diagnostic: v.diagnostic.clone(),
- }
- })
+ .buffer_diagnostics(Some(server_id))
+ .iter()
+ .filter(|v| merge(buffer, &v.diagnostic, cx))
+ .map(|v| {
+ let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
+ let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
+ DiagnosticEntry {
+ range: start..end,
+ diagnostic: v.diagnostic.clone(),
+ }
})
.collect::>();
@@ -11706,12 +11703,11 @@ impl LspStore {
// Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings.
}
"workspace/symbol" => {
- if let Some(options) = parse_register_capabilities(reg)? {
- server.update_capabilities(|capabilities| {
- capabilities.workspace_symbol_provider = Some(options);
- });
- notify_server_capabilities_updated(&server, cx);
- }
+ let options = parse_register_capabilities(reg)?;
+ server.update_capabilities(|capabilities| {
+ capabilities.workspace_symbol_provider = Some(options);
+ });
+ notify_server_capabilities_updated(&server, cx);
}
"workspace/fileOperations" => {
if let Some(options) = reg.register_options {
@@ -11735,12 +11731,11 @@ impl LspStore {
}
}
"textDocument/rangeFormatting" => {
- if let Some(options) = parse_register_capabilities(reg)? {
- server.update_capabilities(|capabilities| {
- capabilities.document_range_formatting_provider = Some(options);
- });
- notify_server_capabilities_updated(&server, cx);
- }
+ let options = parse_register_capabilities(reg)?;
+ server.update_capabilities(|capabilities| {
+ capabilities.document_range_formatting_provider = Some(options);
+ });
+ notify_server_capabilities_updated(&server, cx);
}
"textDocument/onTypeFormatting" => {
if let Some(options) = reg
@@ -11755,36 +11750,32 @@ impl LspStore {
}
}
"textDocument/formatting" => {
- if let Some(options) = parse_register_capabilities(reg)? {
- server.update_capabilities(|capabilities| {
- capabilities.document_formatting_provider = Some(options);
- });
- notify_server_capabilities_updated(&server, cx);
- }
+ let options = parse_register_capabilities(reg)?;
+ server.update_capabilities(|capabilities| {
+ capabilities.document_formatting_provider = Some(options);
+ });
+ notify_server_capabilities_updated(&server, cx);
}
"textDocument/rename" => {
- if let Some(options) = parse_register_capabilities(reg)? {
- server.update_capabilities(|capabilities| {
- capabilities.rename_provider = Some(options);
- });
- notify_server_capabilities_updated(&server, cx);
- }
+ let options = parse_register_capabilities(reg)?;
+ server.update_capabilities(|capabilities| {
+ capabilities.rename_provider = Some(options);
+ });
+ notify_server_capabilities_updated(&server, cx);
}
"textDocument/inlayHint" => {
- if let Some(options) = parse_register_capabilities(reg)? {
- server.update_capabilities(|capabilities| {
- capabilities.inlay_hint_provider = Some(options);
- });
- notify_server_capabilities_updated(&server, cx);
- }
+ let options = parse_register_capabilities(reg)?;
+ server.update_capabilities(|capabilities| {
+ capabilities.inlay_hint_provider = Some(options);
+ });
+ notify_server_capabilities_updated(&server, cx);
}
"textDocument/documentSymbol" => {
- if let Some(options) = parse_register_capabilities(reg)? {
- server.update_capabilities(|capabilities| {
- capabilities.document_symbol_provider = Some(options);
- });
- notify_server_capabilities_updated(&server, cx);
- }
+ let options = parse_register_capabilities(reg)?;
+ server.update_capabilities(|capabilities| {
+ capabilities.document_symbol_provider = Some(options);
+ });
+ notify_server_capabilities_updated(&server, cx);
}
"textDocument/codeAction" => {
if let Some(options) = reg
@@ -11800,12 +11791,11 @@ impl LspStore {
}
}
"textDocument/definition" => {
- if let Some(options) = parse_register_capabilities(reg)? {
- server.update_capabilities(|capabilities| {
- capabilities.definition_provider = Some(options);
- });
- notify_server_capabilities_updated(&server, cx);
- }
+ let options = parse_register_capabilities(reg)?;
+ server.update_capabilities(|capabilities| {
+ capabilities.definition_provider = Some(options);
+ });
+ notify_server_capabilities_updated(&server, cx);
}
"textDocument/completion" => {
if let Some(caps) = reg
@@ -12184,10 +12174,10 @@ impl LspStore {
// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133
fn parse_register_capabilities(
reg: lsp::Registration,
-) -> anyhow::Result |