Compare commits
14 Commits
agent-chec
...
remove-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfbb18476f | ||
|
|
978b75bba9 | ||
|
|
1f20d5bf54 | ||
|
|
9de04ce215 | ||
|
|
d8fc53608e | ||
|
|
39c19abdfd | ||
|
|
b105028c05 | ||
|
|
d2162446d0 | ||
|
|
360d4db87c | ||
|
|
44953375cc | ||
|
|
2444321756 | ||
|
|
13bf45dd4a | ||
|
|
b61b71405d | ||
|
|
cc5eb24066 |
10
.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml
vendored
10
.github/ISSUE_TEMPLATE/07_bug_windows_alpha.yml
vendored
@@ -1,15 +1,15 @@
|
||||
name: Bug Report (Windows)
|
||||
description: Zed Windows-Related Bugs
|
||||
name: Bug Report (Windows Alpha)
|
||||
description: Zed Windows Alpha Related Bugs
|
||||
type: "Bug"
|
||||
labels: ["windows"]
|
||||
title: "Windows: <a short description of the Windows bug>"
|
||||
title: "Windows Alpha: <a short description of the Windows bug>"
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the bug with a one line summary, and provide detailed reproduction steps
|
||||
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
|
||||
value: |
|
||||
<!-- Please insert a one line summary of the issue below -->
|
||||
<!-- Please insert a one-line summary of the issue below -->
|
||||
SUMMARY_SENTENCE_HERE
|
||||
|
||||
### Description
|
||||
|
||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -29,6 +29,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"ui",
|
||||
"url",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -196,6 +197,7 @@ dependencies = [
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"context_server",
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
@@ -20923,6 +20925,7 @@ dependencies = [
|
||||
"menu",
|
||||
"postage",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
|
||||
@@ -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>,
|
||||
@@ -254,6 +219,15 @@ impl ToolCall {
|
||||
}
|
||||
|
||||
if let Some(raw_output) = raw_output {
|
||||
if self.content.is_empty() {
|
||||
if let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx)
|
||||
{
|
||||
self.content
|
||||
.push(ToolCallContent::ContentBlock(ContentBlock::Markdown {
|
||||
markdown,
|
||||
}));
|
||||
}
|
||||
}
|
||||
self.raw_output = Some(raw_output);
|
||||
}
|
||||
}
|
||||
@@ -325,6 +299,7 @@ impl Display for ToolCallStatus {
|
||||
pub enum ContentBlock {
|
||||
Empty,
|
||||
Markdown { markdown: Entity<Markdown> },
|
||||
ResourceLink { resource_link: acp::ResourceLink },
|
||||
}
|
||||
|
||||
impl ContentBlock {
|
||||
@@ -356,36 +331,67 @@ impl ContentBlock {
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
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()))
|
||||
} else {
|
||||
resource_link.uri.clone()
|
||||
}
|
||||
if matches!(self, ContentBlock::Empty) {
|
||||
if let acp::ContentBlock::ResourceLink(resource_link) = block {
|
||||
*self = ContentBlock::ResourceLink { resource_link };
|
||||
return;
|
||||
}
|
||||
acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Resource(_) => String::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let new_content = self.extract_content_from_block(block);
|
||||
|
||||
match self {
|
||||
ContentBlock::Empty => {
|
||||
*self = ContentBlock::Markdown {
|
||||
markdown: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
new_content.into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
};
|
||||
*self = Self::create_markdown_block(new_content, language_registry, cx);
|
||||
}
|
||||
ContentBlock::Markdown { markdown } => {
|
||||
markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx));
|
||||
}
|
||||
ContentBlock::ResourceLink { resource_link } => {
|
||||
let existing_content = Self::resource_link_to_content(&resource_link.uri);
|
||||
let combined = format!("{}\n{}", existing_content, new_content);
|
||||
|
||||
*self = Self::create_markdown_block(combined, language_registry, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_link_to_content(uri: &str) -> String {
|
||||
if let Some(uri) = MentionUri::parse(&uri).log_err() {
|
||||
uri.to_link()
|
||||
} else {
|
||||
uri.to_string().clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_markdown_block(
|
||||
content: String,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> ContentBlock {
|
||||
ContentBlock::Markdown {
|
||||
markdown: cx
|
||||
.new(|cx| Markdown::new(content.into(), Some(language_registry.clone()), None, cx)),
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_content_from_block(&self, block: acp::ContentBlock) -> String {
|
||||
match block {
|
||||
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
|
||||
acp::ContentBlock::ResourceLink(resource_link) => {
|
||||
Self::resource_link_to_content(&resource_link.uri)
|
||||
}
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
resource:
|
||||
acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents {
|
||||
uri,
|
||||
..
|
||||
}),
|
||||
..
|
||||
}) => Self::resource_link_to_content(&uri),
|
||||
acp::ContentBlock::Image(_)
|
||||
| acp::ContentBlock::Audio(_)
|
||||
| acp::ContentBlock::Resource(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +399,7 @@ impl ContentBlock {
|
||||
match self {
|
||||
ContentBlock::Empty => "",
|
||||
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
|
||||
ContentBlock::ResourceLink { resource_link } => &resource_link.uri,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,6 +407,14 @@ impl ContentBlock {
|
||||
match self {
|
||||
ContentBlock::Empty => None,
|
||||
ContentBlock::Markdown { markdown } => Some(markdown),
|
||||
ContentBlock::ResourceLink { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resource_link(&self) -> Option<&acp::ResourceLink> {
|
||||
match self {
|
||||
ContentBlock::ResourceLink { resource_link } => Some(resource_link),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1266,6 +1281,48 @@ impl AcpThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn markdown_for_raw_output(
|
||||
raw_output: &serde_json::Value,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<Markdown>> {
|
||||
match raw_output {
|
||||
serde_json::Value::Null => None,
|
||||
serde_json::Value::Bool(value) => Some(cx.new(|cx| {
|
||||
Markdown::new(
|
||||
value.to_string().into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
serde_json::Value::Number(value) => Some(cx.new(|cx| {
|
||||
Markdown::new(
|
||||
value.to_string().into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
serde_json::Value::String(value) => Some(cx.new(|cx| {
|
||||
Markdown::new(
|
||||
value.clone().into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
value => Some(cx.new(|cx| {
|
||||
Markdown::new(
|
||||
format!("```json\n{}\n```", value).into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1278,7 +1335,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;
|
||||
|
||||
|
||||
125
crates/acp_thread/src/mention.rs
Normal file
125
crates/acp_thread/src/mention.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
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 {
|
||||
let file_path =
|
||||
PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
|
||||
|
||||
Ok(Self::File(file_path))
|
||||
}
|
||||
}
|
||||
"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());
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ assistant_tools.workspace = true
|
||||
chrono.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
@@ -60,6 +61,7 @@ workspace-hack.workspace = true
|
||||
ctor.workspace = true
|
||||
client = { workspace = true, "features" = ["test-support"] }
|
||||
clock = { workspace = true, "features" = ["test-support"] }
|
||||
context_server = { workspace = true, "features" = ["test-support"] }
|
||||
editor = { workspace = true, "features" = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, "features" = ["test-support"] }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{AgentResponseEvent, Thread, templates::Templates};
|
||||
use crate::{
|
||||
CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool,
|
||||
GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool,
|
||||
ThinkingTool, ToolCallAuthorization, WebSearchTool,
|
||||
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool,
|
||||
FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool,
|
||||
OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
|
||||
};
|
||||
use acp_thread::ModelSelector;
|
||||
use agent_client_protocol as acp;
|
||||
@@ -55,6 +55,7 @@ pub struct NativeAgent {
|
||||
project_context: Rc<RefCell<ProjectContext>>,
|
||||
project_context_needs_refresh: watch::Sender<()>,
|
||||
_maintain_project_context: Task<Result<()>>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
/// Shared templates for all threads
|
||||
templates: Arc<Templates>,
|
||||
project: Entity<Project>,
|
||||
@@ -90,6 +91,9 @@ impl NativeAgent {
|
||||
_maintain_project_context: cx.spawn(async move |this, cx| {
|
||||
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
|
||||
}),
|
||||
context_server_registry: cx.new(|cx| {
|
||||
ContextServerRegistry::new(project.read(cx).context_server_store(), cx)
|
||||
}),
|
||||
templates,
|
||||
project,
|
||||
prompt_store,
|
||||
@@ -385,7 +389,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
// Create AcpThread
|
||||
let acp_thread = cx.update(|cx| {
|
||||
cx.new(|cx| {
|
||||
acp_thread::AcpThread::new("agent2", self.clone(), project.clone(), session_id.clone(), cx)
|
||||
acp_thread::AcpThread::new(
|
||||
"agent2",
|
||||
self.clone(),
|
||||
project.clone(),
|
||||
session_id.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?;
|
||||
let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?;
|
||||
@@ -413,11 +423,21 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
log::warn!("No default model configured in settings");
|
||||
anyhow!("No default model configured. Please configure a default model in settings.")
|
||||
anyhow!(
|
||||
"No default model. Please configure a default model in settings."
|
||||
)
|
||||
})?;
|
||||
|
||||
let thread = cx.new(|cx| {
|
||||
let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model);
|
||||
let mut thread = Thread::new(
|
||||
project.clone(),
|
||||
agent.project_context.clone(),
|
||||
agent.context_server_registry.clone(),
|
||||
action_log.clone(),
|
||||
agent.templates.clone(),
|
||||
default_model,
|
||||
cx,
|
||||
);
|
||||
thread.add_tool(CreateDirectoryTool::new(project.clone()));
|
||||
thread.add_tool(CopyPathTool::new(project.clone()));
|
||||
thread.add_tool(DiagnosticsTool::new(project.clone()));
|
||||
@@ -450,7 +470,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
acp_thread: acp_thread.downgrade(),
|
||||
_subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
|
||||
this.sessions.remove(acp_thread.session_id());
|
||||
})
|
||||
}),
|
||||
},
|
||||
);
|
||||
})?;
|
||||
@@ -496,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
|
||||
@@ -603,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,7 +1,9 @@
|
||||
use super::*;
|
||||
use crate::MessageContent;
|
||||
use acp_thread::AgentConnection;
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp};
|
||||
use agent_settings::AgentProfileId;
|
||||
use anyhow::Result;
|
||||
use client::{Client, UserStore};
|
||||
use fs::{FakeFs, Fs};
|
||||
@@ -12,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;
|
||||
@@ -165,7 +167,9 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}),
|
||||
"{}",
|
||||
thread.to_markdown()
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -269,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,
|
||||
@@ -309,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.
|
||||
@@ -334,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())
|
||||
}
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -469,6 +477,82 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_profiles(cx: &mut TestAppContext) {
|
||||
let ThreadTest {
|
||||
model, thread, fs, ..
|
||||
} = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
thread.update(cx, |thread, _cx| {
|
||||
thread.add_tool(DelayTool);
|
||||
thread.add_tool(EchoTool);
|
||||
thread.add_tool(InfiniteTool);
|
||||
});
|
||||
|
||||
// Override profiles and wait for settings to be loaded.
|
||||
fs.insert_file(
|
||||
paths::settings_file(),
|
||||
json!({
|
||||
"agent": {
|
||||
"profiles": {
|
||||
"test-1": {
|
||||
"name": "Test Profile 1",
|
||||
"tools": {
|
||||
EchoTool.name(): true,
|
||||
DelayTool.name(): true,
|
||||
}
|
||||
},
|
||||
"test-2": {
|
||||
"name": "Test Profile 2",
|
||||
"tools": {
|
||||
InfiniteTool.name(): true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes(),
|
||||
)
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
|
||||
// Test that test-1 profile (default) has echo and delay tools
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-1".into()));
|
||||
thread.send("test", cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut pending_completions = fake_model.pending_completions();
|
||||
assert_eq!(pending_completions.len(), 1);
|
||||
let completion = pending_completions.pop().unwrap();
|
||||
let tool_names: Vec<String> = completion
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone())
|
||||
.collect();
|
||||
assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]);
|
||||
fake_model.end_last_completion_stream();
|
||||
|
||||
// Switch to test-2 profile, and verify that it has only the infinite tool.
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_profile(AgentProfileId("test-2".into()));
|
||||
thread.send("test2", cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let mut pending_completions = fake_model.pending_completions();
|
||||
assert_eq!(pending_completions.len(), 1);
|
||||
let completion = pending_completions.pop().unwrap();
|
||||
let tool_names: Vec<String> = completion
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone())
|
||||
.collect();
|
||||
assert_eq!(tool_names, vec![InfiniteTool.name()]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[ignore = "can't run on CI yet"]
|
||||
async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
@@ -595,6 +679,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
agent_settings::init(cx);
|
||||
});
|
||||
cx.executor().forbid_parking();
|
||||
|
||||
@@ -790,6 +875,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
id: acp::ToolCallId("1".into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
raw_output: Some("Finished thinking.".into()),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
@@ -813,6 +899,7 @@ struct ThreadTest {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
thread: Entity<Thread>,
|
||||
project_context: Rc<RefCell<ProjectContext>>,
|
||||
fs: Arc<FakeFs>,
|
||||
}
|
||||
|
||||
enum TestModel {
|
||||
@@ -835,30 +922,57 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
fs.insert_file(
|
||||
paths::settings_file(),
|
||||
json!({
|
||||
"agent": {
|
||||
"default_profile": "test-profile",
|
||||
"profiles": {
|
||||
"test-profile": {
|
||||
"name": "Test Profile",
|
||||
"tools": {
|
||||
EchoTool.name(): true,
|
||||
DelayTool.name(): true,
|
||||
WordListTool.name(): true,
|
||||
ToolRequiringPermission.name(): true,
|
||||
InfiniteTool.name(): true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
.into_bytes(),
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::init(cx);
|
||||
watch_settings(fs.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
agent_settings::init(cx);
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
client::init_settings(cx);
|
||||
let client = Client::production(cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
|
||||
watch_settings(fs.clone(), cx);
|
||||
});
|
||||
|
||||
let templates = Templates::new();
|
||||
|
||||
fs.insert_tree(path!("/test"), json!({})).await;
|
||||
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
|
||||
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
|
||||
|
||||
let model = cx
|
||||
.update(|cx| {
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
client::init_settings(cx);
|
||||
let client = Client::production(cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
|
||||
if let TestModel::Fake = model {
|
||||
Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>)
|
||||
} else {
|
||||
@@ -881,20 +995,25 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
||||
.await;
|
||||
|
||||
let project_context = Rc::new(RefCell::new(ProjectContext::default()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let thread = cx.new(|_| {
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project_context.clone(),
|
||||
context_server_registry,
|
||||
action_log,
|
||||
templates,
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
ThreadTest {
|
||||
model,
|
||||
thread,
|
||||
project_context,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{SystemPromptTemplate, Template, Templates};
|
||||
use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates};
|
||||
use acp_thread::MentionUri;
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::AgentSettings;
|
||||
use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::adapt_schema_to_format;
|
||||
use cloud_llm_client::{CompletionIntent, CompletionMode};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +148,8 @@ pub struct Thread {
|
||||
running_turn: Option<Task<()>>,
|
||||
pending_tool_uses: HashMap<LanguageModelToolUseId, LanguageModelToolUse>,
|
||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
profile_id: AgentProfileId,
|
||||
project_context: Rc<RefCell<ProjectContext>>,
|
||||
templates: Arc<Templates>,
|
||||
pub selected_model: Arc<dyn LanguageModel>,
|
||||
@@ -137,16 +161,21 @@ impl Thread {
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
project_context: Rc<RefCell<ProjectContext>>,
|
||||
context_server_registry: Entity<ContextServerRegistry>,
|
||||
action_log: Entity<ActionLog>,
|
||||
templates: Arc<Templates>,
|
||||
default_model: Arc<dyn LanguageModel>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
|
||||
Self {
|
||||
messages: Vec::new(),
|
||||
completion_mode: CompletionMode::Normal,
|
||||
running_turn: None,
|
||||
pending_tool_uses: HashMap::default(),
|
||||
tools: BTreeMap::default(),
|
||||
context_server_registry,
|
||||
profile_id,
|
||||
project_context,
|
||||
templates,
|
||||
selected_model: default_model,
|
||||
@@ -179,6 +208,10 @@ impl Thread {
|
||||
self.tools.remove(name).is_some()
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
|
||||
self.profile_id = profile_id;
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self) {
|
||||
self.running_turn.take();
|
||||
|
||||
@@ -203,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);
|
||||
@@ -219,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| {
|
||||
@@ -298,6 +332,7 @@ impl Thread {
|
||||
} else {
|
||||
acp::ToolCallStatus::Completed
|
||||
}),
|
||||
raw_output: tool_result.output.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -341,7 +376,7 @@ impl Thread {
|
||||
log::debug!("System message built");
|
||||
AgentMessage {
|
||||
role: Role::System,
|
||||
content: vec![prompt.into()],
|
||||
content: vec![prompt.as_str().into()],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -604,21 +639,23 @@ impl Thread {
|
||||
let messages = self.build_request_messages();
|
||||
log::info!("Request will include {} messages", messages.len());
|
||||
|
||||
let tools: Vec<LanguageModelRequestTool> = self
|
||||
.tools
|
||||
.values()
|
||||
.filter_map(|tool| {
|
||||
let tool_name = tool.name().to_string();
|
||||
log::trace!("Including tool: {}", tool_name);
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool_name,
|
||||
description: tool.description(cx).to_string(),
|
||||
input_schema: tool
|
||||
.input_schema(self.selected_model.tool_input_format())
|
||||
.log_err()?,
|
||||
let tools = if let Some(tools) = self.tools(cx).log_err() {
|
||||
tools
|
||||
.filter_map(|tool| {
|
||||
let tool_name = tool.name().to_string();
|
||||
log::trace!("Including tool: {}", tool_name);
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool_name,
|
||||
description: tool.description().to_string(),
|
||||
input_schema: tool
|
||||
.input_schema(self.selected_model.tool_input_format())
|
||||
.log_err()?,
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
log::info!("Request includes {} tools", tools.len());
|
||||
|
||||
@@ -639,6 +676,35 @@ impl Thread {
|
||||
request
|
||||
}
|
||||
|
||||
fn tools<'a>(&'a self, cx: &'a App) -> Result<impl Iterator<Item = &'a Arc<dyn AnyAgentTool>>> {
|
||||
let profile = AgentSettings::get_global(cx)
|
||||
.profiles
|
||||
.get(&self.profile_id)
|
||||
.context("profile not found")?;
|
||||
|
||||
Ok(self
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool_name, tool)| {
|
||||
if profile.is_tool_enabled(tool_name) {
|
||||
Some(tool)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.chain(self.context_server_registry.read(cx).servers().flat_map(
|
||||
|(server_id, tools)| {
|
||||
tools.iter().filter_map(|(tool_name, tool)| {
|
||||
if profile.is_context_server_tool_enabled(&server_id.0, tool_name) {
|
||||
Some(tool)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
fn build_request_messages(&self) -> Vec<LanguageModelRequestMessage> {
|
||||
log::trace!(
|
||||
"Building request messages from {} thread messages",
|
||||
@@ -658,11 +724,7 @@ impl Thread {
|
||||
},
|
||||
message.content.len()
|
||||
);
|
||||
LanguageModelRequestMessage {
|
||||
role: message.role,
|
||||
content: message.content.clone(),
|
||||
cache: false,
|
||||
}
|
||||
message.to_request()
|
||||
})
|
||||
.collect();
|
||||
messages
|
||||
@@ -677,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,
|
||||
@@ -686,7 +762,7 @@ where
|
||||
|
||||
fn name(&self) -> SharedString;
|
||||
|
||||
fn description(&self, _cx: &mut App) -> SharedString {
|
||||
fn description(&self) -> SharedString {
|
||||
let schema = schemars::schema_for!(Self::Input);
|
||||
SharedString::new(
|
||||
schema
|
||||
@@ -722,13 +798,13 @@ where
|
||||
pub struct Erased<T>(T);
|
||||
|
||||
pub struct AgentToolOutput {
|
||||
llm_output: LanguageModelToolResultContent,
|
||||
raw_output: serde_json::Value,
|
||||
pub llm_output: LanguageModelToolResultContent,
|
||||
pub raw_output: serde_json::Value,
|
||||
}
|
||||
|
||||
pub trait AnyAgentTool {
|
||||
fn name(&self) -> SharedString;
|
||||
fn description(&self, cx: &mut App) -> SharedString;
|
||||
fn description(&self) -> SharedString;
|
||||
fn kind(&self) -> acp::ToolKind;
|
||||
fn initial_title(&self, input: serde_json::Value) -> SharedString;
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
|
||||
@@ -748,8 +824,8 @@ where
|
||||
self.0.name()
|
||||
}
|
||||
|
||||
fn description(&self, cx: &mut App) -> SharedString {
|
||||
self.0.description(cx)
|
||||
fn description(&self) -> SharedString {
|
||||
self.0.description()
|
||||
}
|
||||
|
||||
fn kind(&self) -> agent_client_protocol::ToolKind {
|
||||
@@ -1059,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,3 +1,4 @@
|
||||
mod context_server_registry;
|
||||
mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
mod delete_path_tool;
|
||||
@@ -15,6 +16,7 @@ mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
mod web_search_tool;
|
||||
|
||||
pub use context_server_registry::*;
|
||||
pub use copy_path_tool::*;
|
||||
pub use create_directory_tool::*;
|
||||
pub use delete_path_tool::*;
|
||||
|
||||
231
crates/agent2/src/tools/context_server_registry.rs
Normal file
231
crates/agent2/src/tools/context_server_registry.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream};
|
||||
use agent_client_protocol::ToolKind;
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use context_server::ContextServerId;
|
||||
use gpui::{App, Context, Entity, SharedString, Task};
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct ContextServerRegistry {
|
||||
server_store: Entity<ContextServerStore>,
|
||||
registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
struct RegisteredContextServer {
|
||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
load_tools: Task<Result<()>>,
|
||||
}
|
||||
|
||||
impl ContextServerRegistry {
|
||||
pub fn new(server_store: Entity<ContextServerStore>, cx: &mut Context<Self>) -> Self {
|
||||
let mut this = Self {
|
||||
server_store: server_store.clone(),
|
||||
registered_servers: HashMap::default(),
|
||||
_subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event),
|
||||
};
|
||||
for server in server_store.read(cx).running_servers() {
|
||||
this.reload_tools_for_server(server.id(), cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
pub fn servers(
|
||||
&self,
|
||||
) -> impl Iterator<
|
||||
Item = (
|
||||
&ContextServerId,
|
||||
&BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
),
|
||||
> {
|
||||
self.registered_servers
|
||||
.iter()
|
||||
.map(|(id, server)| (id, &server.tools))
|
||||
}
|
||||
|
||||
fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = server.client() else {
|
||||
return;
|
||||
};
|
||||
if !client.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
return;
|
||||
}
|
||||
|
||||
let registered_server =
|
||||
self.registered_servers
|
||||
.entry(server_id.clone())
|
||||
.or_insert(RegisteredContextServer {
|
||||
tools: BTreeMap::default(),
|
||||
load_tools: Task::ready(Ok(())),
|
||||
});
|
||||
registered_server.load_tools = cx.spawn(async move |this, cx| {
|
||||
let response = client
|
||||
.request::<context_server::types::requests::ListTools>(())
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let Some(registered_server) = this.registered_servers.get_mut(&server_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
registered_server.tools.clear();
|
||||
if let Some(response) = response.log_err() {
|
||||
for tool in response.tools {
|
||||
let tool = Arc::new(ContextServerTool::new(
|
||||
this.server_store.clone(),
|
||||
server.id(),
|
||||
tool,
|
||||
));
|
||||
registered_server.tools.insert(tool.name(), tool);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_context_server_store_event(
|
||||
&mut self,
|
||||
_: Entity<ContextServerStore>,
|
||||
event: &project::context_server_store::Event,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
|
||||
match status {
|
||||
ContextServerStatus::Starting => {}
|
||||
ContextServerStatus::Running => {
|
||||
self.reload_tools_for_server(server_id.clone(), cx);
|
||||
}
|
||||
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
||||
self.registered_servers.remove(&server_id);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextServerTool {
|
||||
store: Entity<ContextServerStore>,
|
||||
server_id: ContextServerId,
|
||||
tool: context_server::types::Tool,
|
||||
}
|
||||
|
||||
impl ContextServerTool {
|
||||
fn new(
|
||||
store: Entity<ContextServerStore>,
|
||||
server_id: ContextServerId,
|
||||
tool: context_server::types::Tool,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
server_id,
|
||||
tool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyAgentTool for ContextServerTool {
|
||||
fn name(&self) -> SharedString {
|
||||
self.tool.name.clone().into()
|
||||
}
|
||||
|
||||
fn description(&self) -> SharedString {
|
||||
self.tool.description.clone().unwrap_or_default().into()
|
||||
}
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: serde_json::Value) -> SharedString {
|
||||
format!("Run MCP tool `{}`", self.tool.name).into()
|
||||
}
|
||||
|
||||
fn input_schema(
|
||||
&self,
|
||||
format: language_model::LanguageModelToolSchemaFormat,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut schema = self.tool.input_schema.clone();
|
||||
assistant_tool::adapt_schema_to_format(&mut schema, format)?;
|
||||
Ok(match schema {
|
||||
serde_json::Value::Null => {
|
||||
serde_json::json!({ "type": "object", "properties": [] })
|
||||
}
|
||||
serde_json::Value::Object(map) if map.is_empty() => {
|
||||
serde_json::json!({ "type": "object", "properties": [] })
|
||||
}
|
||||
_ => schema,
|
||||
})
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<AgentToolOutput>> {
|
||||
let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else {
|
||||
return Task::ready(Err(anyhow!("Context server not found")));
|
||||
};
|
||||
let tool_name = self.tool.name.clone();
|
||||
let server_clone = server.clone();
|
||||
let input_clone = input.clone();
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
let Some(protocol) = server_clone.client() else {
|
||||
bail!("Context server not initialized");
|
||||
};
|
||||
|
||||
let arguments = if let serde_json::Value::Object(map) = input_clone {
|
||||
Some(map.into_iter().collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
"Running tool: {} with arguments: {:?}",
|
||||
tool_name,
|
||||
arguments
|
||||
);
|
||||
let response = protocol
|
||||
.request::<context_server::types::requests::CallTool>(
|
||||
context_server::types::CallToolParams {
|
||||
name: tool_name,
|
||||
arguments,
|
||||
meta: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut result = String::new();
|
||||
for content in response.content {
|
||||
match content {
|
||||
context_server::types::ToolResponseContent::Text { text } => {
|
||||
result.push_str(&text);
|
||||
}
|
||||
context_server::types::ToolResponseContent::Image { .. } => {
|
||||
log::warn!("Ignoring image content from tool response");
|
||||
}
|
||||
context_server::types::ToolResponseContent::Audio { .. } => {
|
||||
log::warn!("Ignoring audio content from tool response");
|
||||
}
|
||||
context_server::types::ToolResponseContent::Resource { .. } => {
|
||||
log::warn!("Ignoring resource content from tool response");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(AgentToolOutput {
|
||||
raw_output: result.clone().into(),
|
||||
llm_output: result.into(),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl AgentTool for DiagnosticsTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
event_stream: ToolCallEventStream,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
match input.path {
|
||||
@@ -119,11 +119,6 @@ impl AgentTool for DiagnosticsTool {
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![output.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if output.is_empty() {
|
||||
@@ -158,18 +153,9 @@ impl AgentTool for DiagnosticsTool {
|
||||
}
|
||||
|
||||
if has_diagnostics {
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![output.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
Task::ready(Ok(output))
|
||||
} else {
|
||||
let text = "No errors or warnings found in the project.";
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![text.into()]),
|
||||
..Default::default()
|
||||
});
|
||||
Task::ready(Ok(text.into()))
|
||||
Task::ready(Ok("No errors or warnings found in the project.".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,9 +454,8 @@ fn resolve_path(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::Templates;
|
||||
|
||||
use super::*;
|
||||
use crate::{ContextServerRegistry, Templates};
|
||||
use action_log::ActionLog;
|
||||
use client::TelemetrySettings;
|
||||
use fs::Fs;
|
||||
@@ -475,9 +474,20 @@ mod tests {
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread =
|
||||
cx.new(|_| Thread::new(project, Rc::default(), action_log, Templates::new(), model));
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log,
|
||||
Templates::new(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = EditFileToolInput {
|
||||
@@ -661,14 +671,18 @@ mod tests {
|
||||
});
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|_| {
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -792,15 +806,19 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|_| {
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -914,15 +932,19 @@ mod tests {
|
||||
init_test(cx);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|_| {
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1041,15 +1063,19 @@ mod tests {
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/project", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|_| {
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1148,14 +1174,18 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|_| {
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
Rc::default(),
|
||||
context_server_registry.clone(),
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1225,14 +1255,18 @@ mod tests {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|_| {
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
Rc::default(),
|
||||
context_server_registry.clone(),
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1305,14 +1339,18 @@ mod tests {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|_| {
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
Rc::default(),
|
||||
context_server_registry.clone(),
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
@@ -1382,14 +1420,18 @@ mod tests {
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|_| {
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
Rc::default(),
|
||||
context_server_registry,
|
||||
action_log.clone(),
|
||||
Templates::new(),
|
||||
model.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
|
||||
@@ -136,7 +136,7 @@ impl AgentTool for FetchTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
event_stream: ToolCallEventStream,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
let text = cx.background_spawn({
|
||||
@@ -149,12 +149,6 @@ impl AgentTool for FetchTool {
|
||||
if text.trim().is_empty() {
|
||||
bail!("no textual content found");
|
||||
}
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![text.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Ok(text)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -139,9 +139,6 @@ impl AgentTool for FindPathTool {
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
raw_output: Some(serde_json::json!({
|
||||
"paths": &matches,
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ impl AgentTool for GrepTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
event_stream: ToolCallEventStream,
|
||||
_event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
const CONTEXT_LINES: u32 = 2;
|
||||
@@ -282,33 +282,22 @@ impl AgentTool for GrepTool {
|
||||
}
|
||||
}
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![output.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
matches_found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let output = if matches_found == 0 {
|
||||
"No matches found".to_string()
|
||||
if matches_found == 0 {
|
||||
Ok("No matches found".into())
|
||||
} else if has_more_matches {
|
||||
format!(
|
||||
Ok(format!(
|
||||
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
|
||||
input.offset + 1,
|
||||
input.offset + matches_found,
|
||||
input.offset + RESULTS_PER_PAGE,
|
||||
)
|
||||
))
|
||||
} else {
|
||||
format!("Found {matches_found} matches:\n{output}")
|
||||
};
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![output.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Ok(output)
|
||||
Ok(format!("Found {matches_found} matches:\n{output}"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,20 +47,13 @@ impl AgentTool for NowTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: Self::Input,
|
||||
event_stream: ToolCallEventStream,
|
||||
_event_stream: ToolCallEventStream,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let now = match input.timezone {
|
||||
Timezone::Utc => Utc::now().to_rfc3339(),
|
||||
Timezone::Local => Local::now().to_rfc3339(),
|
||||
};
|
||||
let content = format!("The current datetime is {now}.");
|
||||
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![content.clone().into()]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
Task::ready(Ok(content))
|
||||
Task::ready(Ok(format!("The current datetime is {now}.")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,20 @@ pub struct AgentProfileSettings {
|
||||
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
|
||||
}
|
||||
|
||||
impl AgentProfileSettings {
|
||||
pub fn is_tool_enabled(&self, tool_name: &str) -> bool {
|
||||
self.tools.get(tool_name) == Some(&true)
|
||||
}
|
||||
|
||||
pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool {
|
||||
self.enable_all_context_servers
|
||||
|| self
|
||||
.context_servers
|
||||
.get(server_id)
|
||||
.map_or(false, |preset| preset.tools.get(tool_name) == Some(&true))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ContextServerPreset {
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
|
||||
@@ -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());
|
||||
let contents = self.mention_set.lock().contents(project, cx);
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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(),
|
||||
},
|
||||
),
|
||||
}));
|
||||
ix = crease_range.end;
|
||||
}
|
||||
}
|
||||
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,
|
||||
}));
|
||||
|
||||
if ix < text.len() {
|
||||
let last_chunk = text[ix..].trim_end();
|
||||
if !last_chunk.is_empty() {
|
||||
chunks.push(last_chunk.into());
|
||||
}
|
||||
}
|
||||
ix = crease_range.end;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if chunks.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
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)))
|
||||
}
|
||||
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();
|
||||
|
||||
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(
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1080,10 +1108,10 @@ impl AcpThreadView {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted);
|
||||
|
||||
let base_container = h_flex().size_4().justify_center();
|
||||
|
||||
if is_collapsible {
|
||||
h_flex()
|
||||
.size_4()
|
||||
.justify_center()
|
||||
base_container
|
||||
.child(
|
||||
div()
|
||||
.group_hover(&group_name, |s| s.invisible().w_0())
|
||||
@@ -1114,7 +1142,7 @@ impl AcpThreadView {
|
||||
),
|
||||
)
|
||||
} else {
|
||||
div().child(tool_icon)
|
||||
base_container.child(tool_icon)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1177,8 +1205,10 @@ impl AcpThreadView {
|
||||
ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx),
|
||||
_ => false,
|
||||
});
|
||||
let is_collapsible =
|
||||
!tool_call.content.is_empty() && !needs_confirmation && !is_edit && !has_diff;
|
||||
let use_card_layout = needs_confirmation || is_edit || has_diff;
|
||||
|
||||
let is_collapsible = !tool_call.content.is_empty() && !use_card_layout;
|
||||
|
||||
let is_open = tool_call.content.is_empty()
|
||||
|| needs_confirmation
|
||||
|| has_nonempty_diff
|
||||
@@ -1197,9 +1227,39 @@ impl AcpThreadView {
|
||||
linear_color_stop(color.opacity(0.2), 0.),
|
||||
))
|
||||
};
|
||||
let gradient_color = if use_card_layout {
|
||||
self.tool_card_header_bg(cx)
|
||||
} else {
|
||||
cx.theme().colors().panel_background
|
||||
};
|
||||
|
||||
let tool_output_display = match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
|
||||
.w_full()
|
||||
.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.child(self.render_tool_call_content(content, tool_call, window, cx))
|
||||
.into_any_element()
|
||||
}))
|
||||
.child(self.render_permission_buttons(
|
||||
options,
|
||||
entry_ix,
|
||||
tool_call.id.clone(),
|
||||
tool_call.content.is_empty(),
|
||||
cx,
|
||||
)),
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => v_flex()
|
||||
.w_full()
|
||||
.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.child(self.render_tool_call_content(content, tool_call, window, cx))
|
||||
.into_any_element()
|
||||
})),
|
||||
ToolCallStatus::Rejected => v_flex().size_0(),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.when(needs_confirmation || is_edit || has_diff, |this| {
|
||||
.when(use_card_layout, |this| {
|
||||
this.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
@@ -1213,7 +1273,7 @@ impl AcpThreadView {
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.map(|this| {
|
||||
if needs_confirmation || is_edit || has_diff {
|
||||
if use_card_layout {
|
||||
this.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
@@ -1230,13 +1290,6 @@ impl AcpThreadView {
|
||||
.group(&card_header_id)
|
||||
.relative()
|
||||
.w_full()
|
||||
.map(|this| {
|
||||
if tool_call.locations.len() == 1 {
|
||||
this.gap_0()
|
||||
} else {
|
||||
this.gap_1p5()
|
||||
}
|
||||
})
|
||||
.text_size(self.tool_name_font_size())
|
||||
.child(self.render_tool_call_icon(
|
||||
card_header_id,
|
||||
@@ -1280,6 +1333,7 @@ impl AcpThreadView {
|
||||
.id("non-card-label-container")
|
||||
.w_full()
|
||||
.relative()
|
||||
.ml_1p5()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -1296,17 +1350,7 @@ impl AcpThreadView {
|
||||
),
|
||||
)),
|
||||
)
|
||||
.map(|this| {
|
||||
if needs_confirmation {
|
||||
this.child(gradient_overlay(
|
||||
self.tool_card_header_bg(cx),
|
||||
))
|
||||
} else {
|
||||
this.child(gradient_overlay(
|
||||
cx.theme().colors().panel_background,
|
||||
))
|
||||
}
|
||||
})
|
||||
.child(gradient_overlay(gradient_color))
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
@@ -1323,54 +1367,7 @@ impl AcpThreadView {
|
||||
)
|
||||
.children(status_icon),
|
||||
)
|
||||
.when(is_open, |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.text_xs()
|
||||
.when(is_collapsible, |this| {
|
||||
this.mt_1()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_lg()
|
||||
})
|
||||
.map(|this| {
|
||||
if is_open {
|
||||
match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { options, .. } => this
|
||||
.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(self.render_tool_call_content(
|
||||
content, tool_call, window, cx,
|
||||
))
|
||||
.into_any_element()
|
||||
}))
|
||||
.child(self.render_permission_buttons(
|
||||
options,
|
||||
entry_ix,
|
||||
tool_call.id.clone(),
|
||||
tool_call.content.is_empty(),
|
||||
cx,
|
||||
)),
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
|
||||
this.children(tool_call.content.iter().map(|content| {
|
||||
div()
|
||||
.py_1p5()
|
||||
.child(self.render_tool_call_content(
|
||||
content, tool_call, window, cx,
|
||||
))
|
||||
.into_any_element()
|
||||
}))
|
||||
}
|
||||
ToolCallStatus::Rejected => this,
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(is_open, |this| this.child(tool_output_display))
|
||||
}
|
||||
|
||||
fn render_tool_call_content(
|
||||
@@ -1382,16 +1379,10 @@ impl AcpThreadView {
|
||||
) -> AnyElement {
|
||||
match content {
|
||||
ToolCallContent::ContentBlock(content) => {
|
||||
if let Some(md) = content.markdown() {
|
||||
div()
|
||||
.p_2()
|
||||
.child(
|
||||
self.render_markdown(
|
||||
md.clone(),
|
||||
default_markdown_style(false, window, cx),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
if let Some(resource_link) = content.resource_link() {
|
||||
self.render_resource_link(resource_link, cx)
|
||||
} else if let Some(markdown) = content.markdown() {
|
||||
self.render_markdown_output(markdown.clone(), tool_call.id.clone(), window, cx)
|
||||
} else {
|
||||
Empty.into_any_element()
|
||||
}
|
||||
@@ -1403,6 +1394,83 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_markdown_output(
|
||||
&self,
|
||||
markdown: Entity<Markdown>,
|
||||
tool_call_id: acp::ToolCallId,
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id.clone()));
|
||||
|
||||
v_flex()
|
||||
.mt_1p5()
|
||||
.ml(px(7.))
|
||||
.px_3p5()
|
||||
.gap_2()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(self.render_markdown(markdown, default_markdown_style(false, window, cx)))
|
||||
.child(
|
||||
Button::new(button_id, "Collapse Output")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::ChevronUp)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call_id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_resource_link(
|
||||
&self,
|
||||
resource_link: &acp::ResourceLink,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let uri: SharedString = resource_link.uri.clone().into();
|
||||
|
||||
let label: SharedString = if let Some(path) = resource_link.uri.strip_prefix("file://") {
|
||||
path.to_string().into()
|
||||
} else {
|
||||
uri.clone()
|
||||
};
|
||||
|
||||
let button_id = SharedString::from(format!("item-{}", uri.clone()));
|
||||
|
||||
div()
|
||||
.ml(px(7.))
|
||||
.pl_2p5()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
Button::new(button_id, label)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.truncate(true)
|
||||
.on_click(cx.listener({
|
||||
let workspace = self.workspace.clone();
|
||||
move |_, _, window, cx: &mut Context<Self>| {
|
||||
Self::open_link(uri.clone(), &workspace, window, cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_permission_buttons(
|
||||
&self,
|
||||
options: &[acp::PermissionOption],
|
||||
@@ -1678,7 +1746,9 @@ impl AcpThreadView {
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.pt_1()
|
||||
.pb_2()
|
||||
.px_2()
|
||||
.gap_0p5()
|
||||
.bg(header_bg)
|
||||
.text_xs()
|
||||
@@ -2562,25 +2632,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 +3051,7 @@ impl AcpThreadView {
|
||||
anchor..anchor,
|
||||
self.message_editor.clone(),
|
||||
self.mention_set.clone(),
|
||||
self.project.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -3117,7 +3194,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),
|
||||
|
||||
@@ -301,7 +301,25 @@ impl LanguageModel for OpenAiLanguageModel {
|
||||
}
|
||||
|
||||
fn supports_images(&self) -> bool {
|
||||
false
|
||||
use open_ai::Model;
|
||||
match &self.model {
|
||||
Model::FourOmni
|
||||
| Model::FourOmniMini
|
||||
| Model::FourPointOne
|
||||
| Model::FourPointOneMini
|
||||
| Model::FourPointOneNano
|
||||
| Model::Five
|
||||
| Model::FiveMini
|
||||
| Model::FiveNano
|
||||
| Model::O1
|
||||
| Model::O3
|
||||
| Model::O4Mini => true,
|
||||
Model::ThreePointFiveTurbo
|
||||
| Model::Four
|
||||
| Model::FourTurbo
|
||||
| Model::O3Mini
|
||||
| Model::Custom { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
|
||||
|
||||
@@ -487,6 +487,8 @@ const GO_MODULE_ROOT_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("GO_MODULE_ROOT"));
|
||||
const GO_SUBTEST_NAME_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("GO_SUBTEST_NAME"));
|
||||
const GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("GO_TABLE_TEST_CASE_NAME"));
|
||||
|
||||
impl ContextProvider for GoContextProvider {
|
||||
fn build_context(
|
||||
@@ -545,10 +547,19 @@ impl ContextProvider for GoContextProvider {
|
||||
let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
|
||||
.map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
|
||||
|
||||
let table_test_case_name = variables.get(&VariableName::Custom(Cow::Borrowed(
|
||||
"_table_test_case_name",
|
||||
)));
|
||||
|
||||
let go_table_test_case_variable = table_test_case_name
|
||||
.and_then(extract_subtest_name)
|
||||
.map(|case_name| (GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.clone(), case_name));
|
||||
|
||||
Task::ready(Ok(TaskVariables::from_iter(
|
||||
[
|
||||
go_package_variable,
|
||||
go_subtest_variable,
|
||||
go_table_test_case_variable,
|
||||
go_module_root_variable,
|
||||
]
|
||||
.into_iter()
|
||||
@@ -570,6 +581,28 @@ impl ContextProvider for GoContextProvider {
|
||||
let module_cwd = Some(GO_MODULE_ROOT_TASK_VARIABLE.template_value());
|
||||
|
||||
Task::ready(Some(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: format!(
|
||||
"go test {} -v -run {}/{}",
|
||||
GO_PACKAGE_TASK_VARIABLE.template_value(),
|
||||
VariableName::Symbol.template_value(),
|
||||
GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
|
||||
),
|
||||
command: "go".into(),
|
||||
args: vec![
|
||||
"test".into(),
|
||||
"-v".into(),
|
||||
"-run".into(),
|
||||
format!(
|
||||
"\\^{}\\$/\\^{}\\$",
|
||||
VariableName::Symbol.template_value(),
|
||||
GO_TABLE_TEST_CASE_NAME_TASK_VARIABLE.template_value(),
|
||||
),
|
||||
],
|
||||
cwd: package_cwd.clone(),
|
||||
tags: vec!["go-table-test-case".to_owned()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: format!(
|
||||
"go test {} -run {}",
|
||||
@@ -842,10 +875,21 @@ mod tests {
|
||||
.collect()
|
||||
});
|
||||
|
||||
let tag_strings: Vec<String> = runnables
|
||||
.iter()
|
||||
.flat_map(|r| &r.runnable.tags)
|
||||
.map(|tag| tag.0.to_string())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
runnables.len() == 2,
|
||||
"Should find test function and subtest with double quotes, found: {}",
|
||||
runnables.len()
|
||||
tag_strings.contains(&"go-test".to_string()),
|
||||
"Should find go-test tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
assert!(
|
||||
tag_strings.contains(&"go-subtest".to_string()),
|
||||
"Should find go-subtest tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
|
||||
let buffer = cx.new(|cx| {
|
||||
@@ -860,10 +904,299 @@ mod tests {
|
||||
.collect()
|
||||
});
|
||||
|
||||
let tag_strings: Vec<String> = runnables
|
||||
.iter()
|
||||
.flat_map(|r| &r.runnable.tags)
|
||||
.map(|tag| tag.0.to_string())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
runnables.len() == 2,
|
||||
"Should find test function and subtest with backticks, found: {}",
|
||||
runnables.len()
|
||||
tag_strings.contains(&"go-test".to_string()),
|
||||
"Should find go-test tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
assert!(
|
||||
tag_strings.contains(&"go-subtest".to_string()),
|
||||
"Should find go-subtest tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_go_table_test_slice_detection(cx: &mut TestAppContext) {
|
||||
let language = language("go", tree_sitter_go::LANGUAGE.into());
|
||||
|
||||
let table_test = r#"
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExample(t *testing.T) {
|
||||
_ = "some random string"
|
||||
|
||||
testCases := []struct{
|
||||
name string
|
||||
anotherStr string
|
||||
}{
|
||||
{
|
||||
name: "test case 1",
|
||||
anotherStr: "foo",
|
||||
},
|
||||
{
|
||||
name: "test case 2",
|
||||
anotherStr: "bar",
|
||||
},
|
||||
}
|
||||
|
||||
notATableTest := []struct{
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "some string",
|
||||
},
|
||||
{
|
||||
name: "some other string",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// test code here
|
||||
})
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let buffer =
|
||||
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
snapshot.runnable_ranges(0..table_test.len()).collect()
|
||||
});
|
||||
|
||||
let tag_strings: Vec<String> = runnables
|
||||
.iter()
|
||||
.flat_map(|r| &r.runnable.tags)
|
||||
.map(|tag| tag.0.to_string())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
tag_strings.contains(&"go-test".to_string()),
|
||||
"Should find go-test tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
assert!(
|
||||
tag_strings.contains(&"go-table-test-case".to_string()),
|
||||
"Should find go-table-test-case tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
|
||||
let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
|
||||
let go_table_test_count = tag_strings
|
||||
.iter()
|
||||
.filter(|&tag| tag == "go-table-test-case")
|
||||
.count();
|
||||
|
||||
assert!(
|
||||
go_test_count == 1,
|
||||
"Should find exactly 1 go-test, found: {}",
|
||||
go_test_count
|
||||
);
|
||||
assert!(
|
||||
go_table_test_count == 2,
|
||||
"Should find exactly 2 go-table-test-case, found: {}",
|
||||
go_table_test_count
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_go_table_test_slice_ignored(cx: &mut TestAppContext) {
|
||||
let language = language("go", tree_sitter_go::LANGUAGE.into());
|
||||
|
||||
let table_test = r#"
|
||||
package main
|
||||
|
||||
func Example() {
|
||||
_ = "some random string"
|
||||
|
||||
notATableTest := []struct{
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "some string",
|
||||
},
|
||||
{
|
||||
name: "some other string",
|
||||
},
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let buffer =
|
||||
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
snapshot.runnable_ranges(0..table_test.len()).collect()
|
||||
});
|
||||
|
||||
let tag_strings: Vec<String> = runnables
|
||||
.iter()
|
||||
.flat_map(|r| &r.runnable.tags)
|
||||
.map(|tag| tag.0.to_string())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!tag_strings.contains(&"go-test".to_string()),
|
||||
"Should find go-test tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
assert!(
|
||||
!tag_strings.contains(&"go-table-test-case".to_string()),
|
||||
"Should find go-table-test-case tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_go_table_test_map_detection(cx: &mut TestAppContext) {
|
||||
let language = language("go", tree_sitter_go::LANGUAGE.into());
|
||||
|
||||
let table_test = r#"
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExample(t *testing.T) {
|
||||
_ = "some random string"
|
||||
|
||||
testCases := map[string]struct {
|
||||
someStr string
|
||||
fail bool
|
||||
}{
|
||||
"test failure": {
|
||||
someStr: "foo",
|
||||
fail: true,
|
||||
},
|
||||
"test success": {
|
||||
someStr: "bar",
|
||||
fail: false,
|
||||
},
|
||||
}
|
||||
|
||||
notATableTest := map[string]struct {
|
||||
someStr string
|
||||
}{
|
||||
"some string": {
|
||||
someStr: "foo",
|
||||
},
|
||||
"some other string": {
|
||||
someStr: "bar",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// test code here
|
||||
})
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let buffer =
|
||||
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
snapshot.runnable_ranges(0..table_test.len()).collect()
|
||||
});
|
||||
|
||||
let tag_strings: Vec<String> = runnables
|
||||
.iter()
|
||||
.flat_map(|r| &r.runnable.tags)
|
||||
.map(|tag| tag.0.to_string())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
tag_strings.contains(&"go-test".to_string()),
|
||||
"Should find go-test tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
assert!(
|
||||
tag_strings.contains(&"go-table-test-case".to_string()),
|
||||
"Should find go-table-test-case tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
|
||||
let go_test_count = tag_strings.iter().filter(|&tag| tag == "go-test").count();
|
||||
let go_table_test_count = tag_strings
|
||||
.iter()
|
||||
.filter(|&tag| tag == "go-table-test-case")
|
||||
.count();
|
||||
|
||||
assert!(
|
||||
go_test_count == 1,
|
||||
"Should find exactly 1 go-test, found: {}",
|
||||
go_test_count
|
||||
);
|
||||
assert!(
|
||||
go_table_test_count == 2,
|
||||
"Should find exactly 2 go-table-test-case, found: {}",
|
||||
go_table_test_count
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_go_table_test_map_ignored(cx: &mut TestAppContext) {
|
||||
let language = language("go", tree_sitter_go::LANGUAGE.into());
|
||||
|
||||
let table_test = r#"
|
||||
package main
|
||||
|
||||
func Example() {
|
||||
_ = "some random string"
|
||||
|
||||
notATableTest := map[string]struct {
|
||||
someStr string
|
||||
}{
|
||||
"some string": {
|
||||
someStr: "foo",
|
||||
},
|
||||
"some other string": {
|
||||
someStr: "bar",
|
||||
},
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
let buffer =
|
||||
cx.new(|cx| crate::Buffer::local(table_test, cx).with_language(language.clone(), cx));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let runnables: Vec<_> = buffer.update(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
snapshot.runnable_ranges(0..table_test.len()).collect()
|
||||
});
|
||||
|
||||
let tag_strings: Vec<String> = runnables
|
||||
.iter()
|
||||
.flat_map(|r| &r.runnable.tags)
|
||||
.map(|tag| tag.0.to_string())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!tag_strings.contains(&"go-test".to_string()),
|
||||
"Should find go-test tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
assert!(
|
||||
!tag_strings.contains(&"go-table-test-case".to_string()),
|
||||
"Should find go-table-test-case tag, found: {:?}",
|
||||
tag_strings
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,3 +91,103 @@
|
||||
) @_
|
||||
(#set! tag go-main)
|
||||
)
|
||||
|
||||
; Table test cases - slice and map
|
||||
(
|
||||
(short_var_declaration
|
||||
left: (expression_list (identifier) @_collection_var)
|
||||
right: (expression_list
|
||||
(composite_literal
|
||||
type: [
|
||||
(slice_type)
|
||||
(map_type
|
||||
key: (type_identifier) @_key_type
|
||||
(#eq? @_key_type "string")
|
||||
)
|
||||
]
|
||||
body: (literal_value
|
||||
[
|
||||
(literal_element
|
||||
(literal_value
|
||||
(keyed_element
|
||||
(literal_element
|
||||
(identifier) @_field_name
|
||||
)
|
||||
(literal_element
|
||||
[
|
||||
(interpreted_string_literal) @run @_table_test_case_name
|
||||
(raw_string_literal) @run @_table_test_case_name
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(keyed_element
|
||||
(literal_element
|
||||
[
|
||||
(interpreted_string_literal) @run @_table_test_case_name
|
||||
(raw_string_literal) @run @_table_test_case_name
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(for_statement
|
||||
(range_clause
|
||||
left: (expression_list
|
||||
[
|
||||
(
|
||||
(identifier)
|
||||
(identifier) @_loop_var
|
||||
)
|
||||
(identifier) @_loop_var
|
||||
]
|
||||
)
|
||||
right: (identifier) @_range_var
|
||||
(#eq? @_range_var @_collection_var)
|
||||
)
|
||||
body: (block
|
||||
(expression_statement
|
||||
(call_expression
|
||||
function: (selector_expression
|
||||
operand: (identifier) @_t_var
|
||||
field: (field_identifier) @_run_method
|
||||
(#eq? @_run_method "Run")
|
||||
)
|
||||
arguments: (argument_list
|
||||
.
|
||||
[
|
||||
(selector_expression
|
||||
operand: (identifier) @_tc_var
|
||||
(#eq? @_tc_var @_loop_var)
|
||||
field: (field_identifier) @_field_check
|
||||
(#eq? @_field_check @_field_name)
|
||||
)
|
||||
(identifier) @_arg_var
|
||||
(#eq? @_arg_var @_loop_var)
|
||||
]
|
||||
.
|
||||
(func_literal
|
||||
parameters: (parameter_list
|
||||
(parameter_declaration
|
||||
type: (pointer_type
|
||||
(qualified_type
|
||||
package: (package_identifier) @_pkg
|
||||
name: (type_identifier) @_type
|
||||
(#eq? @_pkg "testing")
|
||||
(#eq? @_type "T")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) @_
|
||||
(#set! tag go-table-test-case)
|
||||
)
|
||||
|
||||
@@ -238,7 +238,7 @@ impl LspAdapter for RustLspAdapter {
|
||||
)
|
||||
.await?;
|
||||
make_file_executable(&server_path).await?;
|
||||
remove_matching(&container_dir, |path| server_path != path).await;
|
||||
remove_matching(&container_dir, |path| path != destination_path).await;
|
||||
GithubBinaryMetadata::write_to_file(
|
||||
&GithubBinaryMetadata {
|
||||
metadata_version: 1,
|
||||
@@ -1023,8 +1023,14 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
|
||||
last = Some(path);
|
||||
}
|
||||
|
||||
let path = last.context("no cached binary")?;
|
||||
let path = match RustLspAdapter::GITHUB_ASSET_KIND {
|
||||
AssetKind::TarGz | AssetKind::Gz => path.clone(), // Tar and gzip extract in place.
|
||||
AssetKind::Zip => path.clone().join("rust-analyzer.exe"), // zip contains a .exe
|
||||
};
|
||||
|
||||
anyhow::Ok(LanguageServerBinary {
|
||||
path: last.context("no cached binary")?,
|
||||
path,
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
})
|
||||
|
||||
@@ -256,7 +256,7 @@ impl Project {
|
||||
|
||||
let local_path = if is_ssh_terminal { None } else { path.clone() };
|
||||
|
||||
let mut python_venv_activate_command = None;
|
||||
let mut python_venv_activate_command = Task::ready(None);
|
||||
|
||||
let (spawn_task, shell) = match kind {
|
||||
TerminalKind::Shell(_) => {
|
||||
@@ -265,6 +265,7 @@ impl Project {
|
||||
python_venv_directory,
|
||||
&settings.detect_venv,
|
||||
&settings.shell,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -419,9 +420,12 @@ impl Project {
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Some(activate_command) = python_venv_activate_command {
|
||||
this.activate_python_virtual_environment(activate_command, &terminal_handle, cx);
|
||||
}
|
||||
this.activate_python_virtual_environment(
|
||||
python_venv_activate_command,
|
||||
&terminal_handle,
|
||||
cx,
|
||||
);
|
||||
|
||||
terminal_handle
|
||||
})
|
||||
}
|
||||
@@ -539,12 +543,15 @@ impl Project {
|
||||
venv_base_directory: &Path,
|
||||
venv_settings: &VenvSettings,
|
||||
shell: &Shell,
|
||||
) -> Option<String> {
|
||||
let venv_settings = venv_settings.as_option()?;
|
||||
cx: &mut App,
|
||||
) -> Task<Option<String>> {
|
||||
let Some(venv_settings) = venv_settings.as_option() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
let activate_keyword = match venv_settings.activate_script {
|
||||
terminal_settings::ActivateScript::Default => match std::env::consts::OS {
|
||||
"windows" => ".",
|
||||
_ => "source",
|
||||
_ => ".",
|
||||
},
|
||||
terminal_settings::ActivateScript::Nushell => "overlay use",
|
||||
terminal_settings::ActivateScript::PowerShell => ".",
|
||||
@@ -589,30 +596,44 @@ impl Project {
|
||||
.join(activate_script_name)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let quoted = shlex::try_quote(&path).ok()?;
|
||||
smol::block_on(self.fs.metadata(path.as_ref()))
|
||||
.ok()
|
||||
.flatten()?;
|
||||
|
||||
Some(format!(
|
||||
"{} {} ; clear{}",
|
||||
activate_keyword, quoted, line_ending
|
||||
))
|
||||
let is_valid_path = self.resolve_abs_path(path.as_ref(), cx);
|
||||
cx.background_spawn(async move {
|
||||
let quoted = shlex::try_quote(&path).ok()?;
|
||||
if is_valid_path.await.is_some_and(|meta| meta.is_file()) {
|
||||
Some(format!(
|
||||
"{} {} ; clear{}",
|
||||
activate_keyword, quoted, line_ending
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Some(format!(
|
||||
Task::ready(Some(format!(
|
||||
"{activate_keyword} {activate_script_name} {name}; clear{line_ending}",
|
||||
name = venv_settings.venv_name
|
||||
))
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_python_virtual_environment(
|
||||
&self,
|
||||
command: String,
|
||||
command: Task<Option<String>>,
|
||||
terminal_handle: &Entity<Terminal>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
terminal_handle.update(cx, |terminal, _| terminal.input(command.into_bytes()));
|
||||
terminal_handle.update(cx, |_, cx| {
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Some(command) = command.await {
|
||||
this.update(cx, |this, _| {
|
||||
this.input(command.into_bytes());
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
|
||||
|
||||
@@ -8,6 +8,7 @@ use gpui::{
|
||||
use language::{Buffer, BufferEvent, LanguageName, Toolchain};
|
||||
use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent};
|
||||
use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
|
||||
use util::maybe;
|
||||
use workspace::{StatusItemView, Workspace, item::ItemHandle};
|
||||
|
||||
use crate::ToolchainSelector;
|
||||
@@ -55,49 +56,61 @@ impl ActiveToolchain {
|
||||
}
|
||||
fn spawn_tracker_task(window: &mut Window, cx: &mut Context<Self>) -> Task<Option<()>> {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let active_file = this
|
||||
.read_with(cx, |this, _| {
|
||||
this.active_buffer
|
||||
.as_ref()
|
||||
.map(|(_, buffer, _)| buffer.clone())
|
||||
})
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let workspace = this.read_with(cx, |this, _| this.workspace.clone()).ok()?;
|
||||
let language_name = active_file
|
||||
.read_with(cx, |this, _| Some(this.language()?.name()))
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let term = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let languages = workspace.project().read(cx).languages();
|
||||
Project::toolchain_term(languages.clone(), language_name.clone())
|
||||
})
|
||||
.ok()?
|
||||
.await?;
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.term = term;
|
||||
cx.notify();
|
||||
});
|
||||
let (worktree_id, path) = active_file
|
||||
.update(cx, |this, cx| {
|
||||
this.file().and_then(|file| {
|
||||
Some((
|
||||
file.worktree_id(cx),
|
||||
Arc::<Path>::from(file.path().parent()?),
|
||||
))
|
||||
let did_set_toolchain = maybe!(async {
|
||||
let active_file = this
|
||||
.read_with(cx, |this, _| {
|
||||
this.active_buffer
|
||||
.as_ref()
|
||||
.map(|(_, buffer, _)| buffer.clone())
|
||||
})
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let workspace = this.read_with(cx, |this, _| this.workspace.clone()).ok()?;
|
||||
let language_name = active_file
|
||||
.read_with(cx, |this, _| Some(this.language()?.name()))
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let term = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let languages = workspace.project().read(cx).languages();
|
||||
Project::toolchain_term(languages.clone(), language_name.clone())
|
||||
})
|
||||
.ok()?
|
||||
.await?;
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.term = term;
|
||||
cx.notify();
|
||||
});
|
||||
let (worktree_id, path) = active_file
|
||||
.update(cx, |this, cx| {
|
||||
this.file().and_then(|file| {
|
||||
Some((
|
||||
file.worktree_id(cx),
|
||||
Arc::<Path>::from(file.path().parent()?),
|
||||
))
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let toolchain =
|
||||
Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.active_toolchain = Some(toolchain);
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let toolchain =
|
||||
Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?;
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
this.active_toolchain = Some(toolchain);
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
Some(())
|
||||
})
|
||||
.await
|
||||
.is_some();
|
||||
if !did_set_toolchain {
|
||||
this.update(cx, |this, cx| {
|
||||
this.active_toolchain = None;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
did_set_toolchain.then_some(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,6 +123,17 @@ impl ActiveToolchain {
|
||||
let editor = editor.read(cx);
|
||||
if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
|
||||
if let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) {
|
||||
if self
|
||||
.active_buffer
|
||||
.as_ref()
|
||||
.is_some_and(|(old_worktree_id, old_buffer, _)| {
|
||||
(old_worktree_id, old_buffer.entity_id())
|
||||
== (&worktree_id, buffer.entity_id())
|
||||
})
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let subscription = cx.subscribe_in(
|
||||
&buffer,
|
||||
window,
|
||||
@@ -231,7 +255,6 @@ impl StatusItemView for ActiveToolchain {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
self.active_toolchain.take();
|
||||
self.update_lister(editor, window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
|
||||
@@ -887,10 +887,10 @@ macro_rules! maybe {
|
||||
(|| $block)()
|
||||
};
|
||||
(async $block:block) => {
|
||||
(|| async $block)()
|
||||
(async || $block)()
|
||||
};
|
||||
(async move $block:block) => {
|
||||
(|| async move $block)()
|
||||
(async move || $block)()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1175,8 +1175,10 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
||||
VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"),
|
||||
VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
|
||||
VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
|
||||
VimCommand::new(("tabe", "dit"), workspace::NewFile),
|
||||
VimCommand::new(("tabnew", ""), workspace::NewFile),
|
||||
VimCommand::new(("tabe", "dit"), workspace::NewFile)
|
||||
.args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())),
|
||||
VimCommand::new(("tabnew", ""), workspace::NewFile)
|
||||
.args(|_action, args| Some(VimEdit { filename: args }.boxed_clone())),
|
||||
VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
|
||||
VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(),
|
||||
VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(),
|
||||
@@ -2476,4 +2478,110 @@ mod test {
|
||||
"});
|
||||
// Once ctrl-v to input character literals is added there should be a test for redo
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_tabnew(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// Create a new file to ensure that, when the filename is used with
|
||||
// `:tabnew`, it opens the existing file in a new tab.
|
||||
let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
|
||||
fs.as_fake()
|
||||
.insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
|
||||
.await;
|
||||
|
||||
cx.simulate_keystrokes(": tabnew");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
|
||||
|
||||
// Assert that the new tab is empty and not associated with any file, as
|
||||
// no file path was provided to the `:tabnew` command.
|
||||
cx.workspace(|workspace, _window, cx| {
|
||||
let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
|
||||
let buffer = active_editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap();
|
||||
|
||||
assert!(&buffer.read(cx).file().is_none());
|
||||
});
|
||||
|
||||
// Leverage the filename as an argument to the `:tabnew` command,
|
||||
// ensuring that the file, instead of an empty buffer, is opened in a
|
||||
// new tab.
|
||||
cx.simulate_keystrokes(": tabnew space dir/file_2.rs");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
|
||||
});
|
||||
|
||||
// If the `filename` argument provided to the `:tabnew` command is for a
|
||||
// file that doesn't yet exist, it should still associate the buffer
|
||||
// with that file path, so that when the buffer contents are saved, the
|
||||
// file is created.
|
||||
cx.simulate_keystrokes(": tabnew space dir/file_3.rs");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_tabedit(cx: &mut TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// Create a new file to ensure that, when the filename is used with
|
||||
// `:tabedit`, it opens the existing file in a new tab.
|
||||
let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone());
|
||||
fs.as_fake()
|
||||
.insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec())
|
||||
.await;
|
||||
|
||||
cx.simulate_keystrokes(": tabedit");
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2));
|
||||
|
||||
// Assert that the new tab is empty and not associated with any file, as
|
||||
// no file path was provided to the `:tabedit` command.
|
||||
cx.workspace(|workspace, _window, cx| {
|
||||
let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
|
||||
let buffer = active_editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap();
|
||||
|
||||
assert!(&buffer.read(cx).file().is_none());
|
||||
});
|
||||
|
||||
// Leverage the filename as an argument to the `:tabedit` command,
|
||||
// ensuring that the file, instead of an empty buffer, is opened in a
|
||||
// new tab.
|
||||
cx.simulate_keystrokes(": tabedit space dir/file_2.rs");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3));
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx);
|
||||
});
|
||||
|
||||
// If the `filename` argument provided to the `:tabedit` command is for a
|
||||
// file that doesn't yet exist, it should still associate the buffer
|
||||
// with that file path, so that when the buffer contents are saved, the
|
||||
// file is created.
|
||||
cx.simulate_keystrokes(": tabedit space dir/file_3.rs");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4));
|
||||
cx.workspace(|workspace, _, cx| {
|
||||
assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,6 +542,20 @@ define_connection! {
|
||||
ALTER TABLE breakpoints ADD COLUMN condition TEXT;
|
||||
ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
|
||||
),
|
||||
sql!(CREATE TABLE toolchains2 (
|
||||
workspace_id INTEGER,
|
||||
worktree_id INTEGER,
|
||||
language_name TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
raw_json TEXT NOT NULL,
|
||||
relative_worktree_path TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
|
||||
INSERT INTO toolchains2
|
||||
SELECT * FROM toolchains;
|
||||
DROP TABLE toolchains;
|
||||
ALTER TABLE toolchains2 RENAME TO toolchains;
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1428,12 +1442,12 @@ impl WorkspaceDb {
|
||||
self.write(move |conn| {
|
||||
let mut insert = conn
|
||||
.exec_bound(sql!(
|
||||
INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path) VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT DO
|
||||
UPDATE SET
|
||||
name = ?5,
|
||||
path = ?6
|
||||
|
||||
path = ?6,
|
||||
raw_json = ?7
|
||||
))
|
||||
.context("Preparing insertion")?;
|
||||
|
||||
@@ -1444,6 +1458,7 @@ impl WorkspaceDb {
|
||||
toolchain.language_name.as_ref(),
|
||||
toolchain.name.as_ref(),
|
||||
toolchain.path.as_ref(),
|
||||
toolchain.as_json.to_string(),
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 189 KiB |
@@ -26,6 +26,7 @@ collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
copilot.workspace = true
|
||||
db.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
@@ -33,13 +34,13 @@ futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -429,6 +429,7 @@ impl Zeta {
|
||||
body,
|
||||
editable_range,
|
||||
} = gather_task.await?;
|
||||
let done_gathering_context_at = Instant::now();
|
||||
|
||||
log::debug!(
|
||||
"Events:\n{}\nExcerpt:\n{:?}",
|
||||
@@ -481,6 +482,7 @@ impl Zeta {
|
||||
}
|
||||
};
|
||||
|
||||
let received_response_at = Instant::now();
|
||||
log::debug!("completion response: {}", &response.output_excerpt);
|
||||
|
||||
if let Some(usage) = usage {
|
||||
@@ -492,7 +494,7 @@ impl Zeta {
|
||||
.ok();
|
||||
}
|
||||
|
||||
Self::process_completion_response(
|
||||
let edit_prediction = Self::process_completion_response(
|
||||
response,
|
||||
buffer,
|
||||
&snapshot,
|
||||
@@ -505,7 +507,25 @@ impl Zeta {
|
||||
buffer_snapshotted_at,
|
||||
&cx,
|
||||
)
|
||||
.await
|
||||
.await;
|
||||
|
||||
let finished_at = Instant::now();
|
||||
|
||||
// record latency for ~1% of requests
|
||||
if rand::random::<u8>() <= 2 {
|
||||
telemetry::event!(
|
||||
"Edit Prediction Request",
|
||||
context_latency = done_gathering_context_at
|
||||
.duration_since(buffer_snapshotted_at)
|
||||
.as_millis(),
|
||||
request_latency = received_response_at
|
||||
.duration_since(done_gathering_context_at)
|
||||
.as_millis(),
|
||||
process_latency = finished_at.duration_since(received_response_at).as_millis()
|
||||
);
|
||||
}
|
||||
|
||||
edit_prediction
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -391,7 +391,7 @@ Zed will also use the `OPENAI_API_KEY` environment variable if it's defined.
|
||||
|
||||
#### Custom Models {#openai-custom-models}
|
||||
|
||||
The Zed agent comes pre-configured to use the latest version for common models (GPT-3.5 Turbo, GPT-4, GPT-4 Turbo, GPT-4o, GPT-4o mini).
|
||||
The Zed agent comes pre-configured to use the latest version for common models (GPT-5, GPT-5 mini, o4-mini, GPT-4.1, and others).
|
||||
To use alternate models, perhaps a preview release or a dated model release, or if you wish to control the request parameters, you can do so by adding the following to your Zed `settings.json`:
|
||||
|
||||
```json
|
||||
|
||||
Reference in New Issue
Block a user