Cargo.lock 🔗
@@ -29,6 +29,7 @@ dependencies = [
"tempfile",
"terminal",
"ui",
+ "url",
"util",
"workspace-hack",
]
Agus Zubiaga and Cole Miller created
Also adds data-layer support for symbols, thread, and rules.
Release Notes:
- N/A
---------
Co-authored-by: Cole Miller <cole@zed.dev>
Cargo.lock | 1
crates/acp_thread/Cargo.toml | 1
crates/acp_thread/src/acp_thread.rs | 63 +---
crates/acp_thread/src/mention.rs | 122 +++++++++
crates/agent2/src/agent.rs | 46 --
crates/agent2/src/tests/mod.rs | 41 +-
crates/agent2/src/thread.rs | 261 +++++++++++++++++++
crates/agent_ui/src/acp/completion_provider.rs | 77 +++++
crates/agent_ui/src/acp/thread_view.rs | 239 ++++++++++-------
9 files changed, 625 insertions(+), 226 deletions(-)
@@ -29,6 +29,7 @@ dependencies = [
"tempfile",
"terminal",
"ui",
+ "url",
"util",
"workspace-hack",
]
@@ -34,6 +34,7 @@ settings.workspace = true
smol.workspace = true
terminal.workspace = true
ui.workspace = true
+url.workspace = true
util.workspace = true
workspace-hack.workspace = true
@@ -1,13 +1,15 @@
mod connection;
mod diff;
+mod mention;
mod terminal;
pub use connection::*;
pub use diff::*;
+pub use mention::*;
pub use terminal::*;
use action_log::ActionLog;
-use agent_client_protocol as acp;
+use agent_client_protocol::{self as acp};
use anyhow::{Context as _, Result};
use editor::Bias;
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
@@ -21,12 +23,7 @@ use std::error::Error;
use std::fmt::Formatter;
use std::process::ExitStatus;
use std::rc::Rc;
-use std::{
- fmt::Display,
- mem,
- path::{Path, PathBuf},
- sync::Arc,
-};
+use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use ui::App;
use util::ResultExt;
@@ -53,38 +50,6 @@ impl UserMessage {
}
}
-#[derive(Debug)]
-pub struct MentionPath<'a>(&'a Path);
-
-impl<'a> MentionPath<'a> {
- const PREFIX: &'static str = "@file:";
-
- pub fn new(path: &'a Path) -> Self {
- MentionPath(path)
- }
-
- pub fn try_parse(url: &'a str) -> Option<Self> {
- let path = url.strip_prefix(Self::PREFIX)?;
- Some(MentionPath(Path::new(path)))
- }
-
- pub fn path(&self) -> &Path {
- self.0
- }
-}
-
-impl Display for MentionPath<'_> {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "[@{}]({}{})",
- self.0.file_name().unwrap_or_default().display(),
- Self::PREFIX,
- self.0.display()
- )
- }
-}
-
#[derive(Debug, PartialEq)]
pub struct AssistantMessage {
pub chunks: Vec<AssistantMessageChunk>,
@@ -367,16 +332,24 @@ impl ContentBlock {
) {
let new_content = match block {
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
- acp::ContentBlock::ResourceLink(resource_link) => {
- if let Some(path) = resource_link.uri.strip_prefix("file://") {
- format!("{}", MentionPath(path.as_ref()))
+ acp::ContentBlock::Resource(acp::EmbeddedResource {
+ resource:
+ acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents {
+ uri,
+ ..
+ }),
+ ..
+ }) => {
+ if let Some(uri) = MentionUri::parse(&uri).log_err() {
+ uri.to_link()
} else {
- resource_link.uri.clone()
+ uri.clone()
}
}
acp::ContentBlock::Image(_)
| acp::ContentBlock::Audio(_)
- | acp::ContentBlock::Resource(_) => String::new(),
+ | acp::ContentBlock::Resource(acp::EmbeddedResource { .. })
+ | acp::ContentBlock::ResourceLink(_) => String::new(),
};
match self {
@@ -1329,7 +1302,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt as _;
- use std::{cell::RefCell, rc::Rc, time::Duration};
+ use std::{cell::RefCell, path::Path, rc::Rc, time::Duration};
use util::path;
@@ -0,0 +1,122 @@
+use agent_client_protocol as acp;
+use anyhow::{Result, bail};
+use std::path::PathBuf;
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum MentionUri {
+ File(PathBuf),
+ Symbol(PathBuf, String),
+ Thread(acp::SessionId),
+ Rule(String),
+}
+
+impl MentionUri {
+ pub fn parse(input: &str) -> Result<Self> {
+ let url = url::Url::parse(input)?;
+ let path = url.path();
+ match url.scheme() {
+ "file" => {
+ if let Some(fragment) = url.fragment() {
+ Ok(Self::Symbol(path.into(), fragment.into()))
+ } else {
+ Ok(Self::File(path.into()))
+ }
+ }
+ "zed" => {
+ if let Some(thread) = path.strip_prefix("/agent/thread/") {
+ Ok(Self::Thread(acp::SessionId(thread.into())))
+ } else if let Some(rule) = path.strip_prefix("/agent/rule/") {
+ Ok(Self::Rule(rule.into()))
+ } else {
+ bail!("invalid zed url: {:?}", input);
+ }
+ }
+ other => bail!("unrecognized scheme {:?}", other),
+ }
+ }
+
+ pub fn name(&self) -> String {
+ match self {
+ MentionUri::File(path) => path.file_name().unwrap().to_string_lossy().into_owned(),
+ MentionUri::Symbol(_path, name) => name.clone(),
+ MentionUri::Thread(thread) => thread.to_string(),
+ MentionUri::Rule(rule) => rule.clone(),
+ }
+ }
+
+ pub fn to_link(&self) -> String {
+ let name = self.name();
+ let uri = self.to_uri();
+ format!("[{name}]({uri})")
+ }
+
+ pub fn to_uri(&self) -> String {
+ match self {
+ MentionUri::File(path) => {
+ format!("file://{}", path.display())
+ }
+ MentionUri::Symbol(path, name) => {
+ format!("file://{}#{}", path.display(), name)
+ }
+ MentionUri::Thread(thread) => {
+ format!("zed:///agent/thread/{}", thread.0)
+ }
+ MentionUri::Rule(rule) => {
+ format!("zed:///agent/rule/{}", rule)
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_mention_uri_parse_and_display() {
+ // Test file URI
+ let file_uri = "file:///path/to/file.rs";
+ let parsed = MentionUri::parse(file_uri).unwrap();
+ match &parsed {
+ MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"),
+ _ => panic!("Expected File variant"),
+ }
+ assert_eq!(parsed.to_uri(), file_uri);
+
+ // Test symbol URI
+ let symbol_uri = "file:///path/to/file.rs#MySymbol";
+ let parsed = MentionUri::parse(symbol_uri).unwrap();
+ match &parsed {
+ MentionUri::Symbol(path, symbol) => {
+ assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
+ assert_eq!(symbol, "MySymbol");
+ }
+ _ => panic!("Expected Symbol variant"),
+ }
+ assert_eq!(parsed.to_uri(), symbol_uri);
+
+ // Test thread URI
+ let thread_uri = "zed:///agent/thread/session123";
+ let parsed = MentionUri::parse(thread_uri).unwrap();
+ match &parsed {
+ MentionUri::Thread(session_id) => assert_eq!(session_id.0.as_ref(), "session123"),
+ _ => panic!("Expected Thread variant"),
+ }
+ assert_eq!(parsed.to_uri(), thread_uri);
+
+ // Test rule URI
+ let rule_uri = "zed:///agent/rule/my_rule";
+ let parsed = MentionUri::parse(rule_uri).unwrap();
+ match &parsed {
+ MentionUri::Rule(rule) => assert_eq!(rule, "my_rule"),
+ _ => panic!("Expected Rule variant"),
+ }
+ assert_eq!(parsed.to_uri(), rule_uri);
+
+ // Test invalid scheme
+ assert!(MentionUri::parse("http://example.com").is_err());
+
+ // Test invalid zed path
+ assert!(MentionUri::parse("zed:///invalid/path").is_err());
+ }
+}
@@ -1,8 +1,8 @@
use crate::{AgentResponseEvent, Thread, templates::Templates};
use crate::{
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool,
- FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool,
- ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
+ FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool,
+ OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
};
use acp_thread::ModelSelector;
use agent_client_protocol as acp;
@@ -516,10 +516,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
})?;
log::debug!("Found session for: {}", session_id);
- // Convert prompt to message
- let message = convert_prompt_to_message(params.prompt);
+ let message: Vec<MessageContent> = params
+ .prompt
+ .into_iter()
+ .map(Into::into)
+ .collect::<Vec<_>>();
log::info!("Converted prompt to message: {} chars", message.len());
- log::debug!("Message content: {}", message);
+ log::debug!("Message content: {:?}", message);
// Get model using the ModelSelector capability (always available for agent2)
// Get the selected model from the thread directly
@@ -623,39 +626,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}
}
-/// Convert ACP content blocks to a message string
-fn convert_prompt_to_message(blocks: Vec<acp::ContentBlock>) -> String {
- log::debug!("Converting {} content blocks to message", blocks.len());
- let mut message = String::new();
-
- for block in blocks {
- match block {
- acp::ContentBlock::Text(text) => {
- log::trace!("Processing text block: {} chars", text.text.len());
- message.push_str(&text.text);
- }
- acp::ContentBlock::ResourceLink(link) => {
- log::trace!("Processing resource link: {}", link.uri);
- message.push_str(&format!(" @{} ", link.uri));
- }
- acp::ContentBlock::Image(_) => {
- log::trace!("Processing image block");
- message.push_str(" [image] ");
- }
- acp::ContentBlock::Audio(_) => {
- log::trace!("Processing audio block");
- message.push_str(" [audio] ");
- }
- acp::ContentBlock::Resource(resource) => {
- log::trace!("Processing resource block: {:?}", resource.resource);
- message.push_str(&format!(" [resource: {:?}] ", resource.resource));
- }
- }
- }
-
- message
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -1,4 +1,5 @@
use super::*;
+use crate::MessageContent;
use acp_thread::AgentConnection;
use action_log::ActionLog;
use agent_client_protocol::{self as acp};
@@ -13,8 +14,8 @@ use gpui::{
use indoc::indoc;
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
- LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
- StopReason, fake_provider::FakeLanguageModel,
+ LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, Role, StopReason,
+ fake_provider::FakeLanguageModel,
};
use project::Project;
use prompt_store::ProjectContext;
@@ -272,14 +273,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
assert_eq!(
message.content,
vec![
- MessageContent::ToolResult(LanguageModelToolResult {
+ language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
}),
- MessageContent::ToolResult(LanguageModelToolResult {
+ language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(),
is_error: true,
@@ -312,13 +313,15 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
let message = completion.messages.last().unwrap();
assert_eq!(
message.content,
- vec![MessageContent::ToolResult(LanguageModelToolResult {
- tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
- tool_name: ToolRequiringPermission.name().into(),
- is_error: false,
- content: "Allowed".into(),
- output: Some("Allowed".into())
- })]
+ vec![language_model::MessageContent::ToolResult(
+ LanguageModelToolResult {
+ tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
+ tool_name: ToolRequiringPermission.name().into(),
+ is_error: false,
+ content: "Allowed".into(),
+ output: Some("Allowed".into())
+ }
+ )]
);
// Simulate a final tool call, ensuring we don't trigger authorization.
@@ -337,13 +340,15 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
let message = completion.messages.last().unwrap();
assert_eq!(
message.content,
- vec![MessageContent::ToolResult(LanguageModelToolResult {
- tool_use_id: "tool_id_4".into(),
- tool_name: ToolRequiringPermission.name().into(),
- is_error: false,
- content: "Allowed".into(),
- output: Some("Allowed".into())
- })]
+ vec![language_model::MessageContent::ToolResult(
+ LanguageModelToolResult {
+ tool_use_id: "tool_id_4".into(),
+ tool_name: ToolRequiringPermission.name().into(),
+ is_error: false,
+ content: "Allowed".into(),
+ output: Some("Allowed".into())
+ }
+ )]
);
}
@@ -1,4 +1,5 @@
use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates};
+use acp_thread::MentionUri;
use action_log::ActionLog;
use agent_client_protocol as acp;
use agent_settings::{AgentProfileId, AgentSettings};
@@ -13,10 +14,10 @@ use futures::{
};
use gpui::{App, Context, Entity, SharedString, Task};
use language_model::{
- LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
+ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage,
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
- LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason,
+ LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason,
};
use log;
use project::Project;
@@ -25,7 +26,8 @@ use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
-use std::{cell::RefCell, collections::BTreeMap, fmt::Write, rc::Rc, sync::Arc};
+use std::fmt::Write;
+use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc};
use util::{ResultExt, markdown::MarkdownCodeBlock};
#[derive(Debug, Clone)]
@@ -34,6 +36,23 @@ pub struct AgentMessage {
pub content: Vec<MessageContent>,
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MessageContent {
+ Text(String),
+ Thinking {
+ text: String,
+ signature: Option<String>,
+ },
+ Mention {
+ uri: MentionUri,
+ content: String,
+ },
+ RedactedThinking(String),
+ Image(LanguageModelImage),
+ ToolUse(LanguageModelToolUse),
+ ToolResult(LanguageModelToolResult),
+}
+
impl AgentMessage {
pub fn to_markdown(&self) -> String {
let mut markdown = format!("## {}\n", self.role);
@@ -93,6 +112,9 @@ impl AgentMessage {
.unwrap();
}
}
+ MessageContent::Mention { uri, .. } => {
+ write!(markdown, "{}", uri.to_link()).ok();
+ }
}
}
@@ -214,10 +236,11 @@ impl Thread {
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
pub fn send(
&mut self,
- content: impl Into<MessageContent>,
+ content: impl Into<UserMessage>,
cx: &mut Context<Self>,
) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>> {
- let content = content.into();
+ let content = content.into().0;
+
let model = self.selected_model.clone();
log::info!("Thread::send called with model: {:?}", model.name());
log::debug!("Thread::send content: {:?}", content);
@@ -230,7 +253,7 @@ impl Thread {
let user_message_ix = self.messages.len();
self.messages.push(AgentMessage {
role: Role::User,
- content: vec![content],
+ content,
});
log::info!("Total messages in thread: {}", self.messages.len());
self.running_turn = Some(cx.spawn(async move |thread, cx| {
@@ -353,7 +376,7 @@ impl Thread {
log::debug!("System message built");
AgentMessage {
role: Role::System,
- content: vec![prompt.into()],
+ content: vec![prompt.as_str().into()],
}
}
@@ -701,11 +724,7 @@ impl Thread {
},
message.content.len()
);
- LanguageModelRequestMessage {
- role: message.role,
- content: message.content.clone(),
- cache: false,
- }
+ message.to_request()
})
.collect();
messages
@@ -720,6 +739,20 @@ impl Thread {
}
}
+pub struct UserMessage(Vec<MessageContent>);
+
+impl From<Vec<MessageContent>> for UserMessage {
+ fn from(content: Vec<MessageContent>) -> Self {
+ UserMessage(content)
+ }
+}
+
+impl<T: Into<MessageContent>> From<T> for UserMessage {
+ fn from(content: T) -> Self {
+ UserMessage(vec![content.into()])
+ }
+}
+
pub trait AgentTool
where
Self: 'static + Sized,
@@ -1102,3 +1135,207 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver {
&mut self.0
}
}
+
+impl AgentMessage {
+ fn to_request(&self) -> language_model::LanguageModelRequestMessage {
+ let mut message = LanguageModelRequestMessage {
+ role: self.role,
+ content: Vec::with_capacity(self.content.len()),
+ cache: false,
+ };
+
+ const OPEN_CONTEXT: &str = "<context>\n\
+ The following items were attached by the user. \
+ They are up-to-date and don't need to be re-read.\n\n";
+
+ const OPEN_FILES_TAG: &str = "<files>";
+ const OPEN_SYMBOLS_TAG: &str = "<symbols>";
+ const OPEN_THREADS_TAG: &str = "<threads>";
+ const OPEN_RULES_TAG: &str =
+ "<rules>\nThe user has specified the following rules that should be applied:\n";
+
+ let mut file_context = OPEN_FILES_TAG.to_string();
+ let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
+ let mut thread_context = OPEN_THREADS_TAG.to_string();
+ let mut rules_context = OPEN_RULES_TAG.to_string();
+
+ for chunk in &self.content {
+ let chunk = match chunk {
+ MessageContent::Text(text) => language_model::MessageContent::Text(text.clone()),
+ MessageContent::Thinking { text, signature } => {
+ language_model::MessageContent::Thinking {
+ text: text.clone(),
+ signature: signature.clone(),
+ }
+ }
+ MessageContent::RedactedThinking(value) => {
+ language_model::MessageContent::RedactedThinking(value.clone())
+ }
+ MessageContent::ToolUse(value) => {
+ language_model::MessageContent::ToolUse(value.clone())
+ }
+ MessageContent::ToolResult(value) => {
+ language_model::MessageContent::ToolResult(value.clone())
+ }
+ MessageContent::Image(value) => {
+ language_model::MessageContent::Image(value.clone())
+ }
+ MessageContent::Mention { uri, content } => {
+ match uri {
+ MentionUri::File(path) | MentionUri::Symbol(path, _) => {
+ write!(
+ &mut symbol_context,
+ "\n{}",
+ MarkdownCodeBlock {
+ tag: &codeblock_tag(&path),
+ text: &content.to_string(),
+ }
+ )
+ .ok();
+ }
+ MentionUri::Thread(_session_id) => {
+ write!(&mut thread_context, "\n{}\n", content).ok();
+ }
+ MentionUri::Rule(_user_prompt_id) => {
+ write!(
+ &mut rules_context,
+ "\n{}",
+ MarkdownCodeBlock {
+ tag: "",
+ text: &content
+ }
+ )
+ .ok();
+ }
+ }
+
+ language_model::MessageContent::Text(uri.to_link())
+ }
+ };
+
+ message.content.push(chunk);
+ }
+
+ let len_before_context = message.content.len();
+
+ if file_context.len() > OPEN_FILES_TAG.len() {
+ file_context.push_str("</files>\n");
+ message
+ .content
+ .push(language_model::MessageContent::Text(file_context));
+ }
+
+ if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
+ symbol_context.push_str("</symbols>\n");
+ message
+ .content
+ .push(language_model::MessageContent::Text(symbol_context));
+ }
+
+ if thread_context.len() > OPEN_THREADS_TAG.len() {
+ thread_context.push_str("</threads>\n");
+ message
+ .content
+ .push(language_model::MessageContent::Text(thread_context));
+ }
+
+ if rules_context.len() > OPEN_RULES_TAG.len() {
+ rules_context.push_str("</user_rules>\n");
+ message
+ .content
+ .push(language_model::MessageContent::Text(rules_context));
+ }
+
+ if message.content.len() > len_before_context {
+ message.content.insert(
+ len_before_context,
+ language_model::MessageContent::Text(OPEN_CONTEXT.into()),
+ );
+ message
+ .content
+ .push(language_model::MessageContent::Text("</context>".into()));
+ }
+
+ message
+ }
+}
+
+fn codeblock_tag(full_path: &Path) -> String {
+ let mut result = String::new();
+
+ if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
+ let _ = write!(result, "{} ", extension);
+ }
+
+ let _ = write!(result, "{}", full_path.display());
+
+ result
+}
+
+impl From<acp::ContentBlock> for MessageContent {
+ fn from(value: acp::ContentBlock) -> Self {
+ match value {
+ acp::ContentBlock::Text(text_content) => MessageContent::Text(text_content.text),
+ acp::ContentBlock::Image(image_content) => {
+ MessageContent::Image(convert_image(image_content))
+ }
+ acp::ContentBlock::Audio(_) => {
+ // TODO
+ MessageContent::Text("[audio]".to_string())
+ }
+ acp::ContentBlock::ResourceLink(resource_link) => {
+ match MentionUri::parse(&resource_link.uri) {
+ Ok(uri) => Self::Mention {
+ uri,
+ content: String::new(),
+ },
+ Err(err) => {
+ log::error!("Failed to parse mention link: {}", err);
+ MessageContent::Text(format!(
+ "[{}]({})",
+ resource_link.name, resource_link.uri
+ ))
+ }
+ }
+ }
+ acp::ContentBlock::Resource(resource) => match resource.resource {
+ acp::EmbeddedResourceResource::TextResourceContents(resource) => {
+ match MentionUri::parse(&resource.uri) {
+ Ok(uri) => Self::Mention {
+ uri,
+ content: resource.text,
+ },
+ Err(err) => {
+ log::error!("Failed to parse mention link: {}", err);
+ MessageContent::Text(
+ MarkdownCodeBlock {
+ tag: &resource.uri,
+ text: &resource.text,
+ }
+ .to_string(),
+ )
+ }
+ }
+ }
+ acp::EmbeddedResourceResource::BlobResourceContents(_) => {
+ // TODO
+ MessageContent::Text("[blob]".to_string())
+ }
+ },
+ }
+ }
+}
+
+fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage {
+ LanguageModelImage {
+ source: image_content.data.into(),
+ // TODO: make this optional?
+ size: gpui::Size::new(0.into(), 0.into()),
+ }
+}
+
+impl From<&str> for MessageContent {
+ fn from(text: &str) -> Self {
+ MessageContent::Text(text.into())
+ }
+}
@@ -1,18 +1,20 @@
use std::ops::Range;
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
-use anyhow::Result;
+use acp_thread::MentionUri;
+use anyhow::{Context as _, Result};
use collections::HashMap;
use editor::display_map::CreaseId;
use editor::{CompletionProvider, Editor, ExcerptId};
use file_icons::FileIcons;
+use futures::future::try_join_all;
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use parking_lot::Mutex;
-use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, WorktreeId};
+use project::{Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, WorktreeId};
use rope::Point;
use text::{Anchor, ToPoint};
use ui::prelude::*;
@@ -23,21 +25,63 @@ use crate::context_picker::file_context_picker::{extract_file_name_and_directory
#[derive(Default)]
pub struct MentionSet {
- paths_by_crease_id: HashMap<CreaseId, ProjectPath>,
+ paths_by_crease_id: HashMap<CreaseId, MentionUri>,
}
impl MentionSet {
- pub fn insert(&mut self, crease_id: CreaseId, path: ProjectPath) {
- self.paths_by_crease_id.insert(crease_id, path);
- }
-
- pub fn path_for_crease_id(&self, crease_id: CreaseId) -> Option<ProjectPath> {
- self.paths_by_crease_id.get(&crease_id).cloned()
+ pub fn insert(&mut self, crease_id: CreaseId, path: PathBuf) {
+ self.paths_by_crease_id
+ .insert(crease_id, MentionUri::File(path));
}
pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
self.paths_by_crease_id.drain().map(|(id, _)| id)
}
+
+ pub fn contents(
+ &self,
+ project: Entity<Project>,
+ cx: &mut App,
+ ) -> Task<Result<HashMap<CreaseId, Mention>>> {
+ let contents = self
+ .paths_by_crease_id
+ .iter()
+ .map(|(crease_id, uri)| match uri {
+ MentionUri::File(path) => {
+ let crease_id = *crease_id;
+ let uri = uri.clone();
+ let path = path.to_path_buf();
+ let buffer_task = project.update(cx, |project, cx| {
+ let path = project
+ .find_project_path(path, cx)
+ .context("Failed to find project path")?;
+ anyhow::Ok(project.open_buffer(path, cx))
+ });
+
+ cx.spawn(async move |cx| {
+ let buffer = buffer_task?.await?;
+ let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
+
+ anyhow::Ok((crease_id, Mention { uri, content }))
+ })
+ }
+ _ => {
+ // TODO
+ unimplemented!()
+ }
+ })
+ .collect::<Vec<_>>();
+
+ cx.spawn(async move |_cx| {
+ let contents = try_join_all(contents).await?.into_iter().collect();
+ anyhow::Ok(contents)
+ })
+ }
+}
+
+pub struct Mention {
+ pub uri: MentionUri,
+ pub content: String,
}
pub struct ContextPickerCompletionProvider {
@@ -68,6 +112,7 @@ impl ContextPickerCompletionProvider {
source_range: Range<Anchor>,
editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
+ project: Entity<Project>,
cx: &App,
) -> Completion {
let (file_name, directory) =
@@ -112,6 +157,7 @@ impl ContextPickerCompletionProvider {
new_text_len - 1,
editor,
mention_set,
+ project,
)),
}
}
@@ -159,6 +205,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
return Task::ready(Ok(Vec::new()));
};
+ let project = workspace.read(cx).project().clone();
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
@@ -195,6 +242,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
source_range.clone(),
editor.clone(),
mention_set.clone(),
+ project.clone(),
cx,
)
})
@@ -254,6 +302,7 @@ fn confirm_completion_callback(
content_len: usize,
editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
+ project: Entity<Project>,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, window, cx| {
let crease_text = crease_text.clone();
@@ -261,6 +310,7 @@ fn confirm_completion_callback(
let editor = editor.clone();
let project_path = project_path.clone();
let mention_set = mention_set.clone();
+ let project = project.clone();
window.defer(cx, move |window, cx| {
let crease_id = crate::context_picker::insert_crease_for_mention(
excerpt_id,
@@ -272,8 +322,13 @@ fn confirm_completion_callback(
window,
cx,
);
+
+ let Some(path) = project.read(cx).absolute_path(&project_path, cx) else {
+ return;
+ };
+
if let Some(crease_id) = crease_id {
- mention_set.lock().insert(crease_id, project_path);
+ mention_set.lock().insert(crease_id, path);
}
});
false
@@ -1,6 +1,6 @@
use acp_thread::{
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
- LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
+ LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
};
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
@@ -28,6 +28,7 @@ use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use parking_lot::Mutex;
use project::{CompletionIntent, Project};
use settings::{Settings as _, SettingsStore};
+use std::path::PathBuf;
use std::{
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
time::Duration,
@@ -376,81 +377,101 @@ impl AcpThreadView {
let mut ix = 0;
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
let project = self.project.clone();
- self.message_editor.update(cx, |editor, cx| {
- let text = editor.text(cx);
- editor.display_map.update(cx, |map, cx| {
- let snapshot = map.snapshot(cx);
- for (crease_id, crease) in snapshot.crease_snapshot.creases() {
- // Skip creases that have been edited out of the message buffer.
- if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
- continue;
- }
- if let Some(project_path) =
- self.mention_set.lock().path_for_crease_id(crease_id)
- {
- let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
- if crease_range.start > ix {
- chunks.push(text[ix..crease_range.start].into());
- }
- if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
- let path_str = abs_path.display().to_string();
- chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
- uri: path_str.clone(),
- name: path_str,
- annotations: None,
- description: None,
- mime_type: None,
- size: None,
- title: None,
- }));
- }
- ix = crease_range.end;
- }
- }
+ let contents = self.mention_set.lock().contents(project, cx);
- if ix < text.len() {
- let last_chunk = text[ix..].trim_end();
- if !last_chunk.is_empty() {
- chunks.push(last_chunk.into());
- }
+ cx.spawn_in(window, async move |this, cx| {
+ let contents = match contents.await {
+ Ok(contents) => contents,
+ Err(e) => {
+ this.update(cx, |this, cx| {
+ this.last_error =
+ Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx)));
+ })
+ .ok();
+ return;
}
- })
- });
+ };
- if chunks.is_empty() {
- return;
- }
+ this.update_in(cx, |this, window, cx| {
+ this.message_editor.update(cx, |editor, cx| {
+ let text = editor.text(cx);
+ editor.display_map.update(cx, |map, cx| {
+ let snapshot = map.snapshot(cx);
+ for (crease_id, crease) in snapshot.crease_snapshot.creases() {
+ // Skip creases that have been edited out of the message buffer.
+ if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
+ continue;
+ }
- let Some(thread) = self.thread() else {
- return;
- };
- let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
+ if let Some(mention) = contents.get(&crease_id) {
+ let crease_range =
+ crease.range().to_offset(&snapshot.buffer_snapshot);
+ if crease_range.start > ix {
+ chunks.push(text[ix..crease_range.start].into());
+ }
+ chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource {
+ annotations: None,
+ resource: acp::EmbeddedResourceResource::TextResourceContents(
+ acp::TextResourceContents {
+ mime_type: None,
+ text: mention.content.clone(),
+ uri: mention.uri.to_uri(),
+ },
+ ),
+ }));
+ ix = crease_range.end;
+ }
+ }
- cx.spawn(async move |this, cx| {
- let result = task.await;
+ if ix < text.len() {
+ let last_chunk = text[ix..].trim_end();
+ if !last_chunk.is_empty() {
+ chunks.push(last_chunk.into());
+ }
+ }
+ })
+ });
- this.update(cx, |this, cx| {
- if let Err(err) = result {
- this.last_error =
- Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
+ if chunks.is_empty() {
+ return;
}
- })
- })
- .detach();
- let mention_set = self.mention_set.clone();
+ let Some(thread) = this.thread() else {
+ return;
+ };
+ let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
- self.set_editor_is_expanded(false, cx);
+ cx.spawn(async move |this, cx| {
+ let result = task.await;
- self.message_editor.update(cx, |editor, cx| {
- editor.clear(window, cx);
- editor.remove_creases(mention_set.lock().drain(), cx)
- });
+ this.update(cx, |this, cx| {
+ if let Err(err) = result {
+ this.last_error =
+ Some(cx.new(|cx| {
+ Markdown::new(err.to_string().into(), None, None, cx)
+ }))
+ }
+ })
+ })
+ .detach();
- self.scroll_to_bottom(cx);
+ let mention_set = this.mention_set.clone();
- self.message_history.borrow_mut().push(chunks);
+ this.set_editor_is_expanded(false, cx);
+
+ this.message_editor.update(cx, |editor, cx| {
+ editor.clear(window, cx);
+ editor.remove_creases(mention_set.lock().drain(), cx)
+ });
+
+ this.scroll_to_bottom(cx);
+
+ this.message_history.borrow_mut().push(chunks);
+ })
+ .ok();
+ })
+ .detach();
}
fn previous_history_message(
@@ -563,16 +584,19 @@ impl AcpThreadView {
acp::ContentBlock::Text(text_content) => {
text.push_str(&text_content.text);
}
- acp::ContentBlock::ResourceLink(resource_link) => {
- let path = Path::new(&resource_link.uri);
+ acp::ContentBlock::Resource(acp::EmbeddedResource {
+ resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
+ ..
+ }) => {
+ let path = PathBuf::from(&resource.uri);
+ let project_path = project.read(cx).project_path_for_absolute_path(&path, cx);
let start = text.len();
- let content = MentionPath::new(&path).to_string();
+ let content = MentionUri::File(path).to_uri();
text.push_str(&content);
let end = text.len();
- if let Some(project_path) =
- project.read(cx).project_path_for_absolute_path(&path, cx)
- {
- let filename: SharedString = path
+ if let Some(project_path) = project_path {
+ let filename: SharedString = project_path
+ .path
.file_name()
.unwrap_or_default()
.to_string_lossy()
@@ -583,7 +607,8 @@ impl AcpThreadView {
}
acp::ContentBlock::Image(_)
| acp::ContentBlock::Audio(_)
- | acp::ContentBlock::Resource(_) => {}
+ | acp::ContentBlock::Resource(_)
+ | acp::ContentBlock::ResourceLink(_) => {}
}
}
@@ -602,18 +627,21 @@ impl AcpThreadView {
};
let anchor = snapshot.anchor_before(range.start);
- let crease_id = crate::context_picker::insert_crease_for_mention(
- anchor.excerpt_id,
- anchor.text_anchor,
- range.end - range.start,
- filename,
- crease_icon_path,
- message_editor.clone(),
- window,
- cx,
- );
- if let Some(crease_id) = crease_id {
- mention_set.lock().insert(crease_id, project_path);
+ if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) {
+ let crease_id = crate::context_picker::insert_crease_for_mention(
+ anchor.excerpt_id,
+ anchor.text_anchor,
+ range.end - range.start,
+ filename,
+ crease_icon_path,
+ message_editor.clone(),
+ window,
+ cx,
+ );
+
+ if let Some(crease_id) = crease_id {
+ mention_set.lock().insert(crease_id, project_path);
+ }
}
}
@@ -2562,25 +2590,31 @@ impl AcpThreadView {
return;
};
- if let Some(mention_path) = MentionPath::try_parse(&url) {
- workspace.update(cx, |workspace, cx| {
- let project = workspace.project();
- let Some((path, entry)) = project.update(cx, |project, cx| {
- let path = project.find_project_path(mention_path.path(), cx)?;
- let entry = project.entry_for_path(&path, cx)?;
- Some((path, entry))
- }) else {
- return;
- };
+ if let Some(mention) = MentionUri::parse(&url).log_err() {
+ workspace.update(cx, |workspace, cx| match mention {
+ MentionUri::File(path) => {
+ let project = workspace.project();
+ let Some((path, entry)) = project.update(cx, |project, cx| {
+ let path = project.find_project_path(path, cx)?;
+ let entry = project.entry_for_path(&path, cx)?;
+ Some((path, entry))
+ }) else {
+ return;
+ };
- if entry.is_dir() {
- project.update(cx, |_, cx| {
- cx.emit(project::Event::RevealInProjectPanel(entry.id));
- });
- } else {
- workspace
- .open_path(path, None, true, window, cx)
- .detach_and_log_err(cx);
+ if entry.is_dir() {
+ project.update(cx, |_, cx| {
+ cx.emit(project::Event::RevealInProjectPanel(entry.id));
+ });
+ } else {
+ workspace
+ .open_path(path, None, true, window, cx)
+ .detach_and_log_err(cx);
+ }
+ }
+ _ => {
+ // TODO
+ unimplemented!()
}
})
} else {
@@ -2975,6 +3009,7 @@ impl AcpThreadView {
anchor..anchor,
self.message_editor.clone(),
self.mention_set.clone(),
+ self.project.clone(),
cx,
);
@@ -3117,7 +3152,7 @@ fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
style.base_text_style = text_style;
style.link_callback = Some(Rc::new(move |url, cx| {
- if MentionPath::try_parse(url).is_some() {
+ if MentionUri::parse(url).is_ok() {
let colors = cx.theme().colors();
Some(TextStyleRefinement {
background_color: Some(colors.element_background),