Compare commits
26 commits
main
...
mention-mo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1aef322d1b | ||
![]() |
46ed71fb9a | ||
![]() |
2f6c9e3a2b | ||
![]() |
5d5c419fa9 | ||
![]() |
0f58decd6d | ||
![]() |
f338d236bc | ||
![]() |
64eac63b50 | ||
![]() |
929ae77473 | ||
![]() |
d26f80ebad | ||
![]() |
6ed2155b9a | ||
![]() |
d8131278f1 | ||
![]() |
9fa49b89de | ||
![]() |
98ba2d9acd | ||
![]() |
b4d97c437d | ||
![]() |
91e22597a8 | ||
![]() |
e2973998ad | ||
![]() |
219c3bfde8 | ||
![]() |
f66e10f965 | ||
![]() |
38128bab3e | ||
![]() |
cf8e056ec4 | ||
![]() |
76566aaa78 | ||
![]() |
0741c30653 | ||
![]() |
26befa1ec6 | ||
![]() |
217a2ef351 | ||
![]() |
cc396a5e36 | ||
![]() |
3085c6fad4 |
18 changed files with 2291 additions and 314 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -7,6 +7,7 @@ name = "acp_thread"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"action_log",
|
"action_log",
|
||||||
|
"agent",
|
||||||
"agent-client-protocol",
|
"agent-client-protocol",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
|
@ -21,6 +22,7 @@ dependencies = [
|
||||||
"markdown",
|
"markdown",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"project",
|
"project",
|
||||||
|
"prompt_store",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -29,7 +31,9 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"terminal",
|
"terminal",
|
||||||
"ui",
|
"ui",
|
||||||
|
"url",
|
||||||
"util",
|
"util",
|
||||||
|
"uuid",
|
||||||
"workspace-hack",
|
"workspace-hack",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -388,6 +392,7 @@ dependencies = [
|
||||||
"ui",
|
"ui",
|
||||||
"ui_input",
|
"ui_input",
|
||||||
"unindent",
|
"unindent",
|
||||||
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"util",
|
"util",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|
|
@ -18,6 +18,7 @@ test-support = ["gpui/test-support", "project/test-support"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
action_log.workspace = true
|
action_log.workspace = true
|
||||||
agent-client-protocol.workspace = true
|
agent-client-protocol.workspace = true
|
||||||
|
agent.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
@ -28,13 +29,16 @@ language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
markdown.workspace = true
|
markdown.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
|
prompt_store.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
terminal.workspace = true
|
terminal.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
mod connection;
|
mod connection;
|
||||||
mod diff;
|
mod diff;
|
||||||
|
mod mention;
|
||||||
mod terminal;
|
mod terminal;
|
||||||
|
|
||||||
pub use connection::*;
|
pub use connection::*;
|
||||||
pub use diff::*;
|
pub use diff::*;
|
||||||
|
pub use mention::*;
|
||||||
pub use terminal::*;
|
pub use terminal::*;
|
||||||
|
|
||||||
use action_log::ActionLog;
|
use action_log::ActionLog;
|
||||||
use agent_client_protocol as acp;
|
use agent_client_protocol::{self as acp};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use editor::Bias;
|
use editor::Bias;
|
||||||
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
||||||
|
@ -21,12 +23,7 @@ use std::error::Error;
|
||||||
use std::fmt::Formatter;
|
use std::fmt::Formatter;
|
||||||
use std::process::ExitStatus;
|
use std::process::ExitStatus;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::{
|
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
|
||||||
fmt::Display,
|
|
||||||
mem,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
use ui::App;
|
use ui::App;
|
||||||
use util::ResultExt;
|
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)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct AssistantMessage {
|
pub struct AssistantMessage {
|
||||||
pub chunks: Vec<AssistantMessageChunk>,
|
pub chunks: Vec<AssistantMessageChunk>,
|
||||||
|
@ -367,16 +332,24 @@ impl ContentBlock {
|
||||||
) {
|
) {
|
||||||
let new_content = match block {
|
let new_content = match block {
|
||||||
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
|
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
|
||||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||||
if let Some(path) = resource_link.uri.strip_prefix("file://") {
|
resource:
|
||||||
format!("{}", MentionPath(path.as_ref()))
|
acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents {
|
||||||
|
uri,
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if let Some(uri) = MentionUri::parse(&uri).log_err() {
|
||||||
|
uri.as_link().to_string()
|
||||||
} else {
|
} else {
|
||||||
resource_link.uri.clone()
|
uri.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acp::ContentBlock::Image(_)
|
acp::ContentBlock::Image(_)
|
||||||
| acp::ContentBlock::Audio(_)
|
| acp::ContentBlock::Audio(_)
|
||||||
| acp::ContentBlock::Resource(_) => String::new(),
|
| acp::ContentBlock::Resource(acp::EmbeddedResource { .. })
|
||||||
|
| acp::ContentBlock::ResourceLink(_) => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
|
@ -1329,7 +1302,7 @@ mod tests {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use smol::stream::StreamExt as _;
|
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;
|
use util::path;
|
||||||
|
|
||||||
|
|
368
crates/acp_thread/src/mention.rs
Normal file
368
crates/acp_thread/src/mention.rs
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
use agent::ThreadId;
|
||||||
|
use anyhow::{Context as _, Result, bail};
|
||||||
|
use prompt_store::{PromptId, UserPromptId};
|
||||||
|
use std::{
|
||||||
|
fmt,
|
||||||
|
ops::Range,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum MentionUri {
|
||||||
|
File(PathBuf),
|
||||||
|
Symbol {
|
||||||
|
path: PathBuf,
|
||||||
|
name: String,
|
||||||
|
line_range: Range<u32>,
|
||||||
|
},
|
||||||
|
Thread {
|
||||||
|
id: ThreadId,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
TextThread {
|
||||||
|
path: PathBuf,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
Rule {
|
||||||
|
id: PromptId,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
Selection {
|
||||||
|
path: PathBuf,
|
||||||
|
line_range: Range<u32>,
|
||||||
|
},
|
||||||
|
Fetch {
|
||||||
|
url: Url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
let range = fragment
|
||||||
|
.strip_prefix("L")
|
||||||
|
.context("Line range must start with \"L\"")?;
|
||||||
|
let (start, end) = range
|
||||||
|
.split_once(":")
|
||||||
|
.context("Line range must use colon as separator")?;
|
||||||
|
let line_range = start
|
||||||
|
.parse::<u32>()
|
||||||
|
.context("Parsing line range start")?
|
||||||
|
.checked_sub(1)
|
||||||
|
.context("Line numbers should be 1-based")?
|
||||||
|
..end
|
||||||
|
.parse::<u32>()
|
||||||
|
.context("Parsing line range end")?
|
||||||
|
.checked_sub(1)
|
||||||
|
.context("Line numbers should be 1-based")?;
|
||||||
|
if let Some(name) = single_query_param(&url, "symbol")? {
|
||||||
|
Ok(Self::Symbol {
|
||||||
|
name,
|
||||||
|
path: path.into(),
|
||||||
|
line_range,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(Self::Selection {
|
||||||
|
path: path.into(),
|
||||||
|
line_range,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Self::File(path.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"zed" => {
|
||||||
|
if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
|
||||||
|
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
|
||||||
|
Ok(Self::Thread {
|
||||||
|
id: thread_id.into(),
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
} else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
|
||||||
|
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
|
||||||
|
Ok(Self::TextThread {
|
||||||
|
path: path.into(),
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
} else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
|
||||||
|
let name = single_query_param(&url, "name")?.context("Missing rule name")?;
|
||||||
|
let rule_id = UserPromptId(rule_id.parse()?);
|
||||||
|
Ok(Self::Rule {
|
||||||
|
id: rule_id.into(),
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
bail!("invalid zed url: {:?}", input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"http" | "https" => Ok(MentionUri::Fetch { url }),
|
||||||
|
other => bail!("unrecognized scheme {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
match self {
|
||||||
|
MentionUri::File(path) => path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned(),
|
||||||
|
MentionUri::Symbol { name, .. } => name.clone(),
|
||||||
|
MentionUri::Thread { name, .. } => name.clone(),
|
||||||
|
MentionUri::TextThread { name, .. } => name.clone(),
|
||||||
|
MentionUri::Rule { name, .. } => name.clone(),
|
||||||
|
MentionUri::Selection {
|
||||||
|
path, line_range, ..
|
||||||
|
} => selection_name(path, line_range),
|
||||||
|
MentionUri::Fetch { url } => url.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_link<'a>(&'a self) -> MentionLink<'a> {
|
||||||
|
MentionLink(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_uri(&self) -> Url {
|
||||||
|
match self {
|
||||||
|
MentionUri::File(path) => {
|
||||||
|
let mut url = Url::parse("file:///").unwrap();
|
||||||
|
url.set_path(&path.to_string_lossy());
|
||||||
|
url
|
||||||
|
}
|
||||||
|
MentionUri::Symbol {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
line_range,
|
||||||
|
} => {
|
||||||
|
let mut url = Url::parse("file:///").unwrap();
|
||||||
|
url.set_path(&path.to_string_lossy());
|
||||||
|
url.query_pairs_mut().append_pair("symbol", name);
|
||||||
|
url.set_fragment(Some(&format!(
|
||||||
|
"L{}:{}",
|
||||||
|
line_range.start + 1,
|
||||||
|
line_range.end + 1
|
||||||
|
)));
|
||||||
|
url
|
||||||
|
}
|
||||||
|
MentionUri::Selection { path, line_range } => {
|
||||||
|
let mut url = Url::parse("file:///").unwrap();
|
||||||
|
url.set_path(&path.to_string_lossy());
|
||||||
|
url.set_fragment(Some(&format!(
|
||||||
|
"L{}:{}",
|
||||||
|
line_range.start + 1,
|
||||||
|
line_range.end + 1
|
||||||
|
)));
|
||||||
|
url
|
||||||
|
}
|
||||||
|
MentionUri::Thread { name, id } => {
|
||||||
|
let mut url = Url::parse("zed:///").unwrap();
|
||||||
|
url.set_path(&format!("/agent/thread/{id}"));
|
||||||
|
url.query_pairs_mut().append_pair("name", name);
|
||||||
|
url
|
||||||
|
}
|
||||||
|
MentionUri::TextThread { path, name } => {
|
||||||
|
let mut url = Url::parse("zed:///").unwrap();
|
||||||
|
url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
|
||||||
|
url.query_pairs_mut().append_pair("name", name);
|
||||||
|
url
|
||||||
|
}
|
||||||
|
MentionUri::Rule { name, id } => {
|
||||||
|
let mut url = Url::parse("zed:///").unwrap();
|
||||||
|
url.set_path(&format!("/agent/rule/{id}"));
|
||||||
|
url.query_pairs_mut().append_pair("name", name);
|
||||||
|
url
|
||||||
|
}
|
||||||
|
MentionUri::Fetch { url } => url.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MentionLink<'a>(&'a MentionUri);
|
||||||
|
|
||||||
|
impl fmt::Display for MentionLink<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "[@{}]({})", self.0.name(), self.0.to_uri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
|
||||||
|
let pairs = url.query_pairs().collect::<Vec<_>>();
|
||||||
|
match pairs.as_slice() {
|
||||||
|
[] => Ok(None),
|
||||||
|
[(k, v)] => {
|
||||||
|
if k != name {
|
||||||
|
bail!("invalid query parameter")
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(v.to_string()))
|
||||||
|
}
|
||||||
|
_ => bail!("too many query pairs"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
|
||||||
|
format!(
|
||||||
|
"{} ({}:{})",
|
||||||
|
path.file_name().unwrap_or_default().display(),
|
||||||
|
line_range.start + 1,
|
||||||
|
line_range.end + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_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().to_string(), file_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_symbol_uri() {
|
||||||
|
let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20";
|
||||||
|
let parsed = MentionUri::parse(symbol_uri).unwrap();
|
||||||
|
match &parsed {
|
||||||
|
MentionUri::Symbol {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
line_range,
|
||||||
|
} => {
|
||||||
|
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
|
||||||
|
assert_eq!(name, "MySymbol");
|
||||||
|
assert_eq!(line_range.start, 9);
|
||||||
|
assert_eq!(line_range.end, 19);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Symbol variant"),
|
||||||
|
}
|
||||||
|
assert_eq!(parsed.to_uri().to_string(), symbol_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_selection_uri() {
|
||||||
|
let selection_uri = "file:///path/to/file.rs#L5:15";
|
||||||
|
let parsed = MentionUri::parse(selection_uri).unwrap();
|
||||||
|
match &parsed {
|
||||||
|
MentionUri::Selection { path, line_range } => {
|
||||||
|
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
|
||||||
|
assert_eq!(line_range.start, 4);
|
||||||
|
assert_eq!(line_range.end, 14);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Selection variant"),
|
||||||
|
}
|
||||||
|
assert_eq!(parsed.to_uri().to_string(), selection_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_thread_uri() {
|
||||||
|
let thread_uri = "zed:///agent/thread/session123?name=Thread%20name";
|
||||||
|
let parsed = MentionUri::parse(thread_uri).unwrap();
|
||||||
|
match &parsed {
|
||||||
|
MentionUri::Thread {
|
||||||
|
id: thread_id,
|
||||||
|
name,
|
||||||
|
} => {
|
||||||
|
assert_eq!(thread_id.to_string(), "session123");
|
||||||
|
assert_eq!(name, "Thread name");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Thread variant"),
|
||||||
|
}
|
||||||
|
assert_eq!(parsed.to_uri().to_string(), thread_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_rule_uri() {
|
||||||
|
let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some%20rule";
|
||||||
|
let parsed = MentionUri::parse(rule_uri).unwrap();
|
||||||
|
match &parsed {
|
||||||
|
MentionUri::Rule { id, name } => {
|
||||||
|
assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52");
|
||||||
|
assert_eq!(name, "Some rule");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Rule variant"),
|
||||||
|
}
|
||||||
|
assert_eq!(parsed.to_uri().to_string(), rule_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_fetch_http_uri() {
|
||||||
|
let http_uri = "http://example.com/path?query=value#fragment";
|
||||||
|
let parsed = MentionUri::parse(http_uri).unwrap();
|
||||||
|
match &parsed {
|
||||||
|
MentionUri::Fetch { url } => {
|
||||||
|
assert_eq!(url.to_string(), http_uri);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Fetch variant"),
|
||||||
|
}
|
||||||
|
assert_eq!(parsed.to_uri().to_string(), http_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_fetch_https_uri() {
|
||||||
|
let https_uri = "https://example.com/api/endpoint";
|
||||||
|
let parsed = MentionUri::parse(https_uri).unwrap();
|
||||||
|
match &parsed {
|
||||||
|
MentionUri::Fetch { url } => {
|
||||||
|
assert_eq!(url.to_string(), https_uri);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Fetch variant"),
|
||||||
|
}
|
||||||
|
assert_eq!(parsed.to_uri().to_string(), https_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_scheme() {
|
||||||
|
assert!(MentionUri::parse("ftp://example.com").is_err());
|
||||||
|
assert!(MentionUri::parse("ssh://example.com").is_err());
|
||||||
|
assert!(MentionUri::parse("unknown://example.com").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_zed_path() {
|
||||||
|
assert!(MentionUri::parse("zed:///invalid/path").is_err());
|
||||||
|
assert!(MentionUri::parse("zed:///agent/unknown/test").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_line_range_format() {
|
||||||
|
// Missing L prefix
|
||||||
|
assert!(MentionUri::parse("file:///path/to/file.rs#10:20").is_err());
|
||||||
|
|
||||||
|
// Missing colon separator
|
||||||
|
assert!(MentionUri::parse("file:///path/to/file.rs#L1020").is_err());
|
||||||
|
|
||||||
|
// Invalid numbers
|
||||||
|
assert!(MentionUri::parse("file:///path/to/file.rs#L10:abc").is_err());
|
||||||
|
assert!(MentionUri::parse("file:///path/to/file.rs#Labc:20").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_query_parameters() {
|
||||||
|
// Invalid query parameter name
|
||||||
|
assert!(MentionUri::parse("file:///path/to/file.rs#L10:20?invalid=test").is_err());
|
||||||
|
|
||||||
|
// Too many query parameters
|
||||||
|
assert!(
|
||||||
|
MentionUri::parse("file:///path/to/file.rs#L10:20?symbol=test&another=param").is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zero_based_line_numbers() {
|
||||||
|
// Test that 0-based line numbers are rejected (should be 1-based)
|
||||||
|
assert!(MentionUri::parse("file:///path/to/file.rs#L0:10").is_err());
|
||||||
|
assert!(MentionUri::parse("file:///path/to/file.rs#L1:0").is_err());
|
||||||
|
assert!(MentionUri::parse("file:///path/to/file.rs#L0:0").is_err());
|
||||||
|
}
|
||||||
|
}
|
|
@ -205,6 +205,22 @@ impl ThreadStore {
|
||||||
(this, ready_rx)
|
(this, ready_rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn fake(project: Entity<Project>, cx: &mut App) -> Self {
|
||||||
|
Self {
|
||||||
|
project,
|
||||||
|
tools: cx.new(|_| ToolWorkingSet::default()),
|
||||||
|
prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
|
||||||
|
prompt_store: None,
|
||||||
|
context_server_tool_ids: HashMap::default(),
|
||||||
|
threads: Vec::new(),
|
||||||
|
project_context: SharedProjectContext::default(),
|
||||||
|
reload_system_prompt_tx: mpsc::channel(0).0,
|
||||||
|
_reload_system_prompt_task: Task::ready(()),
|
||||||
|
_subscriptions: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_project_event(
|
fn handle_project_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_project: Entity<Project>,
|
_project: Entity<Project>,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::{AgentResponseEvent, Thread, templates::Templates};
|
use crate::{AgentResponseEvent, Thread, templates::Templates};
|
||||||
use crate::{
|
use crate::{
|
||||||
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool,
|
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool,
|
||||||
FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool,
|
FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool,
|
||||||
ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
|
OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
|
||||||
};
|
};
|
||||||
use acp_thread::ModelSelector;
|
use acp_thread::ModelSelector;
|
||||||
use agent_client_protocol as acp;
|
use agent_client_protocol as acp;
|
||||||
|
@ -516,10 +516,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||||
})?;
|
})?;
|
||||||
log::debug!("Found session for: {}", session_id);
|
log::debug!("Found session for: {}", session_id);
|
||||||
|
|
||||||
// Convert prompt to message
|
let message: Vec<MessageContent> = params
|
||||||
let message = convert_prompt_to_message(params.prompt);
|
.prompt
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
log::info!("Converted prompt to message: {} chars", message.len());
|
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 model using the ModelSelector capability (always available for agent2)
|
||||||
// Get the selected model from the thread directly
|
// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::MessageContent;
|
||||||
use acp_thread::AgentConnection;
|
use acp_thread::AgentConnection;
|
||||||
use action_log::ActionLog;
|
use action_log::ActionLog;
|
||||||
use agent_client_protocol::{self as acp};
|
use agent_client_protocol::{self as acp};
|
||||||
|
@ -13,8 +14,8 @@ use gpui::{
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
|
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
|
||||||
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
|
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, Role, StopReason,
|
||||||
StopReason, fake_provider::FakeLanguageModel,
|
fake_provider::FakeLanguageModel,
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::ProjectContext;
|
use prompt_store::ProjectContext;
|
||||||
|
@ -272,14 +273,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
message.content,
|
message.content,
|
||||||
vec![
|
vec![
|
||||||
MessageContent::ToolResult(LanguageModelToolResult {
|
language_model::MessageContent::ToolResult(LanguageModelToolResult {
|
||||||
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
|
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
|
||||||
tool_name: ToolRequiringPermission.name().into(),
|
tool_name: ToolRequiringPermission.name().into(),
|
||||||
is_error: false,
|
is_error: false,
|
||||||
content: "Allowed".into(),
|
content: "Allowed".into(),
|
||||||
output: Some("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_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
|
||||||
tool_name: ToolRequiringPermission.name().into(),
|
tool_name: ToolRequiringPermission.name().into(),
|
||||||
is_error: true,
|
is_error: true,
|
||||||
|
@ -312,13 +313,15 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||||
let message = completion.messages.last().unwrap();
|
let message = completion.messages.last().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
message.content,
|
message.content,
|
||||||
vec![MessageContent::ToolResult(LanguageModelToolResult {
|
vec![language_model::MessageContent::ToolResult(
|
||||||
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
|
LanguageModelToolResult {
|
||||||
tool_name: ToolRequiringPermission.name().into(),
|
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
|
||||||
is_error: false,
|
tool_name: ToolRequiringPermission.name().into(),
|
||||||
content: "Allowed".into(),
|
is_error: false,
|
||||||
output: Some("Allowed".into())
|
content: "Allowed".into(),
|
||||||
})]
|
output: Some("Allowed".into())
|
||||||
|
}
|
||||||
|
)]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Simulate a final tool call, ensuring we don't trigger authorization.
|
// 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();
|
let message = completion.messages.last().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
message.content,
|
message.content,
|
||||||
vec![MessageContent::ToolResult(LanguageModelToolResult {
|
vec![language_model::MessageContent::ToolResult(
|
||||||
tool_use_id: "tool_id_4".into(),
|
LanguageModelToolResult {
|
||||||
tool_name: ToolRequiringPermission.name().into(),
|
tool_use_id: "tool_id_4".into(),
|
||||||
is_error: false,
|
tool_name: ToolRequiringPermission.name().into(),
|
||||||
content: "Allowed".into(),
|
is_error: false,
|
||||||
output: Some("Allowed".into())
|
content: "Allowed".into(),
|
||||||
})]
|
output: Some("Allowed".into())
|
||||||
|
}
|
||||||
|
)]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates};
|
use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates};
|
||||||
|
use acp_thread::MentionUri;
|
||||||
use action_log::ActionLog;
|
use action_log::ActionLog;
|
||||||
use agent_client_protocol as acp;
|
use agent_client_protocol as acp;
|
||||||
use agent_settings::{AgentProfileId, AgentSettings};
|
use agent_settings::{AgentProfileId, AgentSettings};
|
||||||
|
@ -13,10 +14,10 @@ use futures::{
|
||||||
};
|
};
|
||||||
use gpui::{App, Context, Entity, SharedString, Task};
|
use gpui::{App, Context, Entity, SharedString, Task};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage,
|
||||||
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
|
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
|
||||||
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
|
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
|
||||||
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role, StopReason,
|
LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason,
|
||||||
};
|
};
|
||||||
use log;
|
use log;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
@ -25,7 +26,8 @@ use schemars::{JsonSchema, Schema};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, update_settings_file};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::{cell::RefCell, collections::BTreeMap, fmt::Write, rc::Rc, sync::Arc};
|
use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc};
|
||||||
|
use std::{fmt::Write, ops::Range};
|
||||||
use util::{ResultExt, markdown::MarkdownCodeBlock};
|
use util::{ResultExt, markdown::MarkdownCodeBlock};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -34,6 +36,23 @@ pub struct AgentMessage {
|
||||||
pub content: Vec<MessageContent>,
|
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 {
|
impl AgentMessage {
|
||||||
pub fn to_markdown(&self) -> String {
|
pub fn to_markdown(&self) -> String {
|
||||||
let mut markdown = format!("## {}\n", self.role);
|
let mut markdown = format!("## {}\n", self.role);
|
||||||
|
@ -93,6 +112,9 @@ impl AgentMessage {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MessageContent::Mention { uri, .. } => {
|
||||||
|
write!(markdown, "{}", uri.as_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.
|
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
|
||||||
pub fn send(
|
pub fn send(
|
||||||
&mut self,
|
&mut self,
|
||||||
content: impl Into<MessageContent>,
|
content: impl Into<UserMessage>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>> {
|
) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>> {
|
||||||
let content = content.into();
|
let content = content.into().0;
|
||||||
|
|
||||||
let model = self.selected_model.clone();
|
let model = self.selected_model.clone();
|
||||||
log::info!("Thread::send called with model: {:?}", model.name());
|
log::info!("Thread::send called with model: {:?}", model.name());
|
||||||
log::debug!("Thread::send content: {:?}", content);
|
log::debug!("Thread::send content: {:?}", content);
|
||||||
|
@ -230,7 +253,7 @@ impl Thread {
|
||||||
let user_message_ix = self.messages.len();
|
let user_message_ix = self.messages.len();
|
||||||
self.messages.push(AgentMessage {
|
self.messages.push(AgentMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: vec![content],
|
content,
|
||||||
});
|
});
|
||||||
log::info!("Total messages in thread: {}", self.messages.len());
|
log::info!("Total messages in thread: {}", self.messages.len());
|
||||||
self.running_turn = Some(cx.spawn(async move |thread, cx| {
|
self.running_turn = Some(cx.spawn(async move |thread, cx| {
|
||||||
|
@ -353,7 +376,7 @@ impl Thread {
|
||||||
log::debug!("System message built");
|
log::debug!("System message built");
|
||||||
AgentMessage {
|
AgentMessage {
|
||||||
role: Role::System,
|
role: Role::System,
|
||||||
content: vec![prompt.into()],
|
content: vec![prompt.as_str().into()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -701,11 +724,7 @@ impl Thread {
|
||||||
},
|
},
|
||||||
message.content.len()
|
message.content.len()
|
||||||
);
|
);
|
||||||
LanguageModelRequestMessage {
|
message.to_request()
|
||||||
role: message.role,
|
|
||||||
content: message.content.clone(),
|
|
||||||
cache: false,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
messages
|
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
|
pub trait AgentTool
|
||||||
where
|
where
|
||||||
Self: 'static + Sized,
|
Self: 'static + Sized,
|
||||||
|
@ -1102,3 +1135,246 @@ impl std::ops::DerefMut for ToolCallEventStreamReceiver {
|
||||||
&mut self.0
|
&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_FETCH_TAG: &str = "<fetched_urls>";
|
||||||
|
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 fetch_context = OPEN_FETCH_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) => {
|
||||||
|
write!(
|
||||||
|
&mut symbol_context,
|
||||||
|
"\n{}",
|
||||||
|
MarkdownCodeBlock {
|
||||||
|
tag: &codeblock_tag(&path, None),
|
||||||
|
text: &content.to_string(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
MentionUri::Symbol {
|
||||||
|
path, line_range, ..
|
||||||
|
}
|
||||||
|
| MentionUri::Selection {
|
||||||
|
path, line_range, ..
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
|
&mut rules_context,
|
||||||
|
"\n{}",
|
||||||
|
MarkdownCodeBlock {
|
||||||
|
tag: &codeblock_tag(&path, Some(line_range)),
|
||||||
|
text: &content
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
MentionUri::Thread { .. } => {
|
||||||
|
write!(&mut thread_context, "\n{}\n", content).ok();
|
||||||
|
}
|
||||||
|
MentionUri::TextThread { .. } => {
|
||||||
|
write!(&mut thread_context, "\n{}\n", content).ok();
|
||||||
|
}
|
||||||
|
MentionUri::Rule { .. } => {
|
||||||
|
write!(
|
||||||
|
&mut rules_context,
|
||||||
|
"\n{}",
|
||||||
|
MarkdownCodeBlock {
|
||||||
|
tag: "",
|
||||||
|
text: &content
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
MentionUri::Fetch { url } => {
|
||||||
|
write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
language_model::MessageContent::Text(uri.as_link().to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 fetch_context.len() > OPEN_FETCH_TAG.len() {
|
||||||
|
fetch_context.push_str("</fetched_urls>\n");
|
||||||
|
message
|
||||||
|
.content
|
||||||
|
.push(language_model::MessageContent::Text(fetch_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, line_range: Option<&Range<u32>>) -> 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());
|
||||||
|
|
||||||
|
if let Some(range) = line_range {
|
||||||
|
if range.start == range.end {
|
||||||
|
let _ = write!(result, ":{}", range.start + 1);
|
||||||
|
} else {
|
||||||
|
let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -93,6 +93,7 @@ time.workspace = true
|
||||||
time_format.workspace = true
|
time_format.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
ui_input.workspace = true
|
ui_input.workspace = true
|
||||||
|
url.workspace = true
|
||||||
urlencoding.workspace = true
|
urlencoding.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
@ -102,6 +103,8 @@ workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
agent = { workspace = true, features = ["test-support"] }
|
||||||
|
assistant_context = { workspace = true, features = ["test-support"] }
|
||||||
assistant_tools.workspace = true
|
assistant_tools.workspace = true
|
||||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,18 +1,20 @@
|
||||||
use acp_thread::{
|
use acp_thread::{
|
||||||
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
|
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
|
||||||
LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
|
LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
|
||||||
};
|
};
|
||||||
use acp_thread::{AgentConnection, Plan};
|
use acp_thread::{AgentConnection, Plan};
|
||||||
use action_log::ActionLog;
|
use action_log::ActionLog;
|
||||||
|
use agent::{TextThreadStore, ThreadStore};
|
||||||
use agent_client_protocol as acp;
|
use agent_client_protocol as acp;
|
||||||
use agent_servers::AgentServer;
|
use agent_servers::AgentServer;
|
||||||
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
||||||
use audio::{Audio, Sound};
|
use audio::{Audio, Sound};
|
||||||
use buffer_diff::BufferDiff;
|
use buffer_diff::BufferDiff;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
|
use editor::scroll::Autoscroll;
|
||||||
use editor::{
|
use editor::{
|
||||||
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
|
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
|
||||||
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
|
EditorStyle, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects,
|
||||||
};
|
};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -27,7 +29,11 @@ use language::{Buffer, Language};
|
||||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{CompletionIntent, Project};
|
use project::{CompletionIntent, Project};
|
||||||
|
use prompt_store::PromptId;
|
||||||
|
use rope::Point;
|
||||||
use settings::{Settings as _, SettingsStore};
|
use settings::{Settings as _, SettingsStore};
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
|
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
@ -41,6 +47,7 @@ use ui::{
|
||||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||||
use workspace::{CollaboratorId, Workspace};
|
use workspace::{CollaboratorId, Workspace};
|
||||||
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
|
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
|
||||||
|
use zed_actions::assistant::OpenRulesLibrary;
|
||||||
|
|
||||||
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
|
||||||
use crate::acp::message_history::MessageHistory;
|
use crate::acp::message_history::MessageHistory;
|
||||||
|
@ -57,6 +64,8 @@ pub struct AcpThreadView {
|
||||||
agent: Rc<dyn AgentServer>,
|
agent: Rc<dyn AgentServer>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
|
text_thread_store: WeakEntity<TextThreadStore>,
|
||||||
thread_state: ThreadState,
|
thread_state: ThreadState,
|
||||||
diff_editors: HashMap<EntityId, Entity<Editor>>,
|
diff_editors: HashMap<EntityId, Entity<Editor>>,
|
||||||
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
|
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
|
||||||
|
@ -103,6 +112,8 @@ impl AcpThreadView {
|
||||||
agent: Rc<dyn AgentServer>,
|
agent: Rc<dyn AgentServer>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
|
text_thread_store: WeakEntity<TextThreadStore>,
|
||||||
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
|
||||||
min_lines: usize,
|
min_lines: usize,
|
||||||
max_lines: Option<usize>,
|
max_lines: Option<usize>,
|
||||||
|
@ -140,6 +151,8 @@ impl AcpThreadView {
|
||||||
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
|
||||||
mention_set.clone(),
|
mention_set.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
|
thread_store.clone(),
|
||||||
|
text_thread_store.clone(),
|
||||||
cx.weak_entity(),
|
cx.weak_entity(),
|
||||||
))));
|
))));
|
||||||
editor.set_context_menu_options(ContextMenuOptions {
|
editor.set_context_menu_options(ContextMenuOptions {
|
||||||
|
@ -183,6 +196,8 @@ impl AcpThreadView {
|
||||||
agent: agent.clone(),
|
agent: agent.clone(),
|
||||||
workspace: workspace.clone(),
|
workspace: workspace.clone(),
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
|
thread_store,
|
||||||
|
text_thread_store,
|
||||||
thread_state: Self::initial_state(agent, workspace, project, window, cx),
|
thread_state: Self::initial_state(agent, workspace, project, window, cx),
|
||||||
message_editor,
|
message_editor,
|
||||||
message_set_from_history: None,
|
message_set_from_history: None,
|
||||||
|
@ -376,81 +391,111 @@ impl AcpThreadView {
|
||||||
let mut ix = 0;
|
let mut ix = 0;
|
||||||
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
||||||
let project = self.project.clone();
|
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) =
|
let Some(thread_store) = self.thread_store.upgrade() else {
|
||||||
self.mention_set.lock().path_for_crease_id(crease_id)
|
return;
|
||||||
{
|
};
|
||||||
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
|
let Some(text_thread_store) = self.text_thread_store.upgrade() else {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ix < text.len() {
|
|
||||||
let last_chunk = text[ix..].trim_end();
|
|
||||||
if !last_chunk.is_empty() {
|
|
||||||
chunks.push(last_chunk.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if chunks.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(thread) = self.thread() else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
let contents =
|
||||||
let result = task.await;
|
self.mention_set
|
||||||
|
.lock()
|
||||||
|
.contents(project, thread_store, text_thread_store, window, cx);
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
if let Err(err) = result {
|
let contents = match contents.await {
|
||||||
this.last_error =
|
Ok(contents) => contents,
|
||||||
Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
|
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;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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().to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
ix = crease_range.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ix < text.len() {
|
||||||
|
let last_chunk = text[ix..].trim_end();
|
||||||
|
if !last_chunk.is_empty() {
|
||||||
|
chunks.push(last_chunk.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if chunks.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(thread) = this.thread() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let result = task.await;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let mention_set = this.mention_set.clone();
|
||||||
|
|
||||||
|
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();
|
.detach();
|
||||||
|
|
||||||
let mention_set = self.mention_set.clone();
|
|
||||||
|
|
||||||
self.set_editor_is_expanded(false, cx);
|
|
||||||
|
|
||||||
self.message_editor.update(cx, |editor, cx| {
|
|
||||||
editor.clear(window, cx);
|
|
||||||
editor.remove_creases(mention_set.lock().drain(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.scroll_to_bottom(cx);
|
|
||||||
|
|
||||||
self.message_history.borrow_mut().push(chunks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn previous_history_message(
|
fn previous_history_message(
|
||||||
|
@ -563,16 +608,18 @@ impl AcpThreadView {
|
||||||
acp::ContentBlock::Text(text_content) => {
|
acp::ContentBlock::Text(text_content) => {
|
||||||
text.push_str(&text_content.text);
|
text.push_str(&text_content.text);
|
||||||
}
|
}
|
||||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||||
let path = Path::new(&resource_link.uri);
|
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 start = text.len();
|
||||||
let content = MentionPath::new(&path).to_string();
|
let _ = write!(&mut text, "{}", MentionUri::File(path).to_uri());
|
||||||
text.push_str(&content);
|
|
||||||
let end = text.len();
|
let end = text.len();
|
||||||
if let Some(project_path) =
|
if let Some(project_path) = project_path {
|
||||||
project.read(cx).project_path_for_absolute_path(&path, cx)
|
let filename: SharedString = project_path
|
||||||
{
|
.path
|
||||||
let filename: SharedString = path
|
|
||||||
.file_name()
|
.file_name()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
|
@ -583,7 +630,8 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
acp::ContentBlock::Image(_)
|
acp::ContentBlock::Image(_)
|
||||||
| acp::ContentBlock::Audio(_)
|
| acp::ContentBlock::Audio(_)
|
||||||
| acp::ContentBlock::Resource(_) => {}
|
| acp::ContentBlock::Resource(_)
|
||||||
|
| acp::ContentBlock::ResourceLink(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -602,18 +650,23 @@ impl AcpThreadView {
|
||||||
};
|
};
|
||||||
|
|
||||||
let anchor = snapshot.anchor_before(range.start);
|
let anchor = snapshot.anchor_before(range.start);
|
||||||
let crease_id = crate::context_picker::insert_crease_for_mention(
|
if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) {
|
||||||
anchor.excerpt_id,
|
let crease_id = crate::context_picker::insert_crease_for_mention(
|
||||||
anchor.text_anchor,
|
anchor.excerpt_id,
|
||||||
range.end - range.start,
|
anchor.text_anchor,
|
||||||
filename,
|
range.end - range.start,
|
||||||
crease_icon_path,
|
filename,
|
||||||
message_editor.clone(),
|
crease_icon_path,
|
||||||
window,
|
message_editor.clone(),
|
||||||
cx,
|
window,
|
||||||
);
|
cx,
|
||||||
if let Some(crease_id) = crease_id {
|
);
|
||||||
mention_set.lock().insert(crease_id, project_path);
|
|
||||||
|
if let Some(crease_id) = crease_id {
|
||||||
|
mention_set
|
||||||
|
.lock()
|
||||||
|
.insert(crease_id, MentionUri::File(project_path));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2562,26 +2615,95 @@ impl AcpThreadView {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(mention_path) = MentionPath::try_parse(&url) {
|
if let Some(mention) = MentionUri::parse(&url).log_err() {
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| match mention {
|
||||||
let project = workspace.project();
|
MentionUri::File(path) => {
|
||||||
let Some((path, entry)) = project.update(cx, |project, cx| {
|
let project = workspace.project();
|
||||||
let path = project.find_project_path(mention_path.path(), cx)?;
|
let Some((path, entry)) = project.update(cx, |project, cx| {
|
||||||
let entry = project.entry_for_path(&path, cx)?;
|
let path = project.find_project_path(path, cx)?;
|
||||||
Some((path, entry))
|
let entry = project.entry_for_path(&path, cx)?;
|
||||||
}) else {
|
Some((path, entry))
|
||||||
return;
|
}) else {
|
||||||
};
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
if entry.is_dir() {
|
if entry.is_dir() {
|
||||||
project.update(cx, |_, cx| {
|
project.update(cx, |_, cx| {
|
||||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
workspace
|
workspace
|
||||||
.open_path(path, None, true, window, cx)
|
.open_path(path, None, true, window, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MentionUri::Symbol {
|
||||||
|
path, line_range, ..
|
||||||
|
}
|
||||||
|
| MentionUri::Selection { path, line_range } => {
|
||||||
|
let project = workspace.project();
|
||||||
|
let Some((path, _)) = 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
let item = workspace.open_path(path, None, true, window, cx);
|
||||||
|
window
|
||||||
|
.spawn(cx, async move |cx| {
|
||||||
|
let Some(editor) = item.await?.downcast::<Editor>() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let range = Point::new(line_range.start as u32, 0)
|
||||||
|
..Point::new(line_range.start as u32, 0);
|
||||||
|
editor
|
||||||
|
.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.change_selections(
|
||||||
|
SelectionEffects::scroll(Autoscroll::center()),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
|s| s.select_ranges(vec![range]),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
MentionUri::Thread { id, .. } => {
|
||||||
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel
|
||||||
|
.open_thread_by_id(&id, window, cx)
|
||||||
|
.detach_and_log_err(cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MentionUri::TextThread { path, .. } => {
|
||||||
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel
|
||||||
|
.open_saved_prompt_editor(path.as_path().into(), window, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MentionUri::Rule { id, .. } => {
|
||||||
|
let PromptId::User { uuid } = id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
window.dispatch_action(
|
||||||
|
Box::new(OpenRulesLibrary {
|
||||||
|
prompt_to_select: Some(uuid.0),
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MentionUri::Fetch { url } => {
|
||||||
|
cx.open_url(url.as_str());
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
cx.open_url(&url);
|
cx.open_url(&url);
|
||||||
|
@ -2966,7 +3088,7 @@ impl AcpThreadView {
|
||||||
.unwrap_or(path.path.as_os_str())
|
.unwrap_or(path.path.as_os_str())
|
||||||
.display()
|
.display()
|
||||||
.to_string();
|
.to_string();
|
||||||
let completion = ContextPickerCompletionProvider::completion_for_path(
|
let Some(completion) = ContextPickerCompletionProvider::completion_for_path(
|
||||||
path,
|
path,
|
||||||
&path_prefix,
|
&path_prefix,
|
||||||
false,
|
false,
|
||||||
|
@ -2975,8 +3097,11 @@ impl AcpThreadView {
|
||||||
anchor..anchor,
|
anchor..anchor,
|
||||||
self.message_editor.clone(),
|
self.message_editor.clone(),
|
||||||
self.mention_set.clone(),
|
self.mention_set.clone(),
|
||||||
|
self.project.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
self.message_editor.update(cx, |message_editor, cx| {
|
self.message_editor.update(cx, |message_editor, cx| {
|
||||||
message_editor.edit(
|
message_editor.edit(
|
||||||
|
@ -3117,7 +3242,7 @@ fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||||
|
|
||||||
style.base_text_style = text_style;
|
style.base_text_style = text_style;
|
||||||
style.link_callback = Some(Rc::new(move |url, cx| {
|
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();
|
let colors = cx.theme().colors();
|
||||||
Some(TextStyleRefinement {
|
Some(TextStyleRefinement {
|
||||||
background_color: Some(colors.element_background),
|
background_color: Some(colors.element_background),
|
||||||
|
@ -3434,6 +3559,8 @@ mod tests {
|
||||||
Rc::new(agent),
|
Rc::new(agent),
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
|
WeakEntity::new_invalid(),
|
||||||
|
WeakEntity::new_invalid(),
|
||||||
Rc::new(RefCell::new(MessageHistory::default())),
|
Rc::new(RefCell::new(MessageHistory::default())),
|
||||||
1,
|
1,
|
||||||
None,
|
None,
|
||||||
|
@ -3536,6 +3663,8 @@ mod tests {
|
||||||
Rc::new(agent),
|
Rc::new(agent),
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
|
WeakEntity::new_invalid(),
|
||||||
|
WeakEntity::new_invalid(),
|
||||||
Rc::new(RefCell::new(MessageHistory::default())),
|
Rc::new(RefCell::new(MessageHistory::default())),
|
||||||
1,
|
1,
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -924,6 +924,9 @@ impl AgentPanel {
|
||||||
agent: crate::ExternalAgent,
|
agent: crate::ExternalAgent,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let thread_store = self.thread_store.clone();
|
||||||
|
let text_thread_store = self.context_store.clone();
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let server: Rc<dyn AgentServer> = match agent_choice {
|
let server: Rc<dyn AgentServer> = match agent_choice {
|
||||||
Some(agent) => {
|
Some(agent) => {
|
||||||
|
@ -962,6 +965,8 @@ impl AgentPanel {
|
||||||
server,
|
server,
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
project,
|
project,
|
||||||
|
thread_store.downgrade(),
|
||||||
|
text_thread_store.downgrade(),
|
||||||
message_history,
|
message_history,
|
||||||
MIN_EDITOR_LINES,
|
MIN_EDITOR_LINES,
|
||||||
Some(MAX_EDITOR_LINES),
|
Some(MAX_EDITOR_LINES),
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
mod completion_provider;
|
mod completion_provider;
|
||||||
mod fetch_context_picker;
|
pub(crate) mod fetch_context_picker;
|
||||||
pub(crate) mod file_context_picker;
|
pub(crate) mod file_context_picker;
|
||||||
mod rules_context_picker;
|
pub(crate) mod rules_context_picker;
|
||||||
mod symbol_context_picker;
|
pub(crate) mod symbol_context_picker;
|
||||||
mod thread_context_picker;
|
pub(crate) mod thread_context_picker;
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
|
use collections::HashSet;
|
||||||
pub use completion_provider::ContextPickerCompletionProvider;
|
pub use completion_provider::ContextPickerCompletionProvider;
|
||||||
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
|
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
|
||||||
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
||||||
|
@ -45,7 +46,7 @@ use agent::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum ContextPickerEntry {
|
pub(crate) enum ContextPickerEntry {
|
||||||
Mode(ContextPickerMode),
|
Mode(ContextPickerMode),
|
||||||
Action(ContextPickerAction),
|
Action(ContextPickerAction),
|
||||||
}
|
}
|
||||||
|
@ -74,7 +75,7 @@ impl ContextPickerEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum ContextPickerMode {
|
pub(crate) enum ContextPickerMode {
|
||||||
File,
|
File,
|
||||||
Symbol,
|
Symbol,
|
||||||
Fetch,
|
Fetch,
|
||||||
|
@ -83,7 +84,7 @@ enum ContextPickerMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum ContextPickerAction {
|
pub(crate) enum ContextPickerAction {
|
||||||
AddSelections,
|
AddSelections,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,7 +532,7 @@ impl ContextPicker {
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
|
|
||||||
recent_context_picker_entries(
|
recent_context_picker_entries_with_store(
|
||||||
context_store,
|
context_store,
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.text_thread_store.clone(),
|
self.text_thread_store.clone(),
|
||||||
|
@ -585,7 +586,8 @@ impl Render for ContextPicker {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
enum RecentEntry {
|
|
||||||
|
pub(crate) enum RecentEntry {
|
||||||
File {
|
File {
|
||||||
project_path: ProjectPath,
|
project_path: ProjectPath,
|
||||||
path_prefix: Arc<str>,
|
path_prefix: Arc<str>,
|
||||||
|
@ -593,7 +595,7 @@ enum RecentEntry {
|
||||||
Thread(ThreadContextEntry),
|
Thread(ThreadContextEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn available_context_picker_entries(
|
pub(crate) fn available_context_picker_entries(
|
||||||
prompt_store: &Option<Entity<PromptStore>>,
|
prompt_store: &Option<Entity<PromptStore>>,
|
||||||
thread_store: &Option<WeakEntity<ThreadStore>>,
|
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||||
workspace: &Entity<Workspace>,
|
workspace: &Entity<Workspace>,
|
||||||
|
@ -630,24 +632,56 @@ fn available_context_picker_entries(
|
||||||
entries
|
entries
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recent_context_picker_entries(
|
fn recent_context_picker_entries_with_store(
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||||
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||||
workspace: Entity<Workspace>,
|
workspace: Entity<Workspace>,
|
||||||
exclude_path: Option<ProjectPath>,
|
exclude_path: Option<ProjectPath>,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
|
) -> Vec<RecentEntry> {
|
||||||
|
let project = workspace.read(cx).project();
|
||||||
|
|
||||||
|
let mut exclude_paths = context_store.read(cx).file_paths(cx);
|
||||||
|
exclude_paths.extend(exclude_path);
|
||||||
|
|
||||||
|
let exclude_paths = exclude_paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let exclude_threads = context_store.read(cx).thread_ids();
|
||||||
|
|
||||||
|
recent_context_picker_entries(
|
||||||
|
thread_store,
|
||||||
|
text_thread_store,
|
||||||
|
workspace,
|
||||||
|
&exclude_paths,
|
||||||
|
exclude_threads,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn recent_context_picker_entries(
|
||||||
|
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||||
|
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
exclude_paths: &HashSet<PathBuf>,
|
||||||
|
exclude_threads: &HashSet<ThreadId>,
|
||||||
|
cx: &App,
|
||||||
) -> Vec<RecentEntry> {
|
) -> Vec<RecentEntry> {
|
||||||
let mut recent = Vec::with_capacity(6);
|
let mut recent = Vec::with_capacity(6);
|
||||||
let mut current_files = context_store.read(cx).file_paths(cx);
|
|
||||||
current_files.extend(exclude_path);
|
|
||||||
let workspace = workspace.read(cx);
|
let workspace = workspace.read(cx);
|
||||||
let project = workspace.project().read(cx);
|
let project = workspace.project().read(cx);
|
||||||
|
|
||||||
recent.extend(
|
recent.extend(
|
||||||
workspace
|
workspace
|
||||||
.recent_navigation_history_iter(cx)
|
.recent_navigation_history_iter(cx)
|
||||||
.filter(|(path, _)| !current_files.contains(path))
|
.filter(|(_, abs_path)| {
|
||||||
|
abs_path
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |path| !exclude_paths.contains(path.as_path()))
|
||||||
|
})
|
||||||
.take(4)
|
.take(4)
|
||||||
.filter_map(|(project_path, _)| {
|
.filter_map(|(project_path, _)| {
|
||||||
project
|
project
|
||||||
|
@ -659,8 +693,6 @@ fn recent_context_picker_entries(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let current_threads = context_store.read(cx).thread_ids();
|
|
||||||
|
|
||||||
let active_thread_id = workspace
|
let active_thread_id = workspace
|
||||||
.panel::<AgentPanel>(cx)
|
.panel::<AgentPanel>(cx)
|
||||||
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
|
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
|
||||||
|
@ -672,7 +704,7 @@ fn recent_context_picker_entries(
|
||||||
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
|
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
|
||||||
.filter(|(_, thread)| match thread {
|
.filter(|(_, thread)| match thread {
|
||||||
ThreadContextEntry::Thread { id, .. } => {
|
ThreadContextEntry::Thread { id, .. } => {
|
||||||
Some(id) != active_thread_id && !current_threads.contains(id)
|
Some(id) != active_thread_id && !exclude_threads.contains(id)
|
||||||
}
|
}
|
||||||
ThreadContextEntry::Context { .. } => true,
|
ThreadContextEntry::Context { .. } => true,
|
||||||
})
|
})
|
||||||
|
@ -710,7 +742,7 @@ fn add_selections_as_context(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selection_ranges(
|
pub(crate) fn selection_ranges(
|
||||||
workspace: &Entity<Workspace>,
|
workspace: &Entity<Workspace>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
|
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
|
||||||
|
|
|
@ -35,7 +35,7 @@ use super::symbol_context_picker::search_symbols;
|
||||||
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
|
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
|
||||||
use super::{
|
use super::{
|
||||||
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
|
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
|
||||||
available_context_picker_entries, recent_context_picker_entries, selection_ranges,
|
available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges,
|
||||||
};
|
};
|
||||||
use crate::message_editor::ContextCreasesAddon;
|
use crate::message_editor::ContextCreasesAddon;
|
||||||
|
|
||||||
|
@ -787,7 +787,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||||
.and_then(|b| b.read(cx).file())
|
.and_then(|b| b.read(cx).file())
|
||||||
.map(|file| ProjectPath::from_file(file.as_ref(), cx));
|
.map(|file| ProjectPath::from_file(file.as_ref(), cx));
|
||||||
|
|
||||||
let recent_entries = recent_context_picker_entries(
|
let recent_entries = recent_context_picker_entries_with_store(
|
||||||
context_store.clone(),
|
context_store.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
text_thread_store.clone(),
|
text_thread_store.clone(),
|
||||||
|
|
|
@ -11,6 +11,9 @@ workspace = true
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/assistant_context.rs"
|
path = "src/assistant_context.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
agent_settings.workspace = true
|
agent_settings.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|
|
@ -138,6 +138,27 @@ impl ContextStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn fake(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
contexts: Default::default(),
|
||||||
|
contexts_metadata: Default::default(),
|
||||||
|
context_server_slash_command_ids: Default::default(),
|
||||||
|
host_contexts: Default::default(),
|
||||||
|
fs: project.read(cx).fs().clone(),
|
||||||
|
languages: project.read(cx).languages().clone(),
|
||||||
|
slash_commands: Arc::default(),
|
||||||
|
telemetry: project.read(cx).client().telemetry().clone(),
|
||||||
|
_watch_updates: Task::ready(None),
|
||||||
|
client: project.read(cx).client(),
|
||||||
|
project,
|
||||||
|
project_is_shared: false,
|
||||||
|
client_subscription: None,
|
||||||
|
_project_subscriptions: Default::default(),
|
||||||
|
prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_advertise_contexts(
|
async fn handle_advertise_contexts(
|
||||||
this: Entity<Self>,
|
this: Entity<Self>,
|
||||||
envelope: TypedEnvelope<proto::AdvertiseContexts>,
|
envelope: TypedEnvelope<proto::AdvertiseContexts>,
|
||||||
|
|
|
@ -12158,6 +12158,8 @@ impl Editor {
|
||||||
let clipboard_text = Cow::Borrowed(text);
|
let clipboard_text = Cow::Borrowed(text);
|
||||||
|
|
||||||
self.transact(window, cx, |this, window, cx| {
|
self.transact(window, cx, |this, window, cx| {
|
||||||
|
let had_active_edit_prediction = this.has_active_edit_prediction();
|
||||||
|
|
||||||
if let Some(mut clipboard_selections) = clipboard_selections {
|
if let Some(mut clipboard_selections) = clipboard_selections {
|
||||||
let old_selections = this.selections.all::<usize>(cx);
|
let old_selections = this.selections.all::<usize>(cx);
|
||||||
let all_selections_were_entire_line =
|
let all_selections_were_entire_line =
|
||||||
|
@ -12230,6 +12232,11 @@ impl Editor {
|
||||||
} else {
|
} else {
|
||||||
this.insert(&clipboard_text, window, cx);
|
this.insert(&clipboard_text, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let trigger_in_words =
|
||||||
|
this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
|
||||||
|
|
||||||
|
this.trigger_completion_on_input(&text, trigger_in_words, window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,15 @@ impl From<Uuid> for UserPromptId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PromptId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
PromptId::User { uuid } => write!(f, "{}", uuid.0),
|
||||||
|
PromptId::EditWorkflow => write!(f, "Edit workflow"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PromptStore {
|
pub struct PromptStore {
|
||||||
env: heed::Env,
|
env: heed::Env,
|
||||||
metadata_cache: RwLock<MetadataCache>,
|
metadata_cache: RwLock<MetadataCache>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue