Merge remote-tracking branch 'origin/main' into thread-view-ui

This commit is contained in:
Danilo Leal 2025-08-24 11:17:42 -03:00
commit 1e07aeb8a9
27 changed files with 508 additions and 215 deletions

2
Cargo.lock generated
View file

@ -403,6 +403,7 @@ dependencies = [
"parking_lot", "parking_lot",
"paths", "paths",
"picker", "picker",
"postage",
"pretty_assertions", "pretty_assertions",
"project", "project",
"prompt_store", "prompt_store",
@ -8467,6 +8468,7 @@ dependencies = [
"theme", "theme",
"ui", "ui",
"util", "util",
"util_macros",
"workspace", "workspace",
"workspace-hack", "workspace-hack",
"zed_actions", "zed_actions",

View file

@ -509,7 +509,7 @@ impl ContentBlock {
"`Image`".into() "`Image`".into()
} }
fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
match self { match self {
ContentBlock::Empty => "", ContentBlock::Empty => "",
ContentBlock::Markdown { markdown } => markdown.read(cx).source(), ContentBlock::Markdown { markdown } => markdown.read(cx).source(),

View file

@ -46,7 +46,7 @@ pub struct AcpConnectionRegistry {
} }
struct ActiveConnection { struct ActiveConnection {
server_name: &'static str, server_name: SharedString,
connection: Weak<acp::ClientSideConnection>, connection: Weak<acp::ClientSideConnection>,
} }
@ -63,12 +63,12 @@ impl AcpConnectionRegistry {
pub fn set_active_connection( pub fn set_active_connection(
&self, &self,
server_name: &'static str, server_name: impl Into<SharedString>,
connection: &Rc<acp::ClientSideConnection>, connection: &Rc<acp::ClientSideConnection>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.active_connection.replace(Some(ActiveConnection { self.active_connection.replace(Some(ActiveConnection {
server_name, server_name: server_name.into(),
connection: Rc::downgrade(connection), connection: Rc::downgrade(connection),
})); }));
cx.notify(); cx.notify();
@ -85,7 +85,7 @@ struct AcpTools {
} }
struct WatchedConnection { struct WatchedConnection {
server_name: &'static str, server_name: SharedString,
messages: Vec<WatchedConnectionMessage>, messages: Vec<WatchedConnectionMessage>,
list_state: ListState, list_state: ListState,
connection: Weak<acp::ClientSideConnection>, connection: Weak<acp::ClientSideConnection>,
@ -142,7 +142,7 @@ impl AcpTools {
}); });
self.watched_connection = Some(WatchedConnection { self.watched_connection = Some(WatchedConnection {
server_name: active_connection.server_name, server_name: active_connection.server_name.clone(),
messages: vec![], messages: vec![],
list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)), list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
connection: active_connection.connection.clone(), connection: active_connection.connection.clone(),
@ -442,7 +442,7 @@ impl Item for AcpTools {
"ACP: {}", "ACP: {}",
self.watched_connection self.watched_connection
.as_ref() .as_ref()
.map_or("Disconnected", |connection| connection.server_name) .map_or("Disconnected", |connection| &connection.server_name)
) )
.into() .into()
} }

View file

@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc};
use agent_servers::AgentServer; use agent_servers::AgentServer;
use anyhow::Result; use anyhow::Result;
use fs::Fs; use fs::Fs;
use gpui::{App, Entity, Task}; use gpui::{App, Entity, SharedString, Task};
use project::Project; use project::Project;
use prompt_store::PromptStore; use prompt_store::PromptStore;
@ -22,16 +22,16 @@ impl NativeAgentServer {
} }
impl AgentServer for NativeAgentServer { impl AgentServer for NativeAgentServer {
fn name(&self) -> &'static str { fn name(&self) -> SharedString {
"Zed Agent" "Zed Agent".into()
} }
fn empty_state_headline(&self) -> &'static str { fn empty_state_headline(&self) -> SharedString {
self.name() self.name()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_message(&self) -> SharedString {
"" "".into()
} }
fn logo(&self) -> ui::IconName { fn logo(&self) -> ui::IconName {

View file

@ -15,7 +15,7 @@ use std::{path::Path, rc::Rc};
use thiserror::Error; use thiserror::Error;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity};
use acp_thread::{AcpThread, AuthRequired, LoadError}; use acp_thread::{AcpThread, AuthRequired, LoadError};
@ -24,7 +24,7 @@ use acp_thread::{AcpThread, AuthRequired, LoadError};
pub struct UnsupportedVersion; pub struct UnsupportedVersion;
pub struct AcpConnection { pub struct AcpConnection {
server_name: &'static str, server_name: SharedString,
connection: Rc<acp::ClientSideConnection>, connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>, sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>, auth_methods: Vec<acp::AuthMethod>,
@ -38,7 +38,7 @@ pub struct AcpSession {
} }
pub async fn connect( pub async fn connect(
server_name: &'static str, server_name: SharedString,
command: AgentServerCommand, command: AgentServerCommand,
root_dir: &Path, root_dir: &Path,
cx: &mut AsyncApp, cx: &mut AsyncApp,
@ -51,7 +51,7 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
impl AcpConnection { impl AcpConnection {
pub async fn stdio( pub async fn stdio(
server_name: &'static str, server_name: SharedString,
command: AgentServerCommand, command: AgentServerCommand,
root_dir: &Path, root_dir: &Path,
cx: &mut AsyncApp, cx: &mut AsyncApp,
@ -121,7 +121,7 @@ impl AcpConnection {
cx.update(|cx| { cx.update(|cx| {
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
registry.set_active_connection(server_name, &connection, cx) registry.set_active_connection(server_name.clone(), &connection, cx)
}); });
})?; })?;
@ -187,7 +187,7 @@ impl AgentConnection for AcpConnection {
let action_log = cx.new(|_| ActionLog::new(project.clone()))?; let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|_cx| { let thread = cx.new(|_cx| {
AcpThread::new( AcpThread::new(
self.server_name, self.server_name.clone(),
self.clone(), self.clone(),
project, project,
action_log, action_log,

View file

@ -1,5 +1,6 @@
mod acp; mod acp;
mod claude; mod claude;
mod custom;
mod gemini; mod gemini;
mod settings; mod settings;
@ -7,6 +8,7 @@ mod settings;
pub mod e2e_tests; pub mod e2e_tests;
pub use claude::*; pub use claude::*;
pub use custom::*;
pub use gemini::*; pub use gemini::*;
pub use settings::*; pub use settings::*;
@ -31,9 +33,9 @@ pub fn init(cx: &mut App) {
pub trait AgentServer: Send { pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName; fn logo(&self) -> ui::IconName;
fn name(&self) -> &'static str; fn name(&self) -> SharedString;
fn empty_state_headline(&self) -> &'static str; fn empty_state_headline(&self) -> SharedString;
fn empty_state_message(&self) -> &'static str; fn empty_state_message(&self) -> SharedString;
fn connect( fn connect(
&self, &self,

View file

@ -30,7 +30,7 @@ use futures::{
io::BufReader, io::BufReader,
select_biased, select_biased,
}; };
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use util::{ResultExt, debug_panic}; use util::{ResultExt, debug_panic};
@ -43,16 +43,16 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
pub struct ClaudeCode; pub struct ClaudeCode;
impl AgentServer for ClaudeCode { impl AgentServer for ClaudeCode {
fn name(&self) -> &'static str { fn name(&self) -> SharedString {
"Claude Code" "Claude Code".into()
} }
fn empty_state_headline(&self) -> &'static str { fn empty_state_headline(&self) -> SharedString {
self.name() self.name()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_message(&self) -> SharedString {
"How can I help you today?" "How can I help you today?".into()
} }
fn logo(&self) -> ui::IconName { fn logo(&self) -> ui::IconName {

View file

@ -0,0 +1,59 @@
use crate::{AgentServerCommand, AgentServerSettings};
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use std::{path::Path, rc::Rc};
use ui::IconName;
/// A generic agent server implementation for custom user-defined agents
pub struct CustomAgentServer {
name: SharedString,
command: AgentServerCommand,
}
impl CustomAgentServer {
pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self {
Self {
name,
command: settings.command.clone(),
}
}
}
impl crate::AgentServer for CustomAgentServer {
fn name(&self) -> SharedString {
self.name.clone()
}
fn logo(&self) -> IconName {
IconName::Terminal
}
fn empty_state_headline(&self) -> SharedString {
"No conversations yet".into()
}
fn empty_state_message(&self) -> SharedString {
format!("Start a conversation with {}", self.name).into()
}
fn connect(
&self,
root_dir: &Path,
_project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let server_name = self.name();
let command = self.command.clone();
let root_dir = root_dir.to_path_buf();
cx.spawn(async move |mut cx| {
crate::acp::connect(server_name, command, &root_dir, &mut cx).await
})
}
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
self
}
}

View file

@ -1,17 +1,15 @@
use crate::AgentServer;
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use crate::AgentServer;
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use util::path; use util::path;
pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext) pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
@ -479,6 +477,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
gemini: Some(crate::AgentServerSettings { gemini: Some(crate::AgentServerSettings {
command: crate::gemini::tests::local_command(), command: crate::gemini::tests::local_command(),
}), }),
custom: collections::HashMap::default(),
}, },
cx, cx,
); );

View file

@ -4,11 +4,10 @@ use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerCommand}; use crate::{AgentServer, AgentServerCommand};
use acp_thread::{AgentConnection, LoadError}; use acp_thread::{AgentConnection, LoadError};
use anyhow::Result; use anyhow::Result;
use gpui::{Entity, Task}; use gpui::{App, Entity, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider; use language_models::provider::google::GoogleLanguageModelProvider;
use project::Project; use project::Project;
use settings::SettingsStore; use settings::SettingsStore;
use ui::App;
use crate::AllAgentServersSettings; use crate::AllAgentServersSettings;
@ -18,16 +17,16 @@ pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp"; const ACP_ARG: &str = "--experimental-acp";
impl AgentServer for Gemini { impl AgentServer for Gemini {
fn name(&self) -> &'static str { fn name(&self) -> SharedString {
"Gemini CLI" "Gemini CLI".into()
} }
fn empty_state_headline(&self) -> &'static str { fn empty_state_headline(&self) -> SharedString {
self.name() self.name()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_message(&self) -> SharedString {
"Ask questions, edit files, run commands" "Ask questions, edit files, run commands".into()
} }
fn logo(&self) -> ui::IconName { fn logo(&self) -> ui::IconName {

View file

@ -1,6 +1,7 @@
use crate::AgentServerCommand; use crate::AgentServerCommand;
use anyhow::Result; use anyhow::Result;
use gpui::App; use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources}; use settings::{Settings, SettingsSources};
@ -13,9 +14,13 @@ pub fn init(cx: &mut App) {
pub struct AllAgentServersSettings { pub struct AllAgentServersSettings {
pub gemini: Option<AgentServerSettings>, pub gemini: Option<AgentServerSettings>,
pub claude: Option<AgentServerSettings>, pub claude: Option<AgentServerSettings>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, AgentServerSettings>,
} }
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] #[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct AgentServerSettings { pub struct AgentServerSettings {
#[serde(flatten)] #[serde(flatten)]
pub command: AgentServerCommand, pub command: AgentServerCommand,
@ -29,13 +34,26 @@ impl settings::Settings for AllAgentServersSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> { fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default(); let mut settings = AllAgentServersSettings::default();
for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() { for AllAgentServersSettings {
gemini,
claude,
custom,
} in sources.defaults_and_customizations()
{
if gemini.is_some() { if gemini.is_some() {
settings.gemini = gemini.clone(); settings.gemini = gemini.clone();
} }
if claude.is_some() { if claude.is_some() {
settings.claude = claude.clone(); settings.claude = claude.clone();
} }
// Merge custom agents
for (name, config) in custom {
// Skip built-in agent names to avoid conflicts
if name != "gemini" && name != "claude" {
settings.custom.insert(name.clone(), config.clone());
}
}
} }
Ok(settings) Ok(settings)

View file

@ -67,6 +67,7 @@ ordered-float.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
paths.workspace = true paths.workspace = true
picker.workspace = true picker.workspace = true
postage.workspace = true
project.workspace = true project.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
proto.workspace = true proto.workspace = true

View file

@ -21,12 +21,13 @@ use futures::{
future::{Shared, join_all}, future::{Shared, join_all},
}; };
use gpui::{ use gpui::{
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle, EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext,
UnderlineStyle, WeakEntity, Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between,
}; };
use language::{Buffer, Language}; use language::{Buffer, Language};
use language_model::LanguageModelImage; use language_model::LanguageModelImage;
use postage::stream::Stream as _;
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree}; use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{PromptId, PromptStore}; use prompt_store::{PromptId, PromptStore};
use rope::Point; use rope::Point;
@ -44,10 +45,10 @@ use std::{
use text::{OffsetRangeExt, ToOffset as _}; use text::{OffsetRangeExt, ToOffset as _};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ use ui::{
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName, ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _,
IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div, LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled,
h_flex, px, TextSize, TintColor, Toggleable, Window, div, h_flex, px,
}; };
use util::{ResultExt, debug_panic}; use util::{ResultExt, debug_panic};
use workspace::{Workspace, notifications::NotifyResultExt as _}; use workspace::{Workspace, notifications::NotifyResultExt as _};
@ -73,6 +74,7 @@ pub enum MessageEditorEvent {
Send, Send,
Cancel, Cancel,
Focus, Focus,
LostFocus,
} }
impl EventEmitter<MessageEditorEvent> for MessageEditor {} impl EventEmitter<MessageEditorEvent> for MessageEditor {}
@ -130,10 +132,14 @@ impl MessageEditor {
editor editor
}); });
cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| { cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
cx.emit(MessageEditorEvent::Focus) cx.emit(MessageEditorEvent::Focus)
}) })
.detach(); .detach();
cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
cx.emit(MessageEditorEvent::LostFocus)
})
.detach();
let mut subscriptions = Vec::new(); let mut subscriptions = Vec::new();
subscriptions.push(cx.subscribe_in(&editor, window, { subscriptions.push(cx.subscribe_in(&editor, window, {
@ -246,7 +252,7 @@ impl MessageEditor {
.buffer_snapshot .buffer_snapshot
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1); .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
let crease_id = if let MentionUri::File { abs_path } = &mention_uri let crease = if let MentionUri::File { abs_path } = &mention_uri
&& let Some(extension) = abs_path.extension() && let Some(extension) = abs_path.extension()
&& let Some(extension) = extension.to_str() && let Some(extension) = extension.to_str()
&& Img::extensions().contains(&extension) && Img::extensions().contains(&extension)
@ -272,29 +278,31 @@ impl MessageEditor {
Ok(image) Ok(image)
}) })
.shared(); .shared();
insert_crease_for_image( insert_crease_for_mention(
*excerpt_id, *excerpt_id,
start, start,
content_len, content_len,
Some(abs_path.as_path().into()), mention_uri.name().into(),
image, IconName::Image.path().into(),
Some(image),
self.editor.clone(), self.editor.clone(),
window, window,
cx, cx,
) )
} else { } else {
crate::context_picker::insert_crease_for_mention( insert_crease_for_mention(
*excerpt_id, *excerpt_id,
start, start,
content_len, content_len,
crease_text, crease_text,
mention_uri.icon_path(cx), mention_uri.icon_path(cx),
None,
self.editor.clone(), self.editor.clone(),
window, window,
cx, cx,
) )
}; };
let Some(crease_id) = crease_id else { let Some((crease_id, tx)) = crease else {
return Task::ready(()); return Task::ready(());
}; };
@ -331,7 +339,9 @@ impl MessageEditor {
// Notify the user if we failed to load the mentioned context // Notify the user if we failed to load the mentioned context
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
if task.await.notify_async_err(cx).is_none() { let result = task.await.notify_async_err(cx);
drop(tx);
if result.is_none() {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.editor.update(cx, |editor, cx| { this.editor.update(cx, |editor, cx| {
// Remove mention // Remove mention
@ -857,12 +867,13 @@ impl MessageEditor {
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len) snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
}); });
let image = Arc::new(image); let image = Arc::new(image);
let Some(crease_id) = insert_crease_for_image( let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id, excerpt_id,
text_anchor, text_anchor,
content_len, content_len,
None.clone(), MentionUri::PastedImage.name().into(),
Task::ready(Ok(image.clone())).shared(), IconName::Image.path().into(),
Some(Task::ready(Ok(image.clone())).shared()),
self.editor.clone(), self.editor.clone(),
window, window,
cx, cx,
@ -877,6 +888,7 @@ impl MessageEditor {
.update(|_, cx| LanguageModelImage::from_image(image, cx)) .update(|_, cx| LanguageModelImage::from_image(image, cx))
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
.await; .await;
drop(tx);
if let Some(image) = image { if let Some(image) = image {
Ok(Mention::Image(MentionImage { Ok(Mention::Image(MentionImage {
data: image.source, data: image.source,
@ -1097,18 +1109,20 @@ impl MessageEditor {
for (range, mention_uri, mention) in mentions { for (range, mention_uri, mention) in mentions {
let anchor = snapshot.anchor_before(range.start); let anchor = snapshot.anchor_before(range.start);
let Some(crease_id) = crate::context_picker::insert_crease_for_mention( let Some((crease_id, tx)) = insert_crease_for_mention(
anchor.excerpt_id, anchor.excerpt_id,
anchor.text_anchor, anchor.text_anchor,
range.end - range.start, range.end - range.start,
mention_uri.name().into(), mention_uri.name().into(),
mention_uri.icon_path(cx), mention_uri.icon_path(cx),
None,
self.editor.clone(), self.editor.clone(),
window, window,
cx, cx,
) else { ) else {
continue; continue;
}; };
drop(tx);
self.mention_set.mentions.insert( self.mention_set.mentions.insert(
crease_id, crease_id,
@ -1160,17 +1174,16 @@ impl MessageEditor {
}) })
} }
pub fn text(&self, cx: &App) -> String {
self.editor.read(cx).text(cx)
}
#[cfg(test)] #[cfg(test)]
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) { pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
editor.set_text(text, window, cx); editor.set_text(text, window, cx);
}); });
} }
#[cfg(test)]
pub fn text(&self, cx: &App) -> String {
self.editor.read(cx).text(cx)
}
} }
fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String { fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
@ -1227,23 +1240,21 @@ impl Render for MessageEditor {
} }
} }
pub(crate) fn insert_crease_for_image( pub(crate) fn insert_crease_for_mention(
excerpt_id: ExcerptId, excerpt_id: ExcerptId,
anchor: text::Anchor, anchor: text::Anchor,
content_len: usize, content_len: usize,
abs_path: Option<Arc<Path>>, crease_label: SharedString,
image: Shared<Task<Result<Arc<Image>, String>>>, crease_icon: SharedString,
// abs_path: Option<Arc<Path>>,
image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
editor: Entity<Editor>, editor: Entity<Editor>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> Option<CreaseId> { ) -> Option<(CreaseId, postage::barrier::Sender)> {
let crease_label = abs_path let (tx, rx) = postage::barrier::channel();
.as_ref()
.and_then(|path| path.file_name())
.map(|name| name.to_string_lossy().to_string().into())
.unwrap_or(SharedString::from("Image"));
editor.update(cx, |editor, cx| { let crease_id = editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?; let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
@ -1252,7 +1263,15 @@ pub(crate) fn insert_crease_for_image(
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len); let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
let placeholder = FoldPlaceholder { let placeholder = FoldPlaceholder {
render: render_image_fold_icon_button(crease_label, image, cx.weak_entity()), render: render_fold_icon_button(
crease_label,
crease_icon,
start..end,
rx,
image,
cx.weak_entity(),
cx,
),
merge_adjacent: false, merge_adjacent: false,
..Default::default() ..Default::default()
}; };
@ -1269,63 +1288,112 @@ pub(crate) fn insert_crease_for_image(
editor.fold_creases(vec![crease], false, window, cx); editor.fold_creases(vec![crease], false, window, cx);
Some(ids[0]) Some(ids[0])
}) })?;
Some((crease_id, tx))
} }
fn render_image_fold_icon_button( fn render_fold_icon_button(
label: SharedString, label: SharedString,
image_task: Shared<Task<Result<Arc<Image>, String>>>, icon: SharedString,
range: Range<Anchor>,
mut loading_finished: postage::barrier::Receiver,
image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
editor: WeakEntity<Editor>, editor: WeakEntity<Editor>,
cx: &mut App,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> { ) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new({ let loading = cx.new(|cx| {
move |fold_id, fold_range, cx| { let loading = cx.spawn(async move |this, cx| {
let is_in_text_selection = editor loading_finished.recv().await;
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx)) this.update(cx, |this: &mut LoadingContext, cx| {
.unwrap_or_default(); this.loading = None;
cx.notify();
ButtonLike::new(fold_id) })
.style(ButtonStyle::Filled) .ok();
.selected_style(ButtonStyle::Tinted(TintColor::Accent)) });
.toggle_state(is_in_text_selection) LoadingContext {
.child( id: cx.entity_id(),
h_flex() label,
.gap_1() icon,
.child( range,
Icon::new(IconName::Image) editor,
.size(IconSize::XSmall) loading: Some(loading),
.color(Color::Muted), image: image_task.clone(),
)
.child(
Label::new(label.clone())
.size(LabelSize::Small)
.buffer_font(cx)
.single_line(),
),
)
.hoverable_tooltip({
let image_task = image_task.clone();
move |_, cx| {
let image = image_task.peek().cloned().transpose().ok().flatten();
let image_task = image_task.clone();
cx.new::<ImageHover>(|cx| ImageHover {
image,
_task: cx.spawn(async move |this, cx| {
if let Ok(image) = image_task.clone().await {
this.update(cx, |this, cx| {
if this.image.replace(image).is_none() {
cx.notify();
}
})
.ok();
}
}),
})
.into()
}
})
.into_any_element()
} }
}) });
Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
}
struct LoadingContext {
id: EntityId,
label: SharedString,
icon: SharedString,
range: Range<Anchor>,
editor: WeakEntity<Editor>,
loading: Option<Task<()>>,
image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
}
impl Render for LoadingContext {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_in_text_selection = self
.editor
.update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
.unwrap_or_default();
ButtonLike::new(("loading-context", self.id))
.style(ButtonStyle::Filled)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_in_text_selection)
.when_some(self.image.clone(), |el, image_task| {
el.hoverable_tooltip(move |_, cx| {
let image = image_task.peek().cloned().transpose().ok().flatten();
let image_task = image_task.clone();
cx.new::<ImageHover>(|cx| ImageHover {
image,
_task: cx.spawn(async move |this, cx| {
if let Ok(image) = image_task.clone().await {
this.update(cx, |this, cx| {
if this.image.replace(image).is_none() {
cx.notify();
}
})
.ok();
}
}),
})
.into()
})
})
.child(
h_flex()
.gap_1()
.child(
Icon::from_path(self.icon.clone())
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(self.label.clone())
.size(LabelSize::Small)
.buffer_font(cx)
.single_line(),
)
.map(|el| {
if self.loading.is_some() {
el.with_animation(
"loading-context-crease",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any()
} else {
el.into_any()
}
}),
)
}
} }
struct ImageHover { struct ImageHover {

View file

@ -274,9 +274,9 @@ pub struct AcpThreadView {
edits_expanded: bool, edits_expanded: bool,
plan_expanded: bool, plan_expanded: bool,
editor_expanded: bool, editor_expanded: bool,
terminal_expanded: bool,
editing_message: Option<usize>, editing_message: Option<usize>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>, prompt_capabilities: Rc<Cell<PromptCapabilities>>,
is_loading_contents: bool,
_cancel_task: Option<Task<()>>, _cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 3], _subscriptions: [Subscription; 3],
} }
@ -385,10 +385,10 @@ impl AcpThreadView {
edits_expanded: false, edits_expanded: false,
plan_expanded: false, plan_expanded: false,
editor_expanded: false, editor_expanded: false,
terminal_expanded: true,
history_store, history_store,
hovered_recent_history_item: None, hovered_recent_history_item: None,
prompt_capabilities, prompt_capabilities,
is_loading_contents: false,
_subscriptions: subscriptions, _subscriptions: subscriptions,
_cancel_task: None, _cancel_task: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
@ -600,7 +600,7 @@ impl AcpThreadView {
let view = registry.read(cx).provider(&provider_id).map(|provider| { let view = registry.read(cx).provider(&provider_id).map(|provider| {
provider.configuration_view( provider.configuration_view(
language_model::ConfigurationViewTargetAgent::Other(agent_name), language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()),
window, window,
cx, cx,
) )
@ -762,6 +762,7 @@ impl AcpThreadView {
MessageEditorEvent::Focus => { MessageEditorEvent::Focus => {
self.cancel_editing(&Default::default(), window, cx); self.cancel_editing(&Default::default(), window, cx);
} }
MessageEditorEvent::LostFocus => {}
} }
} }
@ -793,6 +794,18 @@ impl AcpThreadView {
cx.notify(); cx.notify();
} }
} }
ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
if let Some(thread) = self.thread()
&& let Some(AgentThreadEntry::UserMessage(user_message)) =
thread.read(cx).entries().get(event.entry_index)
&& user_message.id.is_some()
{
if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
self.editing_message = None;
cx.notify();
}
}
}
ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
self.regenerate(event.entry_index, editor, window, cx); self.regenerate(event.entry_index, editor, window, cx);
} }
@ -823,6 +836,11 @@ impl AcpThreadView {
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(thread) = self.thread() else { return }; let Some(thread) = self.thread() else { return };
if self.is_loading_contents {
return;
}
self.history_store.update(cx, |history, cx| { self.history_store.update(cx, |history, cx| {
history.push_recently_opened_entry( history.push_recently_opened_entry(
HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()), HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()),
@ -876,6 +894,15 @@ impl AcpThreadView {
let Some(thread) = self.thread().cloned() else { let Some(thread) = self.thread().cloned() else {
return; return;
}; };
self.is_loading_contents = true;
let guard = cx.new(|_| ());
cx.observe_release(&guard, |this, _guard, cx| {
this.is_loading_contents = false;
cx.notify();
})
.detach();
let task = cx.spawn_in(window, async move |this, cx| { let task = cx.spawn_in(window, async move |this, cx| {
let (contents, tracked_buffers) = contents.await?; let (contents, tracked_buffers) = contents.await?;
@ -896,6 +923,7 @@ impl AcpThreadView {
action_log.buffer_read(buffer, cx) action_log.buffer_read(buffer, cx)
} }
}); });
drop(guard);
thread.send(contents, cx) thread.send(contents, cx)
})?; })?;
send.await send.await
@ -950,19 +978,24 @@ impl AcpThreadView {
let Some(thread) = self.thread().cloned() else { let Some(thread) = self.thread().cloned() else {
return; return;
}; };
if self.is_loading_contents {
return;
}
let Some(rewind) = thread.update(cx, |thread, cx| { let Some(user_message_id) = thread.update(cx, |thread, _| {
let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?; thread.entries().get(entry_ix)?.user_message()?.id.clone()
Some(thread.rewind(user_message_id, cx))
}) else { }) else {
return; return;
}; };
let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx)); let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx));
let task = cx.foreground_executor().spawn(async move { let task = cx.spawn(async move |_, cx| {
rewind.await?; let contents = contents.await?;
contents.await thread
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
.await?;
Ok(contents)
}); });
self.send_impl(task, window, cx); self.send_impl(task, window, cx);
} }
@ -1346,25 +1379,34 @@ impl AcpThreadView {
base_container base_container
.child( .child(
IconButton::new("cancel", IconName::Close) IconButton::new("cancel", IconName::Close)
.disabled(self.is_loading_contents)
.icon_color(Color::Error) .icon_color(Color::Error)
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.on_click(cx.listener(Self::cancel_editing)) .on_click(cx.listener(Self::cancel_editing))
) )
.child( .child(
IconButton::new("regenerate", IconName::Return) if self.is_loading_contents {
.icon_color(Color::Muted) div()
.icon_size(IconSize::XSmall) .id("loading-edited-message-content")
.tooltip(Tooltip::text( .tooltip(Tooltip::text("Loading Added Context…"))
"Editing will restart the thread from this point." .child(loading_contents_spinner(IconSize::XSmall))
)) .into_any_element()
.on_click(cx.listener({ } else {
let editor = editor.clone(); IconButton::new("regenerate", IconName::Return)
move |this, _, window, cx| { .icon_color(Color::Muted)
this.regenerate( .icon_size(IconSize::XSmall)
entry_ix, &editor, window, cx, .tooltip(Tooltip::text(
); "Editing will restart the thread from this point."
} ))
})), .on_click(cx.listener({
let editor = editor.clone();
move |this, _, window, cx| {
this.regenerate(
entry_ix, &editor, window, cx,
);
}
})).into_any_element()
}
) )
) )
} else { } else {
@ -1377,7 +1419,7 @@ impl AcpThreadView {
.icon_color(Color::Muted) .icon_color(Color::Muted)
.style(ButtonStyle::Transparent) .style(ButtonStyle::Transparent)
.tooltip(move |_window, cx| { .tooltip(move |_window, cx| {
cx.new(|_| UnavailableEditingTooltip::new(agent_name.into())) cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone()))
.into() .into()
}) })
) )
@ -1694,7 +1736,9 @@ impl AcpThreadView {
let is_edit = let is_edit =
matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
let use_card_layout = needs_confirmation || is_edit; let use_card_layout = needs_confirmation || is_edit;
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let gradient_overlay = |color: Hsla| { let gradient_overlay = |color: Hsla| {
@ -2165,6 +2209,8 @@ impl AcpThreadView {
.map(|path| format!("{}", path.display())) .map(|path| format!("{}", path.display()))
.unwrap_or_else(|| "current directory".to_string()); .unwrap_or_else(|| "current directory".to_string());
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let header = h_flex() let header = h_flex()
.id(SharedString::from(format!( .id(SharedString::from(format!(
"terminal-tool-header-{}", "terminal-tool-header-{}",
@ -2298,21 +2344,27 @@ impl AcpThreadView {
"terminal-tool-disclosure-{}", "terminal-tool-disclosure-{}",
terminal.entity_id() terminal.entity_id()
)), )),
self.terminal_expanded, is_expanded,
) )
.opened_icon(IconName::ChevronUp) .opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown) .closed_icon(IconName::ChevronDown)
.on_click(cx.listener(move |this, _event, _window, _cx| { .on_click(cx.listener({
this.terminal_expanded = !this.terminal_expanded; let id = tool_call.id.clone();
})), move |this, _event, _window, _cx| {
); if is_expanded {
this.expanded_tool_calls.remove(&id);
} else {
this.expanded_tool_calls.insert(id.clone());
}
}})),
);
let terminal_view = self let terminal_view = self
.entry_view_state .entry_view_state
.read(cx) .read(cx)
.entry(entry_ix) .entry(entry_ix)
.and_then(|entry| entry.terminal(terminal)); .and_then(|entry| entry.terminal(terminal));
let show_output = self.terminal_expanded && terminal_view.is_some(); let show_output = is_expanded && terminal_view.is_some();
v_flex() v_flex()
.mb_2() .mb_2()
@ -3546,7 +3598,14 @@ impl AcpThreadView {
.thread() .thread()
.is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
if is_generating && is_editor_empty { if self.is_loading_contents {
div()
.id("loading-message-content")
.px_1()
.tooltip(Tooltip::text("Loading Added Context…"))
.child(loading_contents_spinner(IconSize::default()))
.into_any_element()
} else if is_generating && is_editor_empty {
IconButton::new("stop-generation", IconName::Stop) IconButton::new("stop-generation", IconName::Stop)
.icon_color(Color::Error) .icon_color(Color::Error)
.style(ButtonStyle::Tinted(ui::TintColor::Error)) .style(ButtonStyle::Tinted(ui::TintColor::Error))
@ -3915,13 +3974,13 @@ impl AcpThreadView {
match AgentSettings::get_global(cx).notify_when_agent_waiting { match AgentSettings::get_global(cx).notify_when_agent_waiting {
NotifyWhenAgentWaiting::PrimaryScreen => { NotifyWhenAgentWaiting::PrimaryScreen => {
if let Some(primary) = cx.primary_display() { if let Some(primary) = cx.primary_display() {
self.pop_up(icon, caption.into(), title.into(), window, primary, cx); self.pop_up(icon, caption.into(), title, window, primary, cx);
} }
} }
NotifyWhenAgentWaiting::AllScreens => { NotifyWhenAgentWaiting::AllScreens => {
let caption = caption.into(); let caption = caption.into();
for screen in cx.displays() { for screen in cx.displays() {
self.pop_up(icon, caption.clone(), title.into(), window, screen, cx); self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
} }
} }
NotifyWhenAgentWaiting::Never => { NotifyWhenAgentWaiting::Never => {
@ -4630,6 +4689,18 @@ impl AcpThreadView {
} }
} }
fn loading_contents_spinner(size: IconSize) -> AnyElement {
Icon::new(IconName::LoadCircle)
.size(size)
.color(Color::Accent)
.with_animation(
"load_context_circle",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
}
impl Focusable for AcpThreadView { impl Focusable for AcpThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle { fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.thread_state { match self.thread_state {
@ -5140,16 +5211,16 @@ pub(crate) mod tests {
ui::IconName::Ai ui::IconName::Ai
} }
fn name(&self) -> &'static str { fn name(&self) -> SharedString {
"Test" "Test".into()
} }
fn empty_state_headline(&self) -> &'static str { fn empty_state_headline(&self) -> SharedString {
"Test" "Test".into()
} }
fn empty_state_message(&self) -> &'static str { fn empty_state_message(&self) -> SharedString {
"Test" "Test".into()
} }
fn connect( fn connect(

View file

@ -5,6 +5,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use acp_thread::AcpThread; use acp_thread::AcpThread;
use agent_servers::AgentServerSettings;
use agent2::{DbThreadMetadata, HistoryEntry}; use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE}; use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -128,7 +129,7 @@ pub fn init(cx: &mut App) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) { if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx); workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.external_thread(action.agent, None, None, window, cx) panel.external_thread(action.agent.clone(), None, None, window, cx)
}); });
} }
}) })
@ -239,7 +240,7 @@ enum WhichFontSize {
None, None,
} }
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType { pub enum AgentType {
#[default] #[default]
Zed, Zed,
@ -247,23 +248,29 @@ pub enum AgentType {
Gemini, Gemini,
ClaudeCode, ClaudeCode,
NativeAgent, NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
},
} }
impl AgentType { impl AgentType {
fn label(self) -> impl Into<SharedString> { fn label(&self) -> SharedString {
match self { match self {
Self::Zed | Self::TextThread => "Zed Agent", Self::Zed | Self::TextThread => "Zed Agent".into(),
Self::NativeAgent => "Agent 2", Self::NativeAgent => "Agent 2".into(),
Self::Gemini => "Gemini CLI", Self::Gemini => "Gemini CLI".into(),
Self::ClaudeCode => "Claude Code", Self::ClaudeCode => "Claude Code".into(),
Self::Custom { name, .. } => name.into(),
} }
} }
fn icon(self) -> Option<IconName> { fn icon(&self) -> Option<IconName> {
match self { match self {
Self::Zed | Self::NativeAgent | Self::TextThread => None, Self::Zed | Self::NativeAgent | Self::TextThread => None,
Self::Gemini => Some(IconName::AiGemini), Self::Gemini => Some(IconName::AiGemini),
Self::ClaudeCode => Some(IconName::AiClaude), Self::ClaudeCode => Some(IconName::AiClaude),
Self::Custom { .. } => Some(IconName::Terminal),
} }
} }
} }
@ -517,7 +524,7 @@ pub struct AgentPanel {
impl AgentPanel { impl AgentPanel {
fn serialize(&mut self, cx: &mut Context<Self>) { fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width; let width = self.width;
let selected_agent = self.selected_agent; let selected_agent = self.selected_agent.clone();
self.pending_serialization = Some(cx.background_spawn(async move { self.pending_serialization = Some(cx.background_spawn(async move {
KEY_VALUE_STORE KEY_VALUE_STORE
.write_kvp( .write_kvp(
@ -607,7 +614,7 @@ impl AgentPanel {
panel.update(cx, |panel, cx| { panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width.map(|w| w.round()); panel.width = serialized_panel.width.map(|w| w.round());
if let Some(selected_agent) = serialized_panel.selected_agent { if let Some(selected_agent) = serialized_panel.selected_agent {
panel.selected_agent = selected_agent; panel.selected_agent = selected_agent.clone();
panel.new_agent_thread(selected_agent, window, cx); panel.new_agent_thread(selected_agent, window, cx);
} }
cx.notify(); cx.notify();
@ -1077,14 +1084,17 @@ impl AgentPanel {
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
let ext_agent = match agent_choice { let ext_agent = match agent_choice {
Some(agent) => { Some(agent) => {
cx.background_spawn(async move { cx.background_spawn({
if let Some(serialized) = let agent = agent.clone();
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err() async move {
{ if let Some(serialized) =
KEY_VALUE_STORE serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized) {
.await KEY_VALUE_STORE
.log_err(); .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
.await
.log_err();
}
} }
}) })
.detach(); .detach();
@ -1110,7 +1120,9 @@ impl AgentPanel {
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
match ext_agent { match ext_agent {
crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => { crate::ExternalAgent::Gemini
| crate::ExternalAgent::NativeAgent
| crate::ExternalAgent::Custom { .. } => {
if !cx.has_flag::<GeminiAndNativeFeatureFlag>() { if !cx.has_flag::<GeminiAndNativeFeatureFlag>() {
return; return;
} }
@ -1839,14 +1851,14 @@ impl AgentPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if self.selected_agent != agent { if self.selected_agent != agent {
self.selected_agent = agent; self.selected_agent = agent.clone();
self.serialize(cx); self.serialize(cx);
} }
self.new_agent_thread(agent, window, cx); self.new_agent_thread(agent, window, cx);
} }
pub fn selected_agent(&self) -> AgentType { pub fn selected_agent(&self) -> AgentType {
self.selected_agent self.selected_agent.clone()
} }
pub fn new_agent_thread( pub fn new_agent_thread(
@ -1885,6 +1897,13 @@ impl AgentPanel {
window, window,
cx, cx,
), ),
AgentType::Custom { name, settings } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, settings }),
None,
None,
window,
cx,
),
} }
} }
@ -2610,13 +2629,55 @@ impl AgentPanel {
} }
}), }),
) )
})
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
// Add custom agents from settings
let settings =
agent_servers::AllAgentServersSettings::get_global(cx);
for (agent_name, agent_settings) in &settings.custom {
menu = menu.item(
ContextMenuEntry::new(format!("New {} Thread", agent_name))
.icon(IconName::Terminal)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
let agent_settings = agent_settings.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.set_selected_agent(
AgentType::Custom {
name: agent_name
.clone(),
settings:
agent_settings
.clone(),
},
window,
cx,
);
});
}
});
}
}
}),
);
}
menu
}); });
menu menu
})) }))
} }
}); });
let selected_agent_label = self.selected_agent.label().into(); let selected_agent_label = self.selected_agent.label();
let selected_agent = div() let selected_agent = div()
.id("selected_agent_icon") .id("selected_agent_icon")
.when_some(self.selected_agent.icon(), |this, icon| { .when_some(self.selected_agent.icon(), |this, icon| {

View file

@ -28,13 +28,14 @@ use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use agent::{Thread, ThreadId}; use agent::{Thread, ThreadId};
use agent_servers::AgentServerSettings;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry; use assistant_slash_command::SlashCommandRegistry;
use client::Client; use client::Client;
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _; use feature_flags::FeatureFlagAppExt as _;
use fs::Fs; use fs::Fs;
use gpui::{Action, App, Entity, actions}; use gpui::{Action, App, Entity, SharedString, actions};
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
@ -159,13 +160,17 @@ pub struct NewNativeAgentThreadFromSummary {
from_session_id: agent_client_protocol::SessionId, from_session_id: agent_client_protocol::SessionId,
} }
#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
enum ExternalAgent { enum ExternalAgent {
#[default] #[default]
Gemini, Gemini,
ClaudeCode, ClaudeCode,
NativeAgent, NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
},
} }
impl ExternalAgent { impl ExternalAgent {
@ -175,9 +180,13 @@ impl ExternalAgent {
history: Entity<agent2::HistoryStore>, history: Entity<agent2::HistoryStore>,
) -> Rc<dyn agent_servers::AgentServer> { ) -> Rc<dyn agent_servers::AgentServer> {
match self { match self {
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), Self::Gemini => Rc::new(agent_servers::Gemini),
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
name.clone(),
settings,
)),
} }
} }
} }

View file

@ -24,6 +24,7 @@ serde_json_lenient.workspace = true
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
util_macros.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
workspace.workspace = true workspace.workspace = true
zed_actions.workspace = true zed_actions.workspace = true

View file

@ -25,7 +25,7 @@ use util::split_str_with_ranges;
/// Path used for unsaved buffer that contains style json. To support the json language server, this /// Path used for unsaved buffer that contains style json. To support the json language server, this
/// matches the name used in the generated schemas. /// matches the name used in the generated schemas.
const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json"; const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json");
pub(crate) struct DivInspector { pub(crate) struct DivInspector {
state: State, state: State,

View file

@ -643,11 +643,11 @@ pub trait LanguageModelProvider: 'static {
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>; fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
} }
#[derive(Default, Clone, Copy)] #[derive(Default, Clone)]
pub enum ConfigurationViewTargetAgent { pub enum ConfigurationViewTargetAgent {
#[default] #[default]
ZedAgent, ZedAgent,
Other(&'static str), Other(SharedString),
} }
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]

View file

@ -1041,9 +1041,9 @@ impl Render for ConfigurationView {
v_flex() v_flex()
.size_full() .size_full()
.on_action(cx.listener(Self::save_api_key)) .on_action(cx.listener(Self::save_api_key))
.child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic", ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(),
ConfigurationViewTargetAgent::Other(agent) => agent, ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
}))) })))
.child( .child(
List::new() List::new()

View file

@ -921,9 +921,9 @@ impl Render for ConfigurationView {
v_flex() v_flex()
.size_full() .size_full()
.on_action(cx.listener(Self::save_api_key)) .on_action(cx.listener(Self::save_api_key))
.child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent { .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI", ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI".into(),
ConfigurationViewTargetAgent::Other(agent) => agent, ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
}))) })))
.child( .child(
List::new() List::new()

View file

@ -231,6 +231,7 @@
"implements" "implements"
"interface" "interface"
"keyof" "keyof"
"module"
"namespace" "namespace"
"private" "private"
"protected" "protected"
@ -250,4 +251,4 @@
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx) (jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
(jsx_attribute "=" @punctuation.delimiter.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx)
(jsx_text) @text.jsx (jsx_text) @text.jsx

View file

@ -237,6 +237,7 @@
"implements" "implements"
"interface" "interface"
"keyof" "keyof"
"module"
"namespace" "namespace"
"private" "private"
"protected" "protected"
@ -256,4 +257,4 @@
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx) (jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
(jsx_attribute "=" @punctuation.delimiter.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx)
(jsx_text) @text.jsx (jsx_text) @text.jsx

View file

@ -248,6 +248,7 @@
"is" "is"
"keyof" "keyof"
"let" "let"
"module"
"namespace" "namespace"
"new" "new"
"of" "of"
@ -272,4 +273,4 @@
"while" "while"
"with" "with"
"yield" "yield"
] @keyword ] @keyword

View file

@ -835,7 +835,7 @@ impl MultiBuffer {
this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns); this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
drop(snapshot); drop(snapshot);
let mut buffer_ids = Vec::new(); let mut buffer_ids = Vec::with_capacity(buffer_edits.len());
for (buffer_id, mut edits) in buffer_edits { for (buffer_id, mut edits) in buffer_edits {
buffer_ids.push(buffer_id); buffer_ids.push(buffer_id);
edits.sort_by_key(|edit| edit.range.start); edits.sort_by_key(|edit| edit.range.start);

View file

@ -11913,7 +11913,7 @@ impl LspStore {
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
} }
} }
"textDocument/colorProvider" => { "textDocument/documentColor" => {
if let Some(caps) = reg if let Some(caps) = reg
.register_options .register_options
.map(serde_json::from_value) .map(serde_json::from_value)
@ -12064,7 +12064,7 @@ impl LspStore {
}); });
notify_server_capabilities_updated(&server, cx); notify_server_capabilities_updated(&server, cx);
} }
"textDocument/colorProvider" => { "textDocument/documentColor" => {
server.update_capabilities(|capabilities| { server.update_capabilities(|capabilities| {
capabilities.color_provider = None; capabilities.color_provider = None;
}); });

View file

@ -51,7 +51,7 @@ To configure, use
```json5 ```json5
"project_panel": { "project_panel": {
"diagnostics": "all", "show_diagnostics": "all",
} }
``` ```