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",
|
"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",
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
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::{
|
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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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| {
|
||||||
|
|
|
@ -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,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,7 +51,7 @@ To configure, use
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
"project_panel": {
|
"project_panel": {
|
||||||
"diagnostics": "all",
|
"show_diagnostics": "all",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue