Merge remote-tracking branch 'origin/main' into thread-view-ui
This commit is contained in:
commit
1e07aeb8a9
27 changed files with 508 additions and 215 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -403,6 +403,7 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"paths",
|
||||
"picker",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
|
@ -8467,6 +8468,7 @@ dependencies = [
|
|||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"util_macros",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
|
|
|
@ -509,7 +509,7 @@ impl ContentBlock {
|
|||
"`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 {
|
||||
ContentBlock::Empty => "",
|
||||
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
|
||||
|
|
|
@ -46,7 +46,7 @@ pub struct AcpConnectionRegistry {
|
|||
}
|
||||
|
||||
struct ActiveConnection {
|
||||
server_name: &'static str,
|
||||
server_name: SharedString,
|
||||
connection: Weak<acp::ClientSideConnection>,
|
||||
}
|
||||
|
||||
|
@ -63,12 +63,12 @@ impl AcpConnectionRegistry {
|
|||
|
||||
pub fn set_active_connection(
|
||||
&self,
|
||||
server_name: &'static str,
|
||||
server_name: impl Into<SharedString>,
|
||||
connection: &Rc<acp::ClientSideConnection>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.active_connection.replace(Some(ActiveConnection {
|
||||
server_name,
|
||||
server_name: server_name.into(),
|
||||
connection: Rc::downgrade(connection),
|
||||
}));
|
||||
cx.notify();
|
||||
|
@ -85,7 +85,7 @@ struct AcpTools {
|
|||
}
|
||||
|
||||
struct WatchedConnection {
|
||||
server_name: &'static str,
|
||||
server_name: SharedString,
|
||||
messages: Vec<WatchedConnectionMessage>,
|
||||
list_state: ListState,
|
||||
connection: Weak<acp::ClientSideConnection>,
|
||||
|
@ -142,7 +142,7 @@ impl AcpTools {
|
|||
});
|
||||
|
||||
self.watched_connection = Some(WatchedConnection {
|
||||
server_name: active_connection.server_name,
|
||||
server_name: active_connection.server_name.clone(),
|
||||
messages: vec![],
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
|
||||
connection: active_connection.connection.clone(),
|
||||
|
@ -442,7 +442,7 @@ impl Item for AcpTools {
|
|||
"ACP: {}",
|
||||
self.watched_connection
|
||||
.as_ref()
|
||||
.map_or("Disconnected", |connection| connection.server_name)
|
||||
.map_or("Disconnected", |connection| &connection.server_name)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc};
|
|||
use agent_servers::AgentServer;
|
||||
use anyhow::Result;
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, Task};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
|
||||
|
@ -22,16 +22,16 @@ impl NativeAgentServer {
|
|||
}
|
||||
|
||||
impl AgentServer for NativeAgentServer {
|
||||
fn name(&self) -> &'static str {
|
||||
"Zed Agent"
|
||||
fn name(&self) -> SharedString {
|
||||
"Zed Agent".into()
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
fn empty_state_headline(&self) -> SharedString {
|
||||
self.name()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
""
|
||||
fn empty_state_message(&self) -> SharedString {
|
||||
"".into()
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
|
|
|
@ -15,7 +15,7 @@ use std::{path::Path, rc::Rc};
|
|||
use thiserror::Error;
|
||||
|
||||
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};
|
||||
|
||||
|
@ -24,7 +24,7 @@ use acp_thread::{AcpThread, AuthRequired, LoadError};
|
|||
pub struct UnsupportedVersion;
|
||||
|
||||
pub struct AcpConnection {
|
||||
server_name: &'static str,
|
||||
server_name: SharedString,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
|
@ -38,7 +38,7 @@ pub struct AcpSession {
|
|||
}
|
||||
|
||||
pub async fn connect(
|
||||
server_name: &'static str,
|
||||
server_name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
|
@ -51,7 +51,7 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
|
|||
|
||||
impl AcpConnection {
|
||||
pub async fn stdio(
|
||||
server_name: &'static str,
|
||||
server_name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
cx: &mut AsyncApp,
|
||||
|
@ -121,7 +121,7 @@ impl AcpConnection {
|
|||
|
||||
cx.update(|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 thread = cx.new(|_cx| {
|
||||
AcpThread::new(
|
||||
self.server_name,
|
||||
self.server_name.clone(),
|
||||
self.clone(),
|
||||
project,
|
||||
action_log,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod acp;
|
||||
mod claude;
|
||||
mod custom;
|
||||
mod gemini;
|
||||
mod settings;
|
||||
|
||||
|
@ -7,6 +8,7 @@ mod settings;
|
|||
pub mod e2e_tests;
|
||||
|
||||
pub use claude::*;
|
||||
pub use custom::*;
|
||||
pub use gemini::*;
|
||||
pub use settings::*;
|
||||
|
||||
|
@ -31,9 +33,9 @@ pub fn init(cx: &mut App) {
|
|||
|
||||
pub trait AgentServer: Send {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> &'static str;
|
||||
fn empty_state_headline(&self) -> &'static str;
|
||||
fn empty_state_message(&self) -> &'static str;
|
||||
fn name(&self) -> SharedString;
|
||||
fn empty_state_headline(&self) -> SharedString;
|
||||
fn empty_state_message(&self) -> SharedString;
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
|
|
|
@ -30,7 +30,7 @@ use futures::{
|
|||
io::BufReader,
|
||||
select_biased,
|
||||
};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{ResultExt, debug_panic};
|
||||
|
||||
|
@ -43,16 +43,16 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
|
|||
pub struct ClaudeCode;
|
||||
|
||||
impl AgentServer for ClaudeCode {
|
||||
fn name(&self) -> &'static str {
|
||||
"Claude Code"
|
||||
fn name(&self) -> SharedString {
|
||||
"Claude Code".into()
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
fn empty_state_headline(&self) -> SharedString {
|
||||
self.name()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"How can I help you today?"
|
||||
fn empty_state_message(&self) -> SharedString {
|
||||
"How can I help you today?".into()
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
|
|
59
crates/agent_servers/src/custom.rs
Normal file
59
crates/agent_servers/src/custom.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
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;
|
||||
|
||||
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 {
|
||||
command: crate::gemini::tests::local_command(),
|
||||
}),
|
||||
custom: collections::HashMap::default(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
|
|
@ -4,11 +4,10 @@ use std::{any::Any, path::Path};
|
|||
use crate::{AgentServer, AgentServerCommand};
|
||||
use acp_thread::{AgentConnection, LoadError};
|
||||
use anyhow::Result;
|
||||
use gpui::{Entity, Task};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language_models::provider::google::GoogleLanguageModelProvider;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
use ui::App;
|
||||
|
||||
use crate::AllAgentServersSettings;
|
||||
|
||||
|
@ -18,16 +17,16 @@ pub struct Gemini;
|
|||
const ACP_ARG: &str = "--experimental-acp";
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
fn name(&self) -> &'static str {
|
||||
"Gemini CLI"
|
||||
fn name(&self) -> SharedString {
|
||||
"Gemini CLI".into()
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
fn empty_state_headline(&self) -> SharedString {
|
||||
self.name()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"Ask questions, edit files, run commands"
|
||||
fn empty_state_message(&self) -> SharedString {
|
||||
"Ask questions, edit files, run commands".into()
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::AgentServerCommand;
|
||||
use anyhow::Result;
|
||||
use gpui::App;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
@ -13,9 +14,13 @@ pub fn init(cx: &mut App) {
|
|||
pub struct AllAgentServersSettings {
|
||||
pub gemini: 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 {
|
||||
#[serde(flatten)]
|
||||
pub command: AgentServerCommand,
|
||||
|
@ -29,13 +34,26 @@ impl settings::Settings for AllAgentServersSettings {
|
|||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
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() {
|
||||
settings.gemini = gemini.clone();
|
||||
}
|
||||
if claude.is_some() {
|
||||
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)
|
||||
|
|
|
@ -67,6 +67,7 @@ ordered-float.workspace = true
|
|||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
|
|
|
@ -21,12 +21,13 @@ use futures::{
|
|||
future::{Shared, join_all},
|
||||
};
|
||||
use gpui::{
|
||||
AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
HighlightStyle, Image, ImageFormat, Img, KeyContext, Subscription, Task, TextStyle,
|
||||
UnderlineStyle, WeakEntity,
|
||||
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
|
||||
EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext,
|
||||
Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language_model::LanguageModelImage;
|
||||
use postage::stream::Stream as _;
|
||||
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
|
@ -44,10 +45,10 @@ use std::{
|
|||
use text::{OffsetRangeExt, ToOffset as _};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName,
|
||||
IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
|
||||
Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div,
|
||||
h_flex, px,
|
||||
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _,
|
||||
FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
|
||||
LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled,
|
||||
TextSize, TintColor, Toggleable, Window, div, h_flex, px,
|
||||
};
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
||||
|
@ -73,6 +74,7 @@ pub enum MessageEditorEvent {
|
|||
Send,
|
||||
Cancel,
|
||||
Focus,
|
||||
LostFocus,
|
||||
}
|
||||
|
||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
|
@ -130,10 +132,14 @@ impl MessageEditor {
|
|||
editor
|
||||
});
|
||||
|
||||
cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| {
|
||||
cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
|
||||
cx.emit(MessageEditorEvent::Focus)
|
||||
})
|
||||
.detach();
|
||||
cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
|
||||
cx.emit(MessageEditorEvent::LostFocus)
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut subscriptions = Vec::new();
|
||||
subscriptions.push(cx.subscribe_in(&editor, window, {
|
||||
|
@ -246,7 +252,7 @@ impl MessageEditor {
|
|||
.buffer_snapshot
|
||||
.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) = extension.to_str()
|
||||
&& Img::extensions().contains(&extension)
|
||||
|
@ -272,29 +278,31 @@ impl MessageEditor {
|
|||
Ok(image)
|
||||
})
|
||||
.shared();
|
||||
insert_crease_for_image(
|
||||
insert_crease_for_mention(
|
||||
*excerpt_id,
|
||||
start,
|
||||
content_len,
|
||||
Some(abs_path.as_path().into()),
|
||||
image,
|
||||
mention_uri.name().into(),
|
||||
IconName::Image.path().into(),
|
||||
Some(image),
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
crate::context_picker::insert_crease_for_mention(
|
||||
insert_crease_for_mention(
|
||||
*excerpt_id,
|
||||
start,
|
||||
content_len,
|
||||
crease_text,
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
};
|
||||
let Some(crease_id) = crease_id else {
|
||||
let Some((crease_id, tx)) = crease else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
|
@ -331,7 +339,9 @@ impl MessageEditor {
|
|||
|
||||
// Notify the user if we failed to load the mentioned context
|
||||
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.editor.update(cx, |editor, cx| {
|
||||
// Remove mention
|
||||
|
@ -857,12 +867,13 @@ impl MessageEditor {
|
|||
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
|
||||
});
|
||||
let image = Arc::new(image);
|
||||
let Some(crease_id) = insert_crease_for_image(
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
None.clone(),
|
||||
Task::ready(Ok(image.clone())).shared(),
|
||||
MentionUri::PastedImage.name().into(),
|
||||
IconName::Image.path().into(),
|
||||
Some(Task::ready(Ok(image.clone())).shared()),
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
|
@ -877,6 +888,7 @@ impl MessageEditor {
|
|||
.update(|_, cx| LanguageModelImage::from_image(image, cx))
|
||||
.map_err(|e| e.to_string())?
|
||||
.await;
|
||||
drop(tx);
|
||||
if let Some(image) = image {
|
||||
Ok(Mention::Image(MentionImage {
|
||||
data: image.source,
|
||||
|
@ -1097,18 +1109,20 @@ impl MessageEditor {
|
|||
|
||||
for (range, mention_uri, mention) in mentions {
|
||||
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.text_anchor,
|
||||
range.end - range.start,
|
||||
mention_uri.name().into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
self.mention_set.mentions.insert(
|
||||
crease_id,
|
||||
|
@ -1160,17 +1174,16 @@ impl MessageEditor {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn text(&self, cx: &App) -> String {
|
||||
self.editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, 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 {
|
||||
|
@ -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,
|
||||
anchor: text::Anchor,
|
||||
content_len: usize,
|
||||
abs_path: Option<Arc<Path>>,
|
||||
image: Shared<Task<Result<Arc<Image>, String>>>,
|
||||
crease_label: SharedString,
|
||||
crease_icon: SharedString,
|
||||
// abs_path: Option<Arc<Path>>,
|
||||
image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
|
||||
editor: Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<CreaseId> {
|
||||
let crease_label = abs_path
|
||||
.as_ref()
|
||||
.and_then(|path| path.file_name())
|
||||
.map(|name| name.to_string_lossy().to_string().into())
|
||||
.unwrap_or(SharedString::from("Image"));
|
||||
) -> Option<(CreaseId, postage::barrier::Sender)> {
|
||||
let (tx, rx) = postage::barrier::channel();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let crease_id = editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
|
||||
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 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,
|
||||
..Default::default()
|
||||
};
|
||||
|
@ -1269,63 +1288,112 @@ pub(crate) fn insert_crease_for_image(
|
|||
editor.fold_creases(vec![crease], false, window, cx);
|
||||
|
||||
Some(ids[0])
|
||||
})
|
||||
})?;
|
||||
|
||||
Some((crease_id, tx))
|
||||
}
|
||||
|
||||
fn render_image_fold_icon_button(
|
||||
fn render_fold_icon_button(
|
||||
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>,
|
||||
cx: &mut App,
|
||||
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
|
||||
Arc::new({
|
||||
move |fold_id, fold_range, cx| {
|
||||
let is_in_text_selection = editor
|
||||
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
|
||||
.unwrap_or_default();
|
||||
|
||||
ButtonLike::new(fold_id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(is_in_text_selection)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::Image)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.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()
|
||||
let loading = cx.new(|cx| {
|
||||
let loading = cx.spawn(async move |this, cx| {
|
||||
loading_finished.recv().await;
|
||||
this.update(cx, |this: &mut LoadingContext, cx| {
|
||||
this.loading = None;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
LoadingContext {
|
||||
id: cx.entity_id(),
|
||||
label,
|
||||
icon,
|
||||
range,
|
||||
editor,
|
||||
loading: Some(loading),
|
||||
image: image_task.clone(),
|
||||
}
|
||||
})
|
||||
});
|
||||
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 {
|
||||
|
|
|
@ -274,9 +274,9 @@ pub struct AcpThreadView {
|
|||
edits_expanded: bool,
|
||||
plan_expanded: bool,
|
||||
editor_expanded: bool,
|
||||
terminal_expanded: bool,
|
||||
editing_message: Option<usize>,
|
||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||
is_loading_contents: bool,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
}
|
||||
|
@ -385,10 +385,10 @@ impl AcpThreadView {
|
|||
edits_expanded: false,
|
||||
plan_expanded: false,
|
||||
editor_expanded: false,
|
||||
terminal_expanded: true,
|
||||
history_store,
|
||||
hovered_recent_history_item: None,
|
||||
prompt_capabilities,
|
||||
is_loading_contents: false,
|
||||
_subscriptions: subscriptions,
|
||||
_cancel_task: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
|
@ -600,7 +600,7 @@ impl AcpThreadView {
|
|||
|
||||
let view = registry.read(cx).provider(&provider_id).map(|provider| {
|
||||
provider.configuration_view(
|
||||
language_model::ConfigurationViewTargetAgent::Other(agent_name),
|
||||
language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
@ -762,6 +762,7 @@ impl AcpThreadView {
|
|||
MessageEditorEvent::Focus => {
|
||||
self.cancel_editing(&Default::default(), window, cx);
|
||||
}
|
||||
MessageEditorEvent::LostFocus => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -793,6 +794,18 @@ impl AcpThreadView {
|
|||
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) => {
|
||||
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>) {
|
||||
let Some(thread) = self.thread() else { return };
|
||||
|
||||
if self.is_loading_contents {
|
||||
return;
|
||||
}
|
||||
|
||||
self.history_store.update(cx, |history, cx| {
|
||||
history.push_recently_opened_entry(
|
||||
HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()),
|
||||
|
@ -876,6 +894,15 @@ impl AcpThreadView {
|
|||
let Some(thread) = self.thread().cloned() else {
|
||||
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 (contents, tracked_buffers) = contents.await?;
|
||||
|
||||
|
@ -896,6 +923,7 @@ impl AcpThreadView {
|
|||
action_log.buffer_read(buffer, cx)
|
||||
}
|
||||
});
|
||||
drop(guard);
|
||||
thread.send(contents, cx)
|
||||
})?;
|
||||
send.await
|
||||
|
@ -950,19 +978,24 @@ impl AcpThreadView {
|
|||
let Some(thread) = self.thread().cloned() else {
|
||||
return;
|
||||
};
|
||||
if self.is_loading_contents {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(rewind) = thread.update(cx, |thread, cx| {
|
||||
let user_message_id = thread.entries().get(entry_ix)?.user_message()?.id.clone()?;
|
||||
Some(thread.rewind(user_message_id, cx))
|
||||
let Some(user_message_id) = thread.update(cx, |thread, _| {
|
||||
thread.entries().get(entry_ix)?.user_message()?.id.clone()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let contents = message_editor.update(cx, |message_editor, cx| message_editor.contents(cx));
|
||||
|
||||
let task = cx.foreground_executor().spawn(async move {
|
||||
rewind.await?;
|
||||
contents.await
|
||||
let task = cx.spawn(async move |_, cx| {
|
||||
let contents = contents.await?;
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
|
||||
.await?;
|
||||
Ok(contents)
|
||||
});
|
||||
self.send_impl(task, window, cx);
|
||||
}
|
||||
|
@ -1346,25 +1379,34 @@ impl AcpThreadView {
|
|||
base_container
|
||||
.child(
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.disabled(self.is_loading_contents)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(cx.listener(Self::cancel_editing))
|
||||
)
|
||||
.child(
|
||||
IconButton::new("regenerate", IconName::Return)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.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,
|
||||
);
|
||||
}
|
||||
})),
|
||||
if self.is_loading_contents {
|
||||
div()
|
||||
.id("loading-edited-message-content")
|
||||
.tooltip(Tooltip::text("Loading Added Context…"))
|
||||
.child(loading_contents_spinner(IconSize::XSmall))
|
||||
.into_any_element()
|
||||
} else {
|
||||
IconButton::new("regenerate", IconName::Return)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.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 {
|
||||
|
@ -1377,7 +1419,7 @@ impl AcpThreadView {
|
|||
.icon_color(Color::Muted)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.tooltip(move |_window, cx| {
|
||||
cx.new(|_| UnavailableEditingTooltip::new(agent_name.into()))
|
||||
cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone()))
|
||||
.into()
|
||||
})
|
||||
)
|
||||
|
@ -1694,7 +1736,9 @@ impl AcpThreadView {
|
|||
let is_edit =
|
||||
matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some();
|
||||
let use_card_layout = needs_confirmation || is_edit;
|
||||
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
|
||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let gradient_overlay = |color: Hsla| {
|
||||
|
@ -2165,6 +2209,8 @@ impl AcpThreadView {
|
|||
.map(|path| format!("{}", path.display()))
|
||||
.unwrap_or_else(|| "current directory".to_string());
|
||||
|
||||
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let header = h_flex()
|
||||
.id(SharedString::from(format!(
|
||||
"terminal-tool-header-{}",
|
||||
|
@ -2298,21 +2344,27 @@ impl AcpThreadView {
|
|||
"terminal-tool-disclosure-{}",
|
||||
terminal.entity_id()
|
||||
)),
|
||||
self.terminal_expanded,
|
||||
is_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.terminal_expanded = !this.terminal_expanded;
|
||||
})),
|
||||
);
|
||||
.on_click(cx.listener({
|
||||
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
|
||||
.entry_view_state
|
||||
.read(cx)
|
||||
.entry(entry_ix)
|
||||
.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()
|
||||
.mb_2()
|
||||
|
@ -3546,7 +3598,14 @@ impl AcpThreadView {
|
|||
.thread()
|
||||
.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)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
|
@ -3915,13 +3974,13 @@ impl AcpThreadView {
|
|||
match AgentSettings::get_global(cx).notify_when_agent_waiting {
|
||||
NotifyWhenAgentWaiting::PrimaryScreen => {
|
||||
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 => {
|
||||
let caption = caption.into();
|
||||
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 => {
|
||||
|
@ -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 {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match self.thread_state {
|
||||
|
@ -5140,16 +5211,16 @@ pub(crate) mod tests {
|
|||
ui::IconName::Ai
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"Test"
|
||||
fn name(&self) -> SharedString {
|
||||
"Test".into()
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
"Test"
|
||||
fn empty_state_headline(&self) -> SharedString {
|
||||
"Test".into()
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"Test"
|
||||
fn empty_state_message(&self) -> SharedString {
|
||||
"Test".into()
|
||||
}
|
||||
|
||||
fn connect(
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
|
||||
use acp_thread::AcpThread;
|
||||
use agent_servers::AgentServerSettings;
|
||||
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -128,7 +129,7 @@ pub fn init(cx: &mut App) {
|
|||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, 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,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum AgentType {
|
||||
#[default]
|
||||
Zed,
|
||||
|
@ -247,23 +248,29 @@ pub enum AgentType {
|
|||
Gemini,
|
||||
ClaudeCode,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
settings: AgentServerSettings,
|
||||
},
|
||||
}
|
||||
|
||||
impl AgentType {
|
||||
fn label(self) -> impl Into<SharedString> {
|
||||
fn label(&self) -> SharedString {
|
||||
match self {
|
||||
Self::Zed | Self::TextThread => "Zed Agent",
|
||||
Self::NativeAgent => "Agent 2",
|
||||
Self::Gemini => "Gemini CLI",
|
||||
Self::ClaudeCode => "Claude Code",
|
||||
Self::Zed | Self::TextThread => "Zed Agent".into(),
|
||||
Self::NativeAgent => "Agent 2".into(),
|
||||
Self::Gemini => "Gemini CLI".into(),
|
||||
Self::ClaudeCode => "Claude Code".into(),
|
||||
Self::Custom { name, .. } => name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon(self) -> Option<IconName> {
|
||||
fn icon(&self) -> Option<IconName> {
|
||||
match self {
|
||||
Self::Zed | Self::NativeAgent | Self::TextThread => None,
|
||||
Self::Gemini => Some(IconName::AiGemini),
|
||||
Self::ClaudeCode => Some(IconName::AiClaude),
|
||||
Self::Custom { .. } => Some(IconName::Terminal),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -517,7 +524,7 @@ pub struct AgentPanel {
|
|||
impl AgentPanel {
|
||||
fn serialize(&mut self, cx: &mut Context<Self>) {
|
||||
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 {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
|
@ -607,7 +614,7 @@ impl AgentPanel {
|
|||
panel.update(cx, |panel, cx| {
|
||||
panel.width = serialized_panel.width.map(|w| w.round());
|
||||
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);
|
||||
}
|
||||
cx.notify();
|
||||
|
@ -1077,14 +1084,17 @@ impl AgentPanel {
|
|||
cx.spawn_in(window, async move |this, cx| {
|
||||
let ext_agent = match agent_choice {
|
||||
Some(agent) => {
|
||||
cx.background_spawn(async move {
|
||||
if let Some(serialized) =
|
||||
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
|
||||
{
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
|
||||
.await
|
||||
.log_err();
|
||||
cx.background_spawn({
|
||||
let agent = agent.clone();
|
||||
async move {
|
||||
if let Some(serialized) =
|
||||
serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
|
||||
{
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
@ -1110,7 +1120,9 @@ impl AgentPanel {
|
|||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match ext_agent {
|
||||
crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => {
|
||||
crate::ExternalAgent::Gemini
|
||||
| crate::ExternalAgent::NativeAgent
|
||||
| crate::ExternalAgent::Custom { .. } => {
|
||||
if !cx.has_flag::<GeminiAndNativeFeatureFlag>() {
|
||||
return;
|
||||
}
|
||||
|
@ -1839,14 +1851,14 @@ impl AgentPanel {
|
|||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_agent != agent {
|
||||
self.selected_agent = agent;
|
||||
self.selected_agent = agent.clone();
|
||||
self.serialize(cx);
|
||||
}
|
||||
self.new_agent_thread(agent, window, cx);
|
||||
}
|
||||
|
||||
pub fn selected_agent(&self) -> AgentType {
|
||||
self.selected_agent
|
||||
self.selected_agent.clone()
|
||||
}
|
||||
|
||||
pub fn new_agent_thread(
|
||||
|
@ -1885,6 +1897,13 @@ impl AgentPanel {
|
|||
window,
|
||||
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
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
let selected_agent_label = self.selected_agent.label().into();
|
||||
let selected_agent_label = self.selected_agent.label();
|
||||
let selected_agent = div()
|
||||
.id("selected_agent_icon")
|
||||
.when_some(self.selected_agent.icon(), |this, icon| {
|
||||
|
|
|
@ -28,13 +28,14 @@ use std::rc::Rc;
|
|||
use std::sync::Arc;
|
||||
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent_servers::AgentServerSettings;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Entity, actions};
|
||||
use gpui::{Action, App, Entity, SharedString, actions};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||
|
@ -159,13 +160,17 @@ pub struct NewNativeAgentThreadFromSummary {
|
|||
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")]
|
||||
enum ExternalAgent {
|
||||
#[default]
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
settings: AgentServerSettings,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
|
@ -175,9 +180,13 @@ impl ExternalAgent {
|
|||
history: Entity<agent2::HistoryStore>,
|
||||
) -> Rc<dyn agent_servers::AgentServer> {
|
||||
match self {
|
||||
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
|
||||
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
|
||||
Self::Gemini => Rc::new(agent_servers::Gemini),
|
||||
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
|
||||
Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
|
||||
name.clone(),
|
||||
settings,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ serde_json_lenient.workspace = true
|
|||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
util_macros.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
|
|
@ -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
|
||||
/// 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 {
|
||||
state: State,
|
||||
|
|
|
@ -643,11 +643,11 @@ pub trait LanguageModelProvider: 'static {
|
|||
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
#[derive(Default, Clone)]
|
||||
pub enum ConfigurationViewTargetAgent {
|
||||
#[default]
|
||||
ZedAgent,
|
||||
Other(&'static str),
|
||||
Other(SharedString),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
|
|
|
@ -1041,9 +1041,9 @@ impl Render for ConfigurationView {
|
|||
v_flex()
|
||||
.size_full()
|
||||
.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 {
|
||||
ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic",
|
||||
ConfigurationViewTargetAgent::Other(agent) => 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".into(),
|
||||
ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
|
||||
})))
|
||||
.child(
|
||||
List::new()
|
||||
|
|
|
@ -921,9 +921,9 @@ impl Render for ConfigurationView {
|
|||
v_flex()
|
||||
.size_full()
|
||||
.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 {
|
||||
ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI",
|
||||
ConfigurationViewTargetAgent::Other(agent) => 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".into(),
|
||||
ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
|
||||
})))
|
||||
.child(
|
||||
List::new()
|
||||
|
|
|
@ -231,6 +231,7 @@
|
|||
"implements"
|
||||
"interface"
|
||||
"keyof"
|
||||
"module"
|
||||
"namespace"
|
||||
"private"
|
||||
"protected"
|
||||
|
@ -250,4 +251,4 @@
|
|||
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
|
||||
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
|
||||
(jsx_attribute "=" @punctuation.delimiter.jsx)
|
||||
(jsx_text) @text.jsx
|
||||
(jsx_text) @text.jsx
|
||||
|
|
|
@ -237,6 +237,7 @@
|
|||
"implements"
|
||||
"interface"
|
||||
"keyof"
|
||||
"module"
|
||||
"namespace"
|
||||
"private"
|
||||
"protected"
|
||||
|
@ -256,4 +257,4 @@
|
|||
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
|
||||
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
|
||||
(jsx_attribute "=" @punctuation.delimiter.jsx)
|
||||
(jsx_text) @text.jsx
|
||||
(jsx_text) @text.jsx
|
||||
|
|
|
@ -248,6 +248,7 @@
|
|||
"is"
|
||||
"keyof"
|
||||
"let"
|
||||
"module"
|
||||
"namespace"
|
||||
"new"
|
||||
"of"
|
||||
|
@ -272,4 +273,4 @@
|
|||
"while"
|
||||
"with"
|
||||
"yield"
|
||||
] @keyword
|
||||
] @keyword
|
||||
|
|
|
@ -835,7 +835,7 @@ impl MultiBuffer {
|
|||
this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
|
||||
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 {
|
||||
buffer_ids.push(buffer_id);
|
||||
edits.sort_by_key(|edit| edit.range.start);
|
||||
|
|
|
@ -11913,7 +11913,7 @@ impl LspStore {
|
|||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
}
|
||||
"textDocument/colorProvider" => {
|
||||
"textDocument/documentColor" => {
|
||||
if let Some(caps) = reg
|
||||
.register_options
|
||||
.map(serde_json::from_value)
|
||||
|
@ -12064,7 +12064,7 @@ impl LspStore {
|
|||
});
|
||||
notify_server_capabilities_updated(&server, cx);
|
||||
}
|
||||
"textDocument/colorProvider" => {
|
||||
"textDocument/documentColor" => {
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.color_provider = None;
|
||||
});
|
||||
|
|
|
@ -51,7 +51,7 @@ To configure, use
|
|||
|
||||
```json5
|
||||
"project_panel": {
|
||||
"diagnostics": "all",
|
||||
"show_diagnostics": "all",
|
||||
}
|
||||
```
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue