Merge branch 'main' into faster_arm_linux

This commit is contained in:
Peter Tripp 2025-08-20 15:25:38 -04:00
commit 044594da78
47 changed files with 766 additions and 301 deletions

4
Cargo.lock generated
View file

@ -171,9 +171,9 @@ dependencies = [
[[package]] [[package]]
name = "agent-client-protocol" name = "agent-client-protocol"
version = "0.0.26" version = "0.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "160971bb53ca0b2e70ebc857c21e24eb448745f1396371015f4c59e9a9e51ed0" checksum = "4c887e795097665ab95119580534e7cc1335b59e1a7fec296943e534b970f4ed"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures 0.3.31", "futures 0.3.31",

View file

@ -423,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# #
agentic-coding-protocol = "0.0.10" agentic-coding-protocol = "0.0.10"
agent-client-protocol = "0.0.26" agent-client-protocol = "0.0.28"
aho-corasick = "1.1" aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14" any_vec = "0.14"
@ -900,10 +900,12 @@ needless_parens_on_range_literals = "warn"
needless_pub_self = "warn" needless_pub_self = "warn"
needless_return = "warn" needless_return = "warn"
needless_return_with_question_mark = "warn" needless_return_with_question_mark = "warn"
non_minimal_cfg = "warn"
ok_expect = "warn" ok_expect = "warn"
owned_cow = "warn" owned_cow = "warn"
print_literal = "warn" print_literal = "warn"
print_with_newline = "warn" print_with_newline = "warn"
println_empty_string = "warn"
ptr_eq = "warn" ptr_eq = "warn"
question_mark = "warn" question_mark = "warn"
redundant_closure = "warn" redundant_closure = "warn"
@ -924,7 +926,9 @@ unneeded_struct_pattern = "warn"
unsafe_removed_from_name = "warn" unsafe_removed_from_name = "warn"
unused_unit = "warn" unused_unit = "warn"
unusual_byte_groupings = "warn" unusual_byte_groupings = "warn"
while_let_on_iterator = "warn"
write_literal = "warn" write_literal = "warn"
write_with_newline = "warn"
writeln_empty_string = "warn" writeln_empty_string = "warn"
wrong_self_convention = "warn" wrong_self_convention = "warn"
zero_ptr = "warn" zero_ptr = "warn"

View file

@ -2598,6 +2598,14 @@ mod tests {
} }
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
let sessions = self.sessions.lock(); let sessions = self.sessions.lock();
let thread = sessions.get(session_id).unwrap().clone(); let thread = sessions.get(session_id).unwrap().clone();

View file

@ -38,6 +38,8 @@ pub trait AgentConnection {
cx: &mut App, cx: &mut App,
) -> Task<Result<acp::PromptResponse>>; ) -> Task<Result<acp::PromptResponse>>;
fn prompt_capabilities(&self) -> acp::PromptCapabilities;
fn resume( fn resume(
&self, &self,
_session_id: &acp::SessionId, _session_id: &acp::SessionId,
@ -64,6 +66,10 @@ pub trait AgentConnection {
None None
} }
fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any>; fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
} }
@ -81,6 +87,19 @@ pub trait AgentSessionResume {
fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>; fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
} }
pub trait AgentTelemetry {
/// The name of the agent used for telemetry.
fn agent_name(&self) -> String;
/// A representation of the current thread state that can be serialized for
/// storage with telemetry events.
fn thread_data(
&self,
session_id: &acp::SessionId,
cx: &mut App,
) -> Task<Result<serde_json::Value>>;
}
#[derive(Debug)] #[derive(Debug)]
pub struct AuthRequired { pub struct AuthRequired {
pub description: Option<String>, pub description: Option<String>,
@ -317,6 +336,14 @@ mod test_support {
Task::ready(Ok(thread)) Task::ready(Ok(thread))
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}
}
fn authenticate( fn authenticate(
&self, &self,
_method_id: acp::AuthMethodId, _method_id: acp::AuthMethodId,

View file

@ -362,7 +362,7 @@ impl Display for DirectoryContext {
let mut is_first = true; let mut is_first = true;
for descendant in &self.descendants { for descendant in &self.descendants {
if !is_first { if !is_first {
write!(f, "\n")?; writeln!(f)?;
} else { } else {
is_first = false; is_first = false;
} }
@ -650,7 +650,7 @@ impl TextThreadContextHandle {
impl Display for TextThreadContext { impl Display for TextThreadContext {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// TODO: escape title? // TODO: escape title?
write!(f, "<text_thread title=\"{}\">\n", self.title)?; writeln!(f, "<text_thread title=\"{}\">", self.title)?;
write!(f, "{}", self.text.trim())?; write!(f, "{}", self.text.trim())?;
write!(f, "\n</text_thread>") write!(f, "\n</text_thread>")
} }
@ -716,7 +716,7 @@ impl RulesContextHandle {
impl Display for RulesContext { impl Display for RulesContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(title) = &self.title { if let Some(title) = &self.title {
write!(f, "Rules title: {}\n", title)?; writeln!(f, "Rules title: {}", title)?;
} }
let code_block = MarkdownCodeBlock { let code_block = MarkdownCodeBlock {
tag: "", tag: "",

View file

@ -387,7 +387,6 @@ pub struct Thread {
cumulative_token_usage: TokenUsage, cumulative_token_usage: TokenUsage,
exceeded_window_error: Option<ExceededWindowError>, exceeded_window_error: Option<ExceededWindowError>,
tool_use_limit_reached: bool, tool_use_limit_reached: bool,
feedback: Option<ThreadFeedback>,
retry_state: Option<RetryState>, retry_state: Option<RetryState>,
message_feedback: HashMap<MessageId, ThreadFeedback>, message_feedback: HashMap<MessageId, ThreadFeedback>,
last_received_chunk_at: Option<Instant>, last_received_chunk_at: Option<Instant>,
@ -487,7 +486,6 @@ impl Thread {
cumulative_token_usage: TokenUsage::default(), cumulative_token_usage: TokenUsage::default(),
exceeded_window_error: None, exceeded_window_error: None,
tool_use_limit_reached: false, tool_use_limit_reached: false,
feedback: None,
retry_state: None, retry_state: None,
message_feedback: HashMap::default(), message_feedback: HashMap::default(),
last_error_context: None, last_error_context: None,
@ -612,7 +610,6 @@ impl Thread {
cumulative_token_usage: serialized.cumulative_token_usage, cumulative_token_usage: serialized.cumulative_token_usage,
exceeded_window_error: None, exceeded_window_error: None,
tool_use_limit_reached: serialized.tool_use_limit_reached, tool_use_limit_reached: serialized.tool_use_limit_reached,
feedback: None,
message_feedback: HashMap::default(), message_feedback: HashMap::default(),
last_error_context: None, last_error_context: None,
last_received_chunk_at: None, last_received_chunk_at: None,
@ -2787,10 +2784,6 @@ impl Thread {
cx.emit(ThreadEvent::CancelEditing); cx.emit(ThreadEvent::CancelEditing);
} }
pub fn feedback(&self) -> Option<ThreadFeedback> {
self.feedback
}
pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> { pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> {
self.message_feedback.get(&message_id).copied() self.message_feedback.get(&message_id).copied()
} }
@ -2852,52 +2845,6 @@ impl Thread {
}) })
} }
pub fn report_feedback(
&mut self,
feedback: ThreadFeedback,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let last_assistant_message_id = self
.messages
.iter()
.rev()
.find(|msg| msg.role == Role::Assistant)
.map(|msg| msg.id);
if let Some(message_id) = last_assistant_message_id {
self.report_message_feedback(message_id, feedback, cx)
} else {
let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
let serialized_thread = self.serialize(cx);
let thread_id = self.id().clone();
let client = self.project.read(cx).client();
self.feedback = Some(feedback);
cx.notify();
cx.background_spawn(async move {
let final_project_snapshot = final_project_snapshot.await;
let serialized_thread = serialized_thread.await?;
let thread_data = serde_json::to_value(serialized_thread)
.unwrap_or_else(|_| serde_json::Value::Null);
let rating = match feedback {
ThreadFeedback::Positive => "positive",
ThreadFeedback::Negative => "negative",
};
telemetry::event!(
"Assistant Thread Rated",
rating,
thread_id,
thread_data,
final_project_snapshot
);
client.telemetry().flush_events().await;
Ok(())
})
}
}
/// Create a snapshot of the current project state including git information and unsaved buffers. /// Create a snapshot of the current project state including git information and unsaved buffers.
fn project_snapshot( fn project_snapshot(
project: Entity<Project>, project: Entity<Project>,

View file

@ -913,6 +913,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}) })
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: false,
embedded_context: true,
}
}
fn resume( fn resume(
&self, &self,
session_id: &acp::SessionId, session_id: &acp::SessionId,
@ -948,11 +956,36 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}) })
} }
fn telemetry(&self) -> Option<Rc<dyn acp_thread::AgentTelemetry>> {
Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>)
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> { fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self self
} }
} }
impl acp_thread::AgentTelemetry for NativeAgentConnection {
fn agent_name(&self) -> String {
"Zed".into()
}
fn thread_data(
&self,
session_id: &acp::SessionId,
cx: &mut App,
) -> Task<Result<serde_json::Value>> {
let Some(session) = self.0.read(cx).sessions.get(session_id) else {
return Task::ready(Err(anyhow!("Session not found")));
};
let task = session.thread.read(cx).to_db(cx);
cx.background_spawn(async move {
serde_json::to_value(task.await).context("Failed to serialize thread")
})
}
}
struct NativeAgentSessionEditor { struct NativeAgentSessionEditor {
thread: Entity<Thread>, thread: Entity<Thread>,
acp_thread: WeakEntity<AcpThread>, acp_thread: WeakEntity<AcpThread>,

View file

@ -161,9 +161,9 @@ impl UserMessage {
} }
UserMessageContent::Mention { uri, content } => { UserMessageContent::Mention { uri, content } => {
if !content.is_empty() { if !content.is_empty() {
let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content); let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content);
} else { } else {
let _ = write!(&mut markdown, "{}\n", uri.as_link()); let _ = writeln!(&mut markdown, "{}", uri.as_link());
} }
} }
} }

View file

@ -8,16 +8,11 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use util::markdown::MarkdownInlineCode; use util::markdown::MarkdownInlineCode;
/// Copies a file or directory in the project, and returns confirmation that the /// Copies a file or directory in the project, and returns confirmation that the copy succeeded.
/// copy succeeded.
///
/// Directory contents will be copied recursively (like `cp -r`). /// Directory contents will be copied recursively (like `cp -r`).
/// ///
/// This tool should be used when it's desirable to create a copy of a file or /// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
/// directory without modifying the original. It's much more efficient than /// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal.
/// doing this by separately reading and then writing the file or directory's
/// contents, so this tool should be preferred over that approach whenever
/// copying is the goal.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CopyPathToolInput { pub struct CopyPathToolInput {
/// The source path of the file or directory to copy. /// The source path of the file or directory to copy.
@ -33,12 +28,10 @@ pub struct CopyPathToolInput {
/// You can copy the first file by providing a source_path of "directory1/a/something.txt" /// You can copy the first file by providing a source_path of "directory1/a/something.txt"
/// </example> /// </example>
pub source_path: String, pub source_path: String,
/// The destination path where the file or directory should be copied to. /// The destination path where the file or directory should be copied to.
/// ///
/// <example> /// <example>
/// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt"
/// provide a destination_path of "directory2/b/copy.txt"
/// </example> /// </example>
pub destination_path: String, pub destination_path: String,
} }

View file

@ -9,12 +9,9 @@ use util::markdown::MarkdownInlineCode;
use crate::{AgentTool, ToolCallEventStream}; use crate::{AgentTool, ToolCallEventStream};
/// Creates a new directory at the specified path within the project. Returns /// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
/// confirmation that the directory was created.
/// ///
/// This tool creates a directory and all necessary parent directories (similar /// This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.
/// to `mkdir -p`). It should be used whenever you need to create new
/// directories within the project.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateDirectoryToolInput { pub struct CreateDirectoryToolInput {
/// The path of the new directory. /// The path of the new directory.

View file

@ -9,8 +9,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
/// Deletes the file or directory (and the directory's contents, recursively) at /// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion.
/// the specified path in the project, and returns confirmation of the deletion.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeletePathToolInput { pub struct DeletePathToolInput {
/// The path of the file or directory to delete. /// The path of the file or directory to delete.

View file

@ -34,25 +34,21 @@ const DEFAULT_UI_TEXT: &str = "Editing file";
/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location /// - Use the `list_directory` tool to verify the parent directory exists and is the correct location
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput { pub struct EditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit.
/// shown in the UI and also passed to another model to perform the edit.
/// ///
/// Be terse, but also descriptive in what you want to achieve with this /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.
/// edit. Avoid generic instructions.
/// ///
/// NEVER mention the file path in this description. /// NEVER mention the file path in this description.
/// ///
/// <example>Fix API endpoint URLs</example> /// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example> /// <example>Update copyright year in `page_footer`</example>
/// ///
/// Make sure to include this field before all the others in the input object /// Make sure to include this field before all the others in the input object so that we can display it immediately.
/// so that we can display it immediately.
pub display_description: String, pub display_description: String,
/// The full path of the file to create or modify in the project. /// The full path of the file to create or modify in the project.
/// ///
/// WARNING: When specifying which file path need changing, you MUST /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.
/// start each path with one of the project's root directories.
/// ///
/// The following examples assume we have two root directories in the project: /// The following examples assume we have two root directories in the project:
/// - /a/b/backend /// - /a/b/backend
@ -61,22 +57,19 @@ pub struct EditFileToolInput {
/// <example> /// <example>
/// `backend/src/main.rs` /// `backend/src/main.rs`
/// ///
/// Notice how the file path starts with `backend`. Without that, the path /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
/// would be ambiguous and the call would fail!
/// </example> /// </example>
/// ///
/// <example> /// <example>
/// `frontend/db.js` /// `frontend/db.js`
/// </example> /// </example>
pub path: PathBuf, pub path: PathBuf,
/// The mode of operation on the file. Possible values: /// The mode of operation on the file. Possible values:
/// - 'edit': Make granular edits to an existing file. /// - 'edit': Make granular edits to an existing file.
/// - 'create': Create a new file if it doesn't exist. /// - 'create': Create a new file if it doesn't exist.
/// - 'overwrite': Replace the entire contents of an existing file. /// - 'overwrite': Replace the entire contents of an existing file.
/// ///
/// When a file already exists or you just created it, prefer editing /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.
/// it as opposed to recreating it from scratch.
pub mode: EditFileMode, pub mode: EditFileMode,
} }

View file

@ -31,7 +31,6 @@ pub struct FindPathToolInput {
/// You can get back the first two paths by providing a glob of "*thing*.txt" /// You can get back the first two paths by providing a glob of "*thing*.txt"
/// </example> /// </example>
pub glob: String, pub glob: String,
/// Optional starting position for paginated results (0-based). /// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning. /// When not provided, starts from the beginning.
#[serde(default)] #[serde(default)]

View file

@ -27,8 +27,7 @@ use util::paths::PathMatcher;
/// - DO NOT use HTML entities solely to escape characters in the tool parameters. /// - DO NOT use HTML entities solely to escape characters in the tool parameters.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GrepToolInput { pub struct GrepToolInput {
/// A regex pattern to search for in the entire project. Note that the regex /// A regex pattern to search for in the entire project. Note that the regex will be parsed by the Rust `regex` crate.
/// will be parsed by the Rust `regex` crate.
/// ///
/// Do NOT specify a path here! This will only be matched against the code **content**. /// Do NOT specify a path here! This will only be matched against the code **content**.
pub regex: String, pub regex: String,

View file

@ -10,14 +10,12 @@ use std::fmt::Write;
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use util::markdown::MarkdownInlineCode; use util::markdown::MarkdownInlineCode;
/// Lists files and directories in a given path. Prefer the `grep` or /// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
/// `find_path` tools when searching the codebase.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListDirectoryToolInput { pub struct ListDirectoryToolInput {
/// The fully-qualified path of the directory to list in the project. /// The fully-qualified path of the directory to list in the project.
/// ///
/// This path should never be absolute, and the first component /// This path should never be absolute, and the first component of the path should always be a root directory in a project.
/// of the path should always be a root directory in a project.
/// ///
/// <example> /// <example>
/// If the project has the following root directories: /// If the project has the following root directories:

View file

@ -8,14 +8,11 @@ use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use util::markdown::MarkdownInlineCode; use util::markdown::MarkdownInlineCode;
/// Moves or rename a file or directory in the project, and returns confirmation /// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded.
/// that the move succeeded.
/// ///
/// If the source and destination directories are the same, but the filename is /// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move.
/// different, this performs a rename. Otherwise, it performs a move.
/// ///
/// This tool should be used when it's desirable to move or rename a file or /// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all.
/// directory without changing its contents at all.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct MovePathToolInput { pub struct MovePathToolInput {
/// The source path of the file or directory to move/rename. /// The source path of the file or directory to move/rename.

View file

@ -8,19 +8,15 @@ use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use util::markdown::MarkdownEscaped; use util::markdown::MarkdownEscaped;
/// This tool opens a file or URL with the default application associated with /// This tool opens a file or URL with the default application associated with it on the user's operating system:
/// it on the user's operating system:
/// ///
/// - On macOS, it's equivalent to the `open` command /// - On macOS, it's equivalent to the `open` command
/// - On Windows, it's equivalent to `start` /// - On Windows, it's equivalent to `start`
/// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate /// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate
/// ///
/// For example, it can open a web browser with a URL, open a PDF file with the /// For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc.
/// default PDF viewer, etc.
/// ///
/// You MUST ONLY use this tool when the user has explicitly requested opening /// You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that the user would like for you to use this tool.
/// something. You MUST NEVER assume that the user would like for you to use
/// this tool.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct OpenToolInput { pub struct OpenToolInput {
/// The path or URL to open with the default application. /// The path or URL to open with the default application.

View file

@ -21,8 +21,7 @@ use crate::{AgentTool, ToolCallEventStream};
pub struct ReadFileToolInput { pub struct ReadFileToolInput {
/// The relative path of the file to read. /// The relative path of the file to read.
/// ///
/// This path should never be absolute, and the first component /// This path should never be absolute, and the first component of the path should always be a root directory in a project.
/// of the path should always be a root directory in a project.
/// ///
/// <example> /// <example>
/// If the project has the following root directories: /// If the project has the following root directories:
@ -34,11 +33,9 @@ pub struct ReadFileToolInput {
/// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
/// </example> /// </example>
pub path: String, pub path: String,
/// Optional line number to start reading on (1-based index) /// Optional line number to start reading on (1-based index)
#[serde(default)] #[serde(default)]
pub start_line: Option<u32>, pub start_line: Option<u32>,
/// Optional line number to end reading on (1-based index, inclusive) /// Optional line number to end reading on (1-based index, inclusive)
#[serde(default)] #[serde(default)]
pub end_line: Option<u32>, pub end_line: Option<u32>,

View file

@ -11,8 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
/// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action. /// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ThinkingToolInput { pub struct ThinkingToolInput {
/// Content to think about. This should be a description of what to think about or /// Content to think about. This should be a description of what to think about or a problem to solve.
/// a problem to solve.
content: String, content: String,
} }

View file

@ -14,7 +14,7 @@ use ui::prelude::*;
use web_search::WebSearchRegistry; use web_search::WebSearchRegistry;
/// Search the web for information using your query. /// Search the web for information using your query.
/// Use this when you need real-time information, facts, or data that might not be in your training. \ /// Use this when you need real-time information, facts, or data that might not be in your training.
/// Results will include snippets and links from relevant web pages. /// Results will include snippets and links from relevant web pages.
#[derive(Debug, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput { pub struct WebSearchToolInput {

View file

@ -498,6 +498,14 @@ impl AgentConnection for AcpConnection {
}) })
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: false,
audio: false,
embedded_context: false,
}
}
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
let task = self let task = self
.connection .connection

View file

@ -21,6 +21,7 @@ pub struct AcpConnection {
connection: Rc<acp::ClientSideConnection>, connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>, sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>, auth_methods: Vec<acp::AuthMethod>,
prompt_capabilities: acp::PromptCapabilities,
_io_task: Task<Result<()>>, _io_task: Task<Result<()>>,
} }
@ -119,6 +120,7 @@ impl AcpConnection {
connection: connection.into(), connection: connection.into(),
server_name, server_name,
sessions, sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
_io_task: io_task, _io_task: io_task,
}) })
} }
@ -206,6 +208,10 @@ impl AgentConnection for AcpConnection {
}) })
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
self.prompt_capabilities
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
let conn = self.connection.clone(); let conn = self.connection.clone();
let params = acp::CancelNotification { let params = acp::CancelNotification {

View file

@ -319,6 +319,14 @@ impl AgentConnection for ClaudeAgentConnection {
cx.foreground_executor().spawn(async move { end_rx.await? }) cx.foreground_executor().spawn(async move { end_rx.await? })
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: false,
embedded_context: true,
}
}
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
let sessions = self.sessions.borrow(); let sessions = self.sessions.borrow();
let Some(session) = sessions.get(session_id) else { let Some(session) = sessions.get(session_id) else {

View file

@ -1,8 +1,11 @@
use std::cell::Cell;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use acp_thread::MentionUri; use acp_thread::MentionUri;
use agent_client_protocol as acp;
use agent2::{HistoryEntry, HistoryStore}; use agent2::{HistoryEntry, HistoryStore};
use anyhow::Result; use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId}; use editor::{CompletionProvider, Editor, ExcerptId};
@ -63,6 +66,7 @@ pub struct ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>, history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
} }
impl ContextPickerCompletionProvider { impl ContextPickerCompletionProvider {
@ -71,12 +75,14 @@ impl ContextPickerCompletionProvider {
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>, history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
) -> Self { ) -> Self {
Self { Self {
message_editor, message_editor,
workspace, workspace,
history_store, history_store,
prompt_store, prompt_store,
prompt_capabilities,
} }
} }
@ -544,17 +550,19 @@ impl ContextPickerCompletionProvider {
}), }),
); );
const RECENT_COUNT: usize = 2; if self.prompt_capabilities.get().embedded_context {
let threads = self const RECENT_COUNT: usize = 2;
.history_store let threads = self
.read(cx) .history_store
.recently_opened_entries(cx) .read(cx)
.into_iter() .recently_opened_entries(cx)
.filter(|thread| !mentions.contains(&thread.mention_uri())) .into_iter()
.take(RECENT_COUNT) .filter(|thread| !mentions.contains(&thread.mention_uri()))
.collect::<Vec<_>>(); .take(RECENT_COUNT)
.collect::<Vec<_>>();
recent.extend(threads.into_iter().map(Match::RecentThread)); recent.extend(threads.into_iter().map(Match::RecentThread));
}
recent recent
} }
@ -564,11 +572,17 @@ impl ContextPickerCompletionProvider {
workspace: &Entity<Workspace>, workspace: &Entity<Workspace>,
cx: &mut App, cx: &mut App,
) -> Vec<ContextPickerEntry> { ) -> Vec<ContextPickerEntry> {
let mut entries = vec![ let embedded_context = self.prompt_capabilities.get().embedded_context;
ContextPickerEntry::Mode(ContextPickerMode::File), let mut entries = if embedded_context {
ContextPickerEntry::Mode(ContextPickerMode::Symbol), vec![
ContextPickerEntry::Mode(ContextPickerMode::Thread), ContextPickerEntry::Mode(ContextPickerMode::File),
]; ContextPickerEntry::Mode(ContextPickerMode::Symbol),
ContextPickerEntry::Mode(ContextPickerMode::Thread),
]
} else {
// File is always available, but we don't need a mode entry
vec![]
};
let has_selection = workspace let has_selection = workspace
.read(cx) .read(cx)
@ -583,11 +597,13 @@ impl ContextPickerCompletionProvider {
)); ));
} }
if self.prompt_store.is_some() { if embedded_context {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules)); if self.prompt_store.is_some() {
} entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
}
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch)); entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
}
entries entries
} }
@ -625,7 +641,11 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let offset_to_line = buffer.point_to_offset(line_start); let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines(); let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?; let line = lines.next()?;
MentionCompletion::try_parse(line, offset_to_line) MentionCompletion::try_parse(
self.prompt_capabilities.get().embedded_context,
line,
offset_to_line,
)
}); });
let Some(state) = state else { let Some(state) = state else {
return Task::ready(Ok(Vec::new())); return Task::ready(Ok(Vec::new()));
@ -745,12 +765,16 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let offset_to_line = buffer.point_to_offset(line_start); let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines(); let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() { if let Some(line) = lines.next() {
MentionCompletion::try_parse(line, offset_to_line) MentionCompletion::try_parse(
.map(|completion| { self.prompt_capabilities.get().embedded_context,
completion.source_range.start <= offset_to_line + position.column as usize line,
&& completion.source_range.end >= offset_to_line + position.column as usize offset_to_line,
}) )
.unwrap_or(false) .map(|completion| {
completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize
})
.unwrap_or(false)
} else { } else {
false false
} }
@ -841,7 +865,7 @@ struct MentionCompletion {
} }
impl MentionCompletion { impl MentionCompletion {
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> { fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> {
let last_mention_start = line.rfind('@')?; let last_mention_start = line.rfind('@')?;
if last_mention_start >= line.len() { if last_mention_start >= line.len() {
return Some(Self::default()); return Some(Self::default());
@ -865,7 +889,9 @@ impl MentionCompletion {
if let Some(mode_text) = parts.next() { if let Some(mode_text) = parts.next() {
end += mode_text.len(); end += mode_text.len();
if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() { if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok()
&& (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File))
{
mode = Some(parsed_mode); mode = Some(parsed_mode);
} else { } else {
argument = Some(mode_text.to_string()); argument = Some(mode_text.to_string());
@ -898,10 +924,10 @@ mod tests {
#[test] #[test]
fn test_mention_completion_parse() { fn test_mention_completion_parse() {
assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None); assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None);
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @", 0), MentionCompletion::try_parse(true, "Lorem @", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..7, source_range: 6..7,
mode: None, mode: None,
@ -910,7 +936,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file", 0), MentionCompletion::try_parse(true, "Lorem @file", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..11, source_range: 6..11,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -919,7 +945,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file ", 0), MentionCompletion::try_parse(true, "Lorem @file ", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..12, source_range: 6..12,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -928,7 +954,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file main.rs", 0), MentionCompletion::try_parse(true, "Lorem @file main.rs", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..19, source_range: 6..19,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -937,7 +963,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file main.rs ", 0), MentionCompletion::try_parse(true, "Lorem @file main.rs ", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..19, source_range: 6..19,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -946,7 +972,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0), MentionCompletion::try_parse(true, "Lorem @file main.rs Ipsum", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..19, source_range: 6..19,
mode: Some(ContextPickerMode::File), mode: Some(ContextPickerMode::File),
@ -955,7 +981,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
MentionCompletion::try_parse("Lorem @main", 0), MentionCompletion::try_parse(true, "Lorem @main", 0),
Some(MentionCompletion { Some(MentionCompletion {
source_range: 6..11, source_range: 6..11,
mode: None, mode: None,
@ -963,6 +989,28 @@ mod tests {
}) })
); );
assert_eq!(MentionCompletion::try_parse("test@", 0), None); assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None);
// Allowed non-file mentions
assert_eq!(
MentionCompletion::try_parse(true, "Lorem @symbol main", 0),
Some(MentionCompletion {
source_range: 6..18,
mode: Some(ContextPickerMode::Symbol),
argument: Some("main".to_string()),
})
);
// Disallowed non-file mentions
assert_eq!(
MentionCompletion::try_parse(false, "Lorem @symbol main", 0),
Some(MentionCompletion {
source_range: 6..18,
mode: None,
argument: Some("main".to_string()),
})
);
} }
} }

View file

@ -34,7 +34,7 @@ use settings::Settings;
use std::{ use std::{
cell::Cell, cell::Cell,
ffi::OsStr, ffi::OsStr,
fmt::{Display, Write}, fmt::Write,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
@ -51,7 +51,10 @@ use ui::{
}; };
use url::Url; use url::Url;
use util::ResultExt; use util::ResultExt;
use workspace::{Workspace, notifications::NotifyResultExt as _}; use workspace::{
Toast, Workspace,
notifications::{NotificationId, NotifyResultExt as _},
};
use zed_actions::agent::Chat; use zed_actions::agent::Chat;
const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50); const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50);
@ -64,6 +67,7 @@ pub struct MessageEditor {
history_store: Entity<HistoryStore>, history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
prevent_slash_commands: bool, prevent_slash_commands: bool,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
_parse_slash_command_task: Task<()>, _parse_slash_command_task: Task<()>,
} }
@ -96,11 +100,13 @@ impl MessageEditor {
}, },
None, None,
); );
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let completion_provider = ContextPickerCompletionProvider::new( let completion_provider = ContextPickerCompletionProvider::new(
cx.weak_entity(), cx.weak_entity(),
workspace.clone(), workspace.clone(),
history_store.clone(), history_store.clone(),
prompt_store.clone(), prompt_store.clone(),
prompt_capabilities.clone(),
); );
let semantics_provider = Rc::new(SlashCommandSemanticsProvider { let semantics_provider = Rc::new(SlashCommandSemanticsProvider {
range: Cell::new(None), range: Cell::new(None),
@ -158,6 +164,7 @@ impl MessageEditor {
history_store, history_store,
prompt_store, prompt_store,
prevent_slash_commands, prevent_slash_commands,
prompt_capabilities,
_subscriptions: subscriptions, _subscriptions: subscriptions,
_parse_slash_command_task: Task::ready(()), _parse_slash_command_task: Task::ready(()),
} }
@ -193,6 +200,10 @@ impl MessageEditor {
.detach(); .detach();
} }
pub fn set_prompt_capabilities(&mut self, capabilities: acp::PromptCapabilities) {
self.prompt_capabilities.set(capabilities);
}
#[cfg(test)] #[cfg(test)]
pub(crate) fn editor(&self) -> &Entity<Editor> { pub(crate) fn editor(&self) -> &Entity<Editor> {
&self.editor &self.editor
@ -230,7 +241,7 @@ impl MessageEditor {
let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else { let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
return Task::ready(()); return Task::ready(());
}; };
let Some(anchor) = snapshot let Some(start_anchor) = snapshot
.buffer_snapshot .buffer_snapshot
.anchor_in_excerpt(*excerpt_id, start) .anchor_in_excerpt(*excerpt_id, start)
else { else {
@ -244,6 +255,33 @@ impl MessageEditor {
.unwrap_or_default(); .unwrap_or_default();
if Img::extensions().contains(&extension) && !extension.contains("svg") { if Img::extensions().contains(&extension) && !extension.contains("svg") {
if !self.prompt_capabilities.get().image {
struct ImagesNotAllowed;
let end_anchor = snapshot.buffer_snapshot.anchor_before(
start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1,
);
self.editor.update(cx, |editor, cx| {
// Remove mention
editor.edit([((start_anchor..end_anchor), "")], cx);
});
self.workspace
.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
NotificationId::unique::<ImagesNotAllowed>(),
"This agent does not support images yet",
)
.autohide(),
cx,
);
})
.ok();
return Task::ready(());
}
let project = self.project.clone(); let project = self.project.clone();
let Some(project_path) = project let Some(project_path) = project
.read(cx) .read(cx)
@ -277,7 +315,7 @@ impl MessageEditor {
}; };
return self.confirm_mention_for_image( return self.confirm_mention_for_image(
crease_id, crease_id,
anchor, start_anchor,
Some(abs_path.clone()), Some(abs_path.clone()),
image, image,
window, window,
@ -301,17 +339,22 @@ impl MessageEditor {
match mention_uri { match mention_uri {
MentionUri::Fetch { url } => { MentionUri::Fetch { url } => {
self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx) self.confirm_mention_for_fetch(crease_id, start_anchor, url, window, cx)
} }
MentionUri::Directory { abs_path } => { MentionUri::Directory { abs_path } => {
self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx) self.confirm_mention_for_directory(crease_id, start_anchor, abs_path, window, cx)
} }
MentionUri::Thread { id, name } => { MentionUri::Thread { id, name } => {
self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx) self.confirm_mention_for_thread(crease_id, start_anchor, id, name, window, cx)
}
MentionUri::TextThread { path, name } => {
self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx)
} }
MentionUri::TextThread { path, name } => self.confirm_mention_for_text_thread(
crease_id,
start_anchor,
path,
name,
window,
cx,
),
MentionUri::File { .. } MentionUri::File { .. }
| MentionUri::Symbol { .. } | MentionUri::Symbol { .. }
| MentionUri::Rule { .. } | MentionUri::Rule { .. }
@ -391,30 +434,33 @@ impl MessageEditor {
let rope = buffer let rope = buffer
.read_with(cx, |buffer, _cx| buffer.as_rope().clone()) .read_with(cx, |buffer, _cx| buffer.as_rope().clone())
.log_err()?; .log_err()?;
Some(rope) Some((rope, buffer))
}); });
cx.background_spawn(async move { cx.background_spawn(async move {
let rope = rope_task.await?; let (rope, buffer) = rope_task.await?;
Some((rel_path, full_path, rope.to_string())) Some((rel_path, full_path, rope.to_string(), buffer))
}) })
})) }))
})?; })?;
let contents = cx let contents = cx
.background_spawn(async move { .background_spawn(async move {
let contents = descendants_future.await.into_iter().flatten(); let (contents, tracked_buffers) = descendants_future
contents.collect() .await
.into_iter()
.flatten()
.map(|(rel_path, full_path, rope, buffer)| {
((rel_path, full_path, rope), buffer)
})
.unzip();
(render_directory_contents(contents), tracked_buffers)
}) })
.await; .await;
anyhow::Ok(contents) anyhow::Ok(contents)
}); });
let task = cx let task = cx
.spawn(async move |_, _| { .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
task.await
.map(|contents| DirectoryContents(contents).to_string())
.map_err(|e| e.to_string())
})
.shared(); .shared();
self.mention_set self.mention_set
@ -663,7 +709,7 @@ impl MessageEditor {
&self, &self,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<Result<Vec<acp::ContentBlock>>> { ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
let contents = let contents =
self.mention_set self.mention_set
.contents(&self.project, self.prompt_store.as_ref(), window, cx); .contents(&self.project, self.prompt_store.as_ref(), window, cx);
@ -672,6 +718,7 @@ impl MessageEditor {
cx.spawn(async move |_, cx| { cx.spawn(async move |_, cx| {
let contents = contents.await?; let contents = contents.await?;
let mut all_tracked_buffers = Vec::new();
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let mut ix = 0; let mut ix = 0;
@ -702,7 +749,12 @@ impl MessageEditor {
chunks.push(chunk); chunks.push(chunk);
} }
let chunk = match mention { let chunk = match mention {
Mention::Text { uri, content } => { Mention::Text {
uri,
content,
tracked_buffers,
} => {
all_tracked_buffers.extend(tracked_buffers.iter().cloned());
acp::ContentBlock::Resource(acp::EmbeddedResource { acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None, annotations: None,
resource: acp::EmbeddedResourceResource::TextResourceContents( resource: acp::EmbeddedResourceResource::TextResourceContents(
@ -745,7 +797,7 @@ impl MessageEditor {
} }
}); });
chunks (chunks, all_tracked_buffers)
}) })
}) })
} }
@ -769,6 +821,10 @@ impl MessageEditor {
} }
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) { fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if !self.prompt_capabilities.get().image {
return;
}
let images = cx let images = cx
.read_from_clipboard() .read_from_clipboard()
.map(|item| { .map(|item| {
@ -1043,7 +1099,7 @@ impl MessageEditor {
.add_fetch_result(url, Task::ready(Ok(text)).shared()); .add_fetch_result(url, Task::ready(Ok(text)).shared());
} }
MentionUri::Directory { abs_path } => { MentionUri::Directory { abs_path } => {
let task = Task::ready(Ok(text)).shared(); let task = Task::ready(Ok((text, Vec::new()))).shared();
self.mention_set.directories.insert(abs_path, task); self.mention_set.directories.insert(abs_path, task);
} }
MentionUri::File { .. } MentionUri::File { .. }
@ -1153,16 +1209,13 @@ impl MessageEditor {
} }
} }
struct DirectoryContents(Arc<[(Arc<Path>, PathBuf, String)]>); fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
let mut output = String::new();
impl Display for DirectoryContents { for (_relative_path, full_path, content) in entries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let fence = codeblock_fence_for_path(Some(&full_path), None);
for (_relative_path, full_path, content) in self.0.iter() { write!(output, "\n{fence}\n{content}\n```").unwrap();
let fence = codeblock_fence_for_path(Some(full_path), None);
write!(f, "\n{fence}\n{content}\n```")?;
}
Ok(())
} }
output
} }
impl Focusable for MessageEditor { impl Focusable for MessageEditor {
@ -1328,7 +1381,11 @@ impl Render for ImageHover {
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub enum Mention { pub enum Mention {
Text { uri: MentionUri, content: String }, Text {
uri: MentionUri,
content: String,
tracked_buffers: Vec<Entity<Buffer>>,
},
Image(MentionImage), Image(MentionImage),
} }
@ -1346,7 +1403,7 @@ pub struct MentionSet {
images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>, images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
thread_summaries: HashMap<acp::SessionId, Shared<Task<Result<SharedString, String>>>>, thread_summaries: HashMap<acp::SessionId, Shared<Task<Result<SharedString, String>>>>,
text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>, directories: HashMap<PathBuf, Shared<Task<Result<(String, Vec<Entity<Buffer>>), String>>>>,
} }
impl MentionSet { impl MentionSet {
@ -1382,6 +1439,7 @@ impl MentionSet {
self.fetch_results.clear(); self.fetch_results.clear();
self.thread_summaries.clear(); self.thread_summaries.clear();
self.text_thread_summaries.clear(); self.text_thread_summaries.clear();
self.directories.clear();
self.uri_by_crease_id self.uri_by_crease_id
.drain() .drain()
.map(|(id, _)| id) .map(|(id, _)| id)
@ -1424,7 +1482,14 @@ impl MentionSet {
let buffer = buffer_task?.await?; let buffer = buffer_task?.await?;
let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
anyhow::Ok((crease_id, Mention::Text { uri, content })) anyhow::Ok((
crease_id,
Mention::Text {
uri,
content,
tracked_buffers: vec![buffer],
},
))
}) })
} }
MentionUri::Directory { abs_path } => { MentionUri::Directory { abs_path } => {
@ -1433,11 +1498,14 @@ impl MentionSet {
}; };
let uri = uri.clone(); let uri = uri.clone();
cx.spawn(async move |_| { cx.spawn(async move |_| {
let (content, tracked_buffers) =
content.await.map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(( Ok((
crease_id, crease_id,
Mention::Text { Mention::Text {
uri, uri,
content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, content,
tracked_buffers,
}, },
)) ))
}) })
@ -1473,7 +1541,14 @@ impl MentionSet {
.collect() .collect()
})?; })?;
anyhow::Ok((crease_id, Mention::Text { uri, content })) anyhow::Ok((
crease_id,
Mention::Text {
uri,
content,
tracked_buffers: vec![buffer],
},
))
}) })
} }
MentionUri::Thread { id, .. } => { MentionUri::Thread { id, .. } => {
@ -1490,6 +1565,7 @@ impl MentionSet {
.await .await
.map_err(|e| anyhow::anyhow!("{e}"))? .map_err(|e| anyhow::anyhow!("{e}"))?
.to_string(), .to_string(),
tracked_buffers: Vec::new(),
}, },
)) ))
}) })
@ -1505,6 +1581,7 @@ impl MentionSet {
Mention::Text { Mention::Text {
uri, uri,
content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
tracked_buffers: Vec::new(),
}, },
)) ))
}) })
@ -1518,7 +1595,14 @@ impl MentionSet {
cx.spawn(async move |_| { cx.spawn(async move |_| {
// TODO: report load errors instead of just logging // TODO: report load errors instead of just logging
let text = text_task.await?; let text = text_task.await?;
anyhow::Ok((crease_id, Mention::Text { uri, content: text })) anyhow::Ok((
crease_id,
Mention::Text {
uri,
content: text,
tracked_buffers: Vec::new(),
},
))
}) })
} }
MentionUri::Fetch { url } => { MentionUri::Fetch { url } => {
@ -1532,6 +1616,7 @@ impl MentionSet {
Mention::Text { Mention::Text {
uri, uri,
content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?, content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
tracked_buffers: Vec::new(),
}, },
)) ))
}) })
@ -1703,6 +1788,7 @@ impl Addon for MessageEditorAddon {
mod tests { mod tests {
use std::{ops::Range, path::Path, sync::Arc}; use std::{ops::Range, path::Path, sync::Arc};
use acp_thread::MentionUri;
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent2::HistoryStore; use agent2::HistoryStore;
use assistant_context::ContextStore; use assistant_context::ContextStore;
@ -1815,7 +1901,7 @@ mod tests {
editor.backspace(&Default::default(), window, cx); editor.backspace(&Default::default(), window, cx);
}); });
let content = message_editor let (content, _) = message_editor
.update_in(cx, |message_editor, window, cx| { .update_in(cx, |message_editor, window, cx| {
message_editor.contents(window, cx) message_editor.contents(window, cx)
}) })
@ -1970,6 +2056,34 @@ mod tests {
(message_editor, editor) (message_editor, editor)
}); });
cx.simulate_input("Lorem @");
editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "Lorem @");
assert!(editor.has_visible_completions_menu());
// Only files since we have default capabilities
assert_eq!(
current_completion_labels(editor),
&[
"eight.txt dir/b/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
]
);
editor.set_text("", window, cx);
});
message_editor.update(&mut cx, |editor, _cx| {
// Enable all prompt capabilities
editor.set_prompt_capabilities(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
});
});
cx.simulate_input("Lorem "); cx.simulate_input("Lorem ");
editor.update(&mut cx, |editor, cx| { editor.update(&mut cx, |editor, cx| {
@ -2046,13 +2160,13 @@ mod tests {
.into_values() .into_values()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
pretty_assertions::assert_eq!( {
contents, let [Mention::Text { content, uri, .. }] = contents.as_slice() else {
[Mention::Text { panic!("Unexpected mentions");
content: "1".into(), };
uri: url_one.parse().unwrap() pretty_assertions::assert_eq!(content, "1");
}] pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
); }
cx.simulate_input(" "); cx.simulate_input(" ");
@ -2098,15 +2212,15 @@ mod tests {
.into_values() .into_values()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(contents.len(), 2);
let url_eight = uri!("file:///dir/b/eight.txt"); let url_eight = uri!("file:///dir/b/eight.txt");
pretty_assertions::assert_eq!(
contents[1], {
Mention::Text { let [_, Mention::Text { content, uri, .. }] = contents.as_slice() else {
content: "8".to_string(), panic!("Unexpected mentions");
uri: url_eight.parse().unwrap(), };
} pretty_assertions::assert_eq!(content, "8");
); pretty_assertions::assert_eq!(uri, &url_eight.parse::<MentionUri>().unwrap());
}
editor.update(&mut cx, |editor, cx| { editor.update(&mut cx, |editor, cx| {
assert_eq!( assert_eq!(
@ -2208,14 +2322,18 @@ mod tests {
.into_values() .into_values()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!(contents.len(), 3); {
pretty_assertions::assert_eq!( let [_, _, Mention::Text { content, uri, .. }] = contents.as_slice() else {
contents[2], panic!("Unexpected mentions");
Mention::Text { };
content: "1".into(), pretty_assertions::assert_eq!(content, "1");
uri: format!("{url_one}?symbol=MySymbol#L1:1").parse().unwrap(), pretty_assertions::assert_eq!(
} uri,
); &format!("{url_one}?symbol=MySymbol#L1:1")
.parse::<MentionUri>()
.unwrap()
);
}
cx.run_until_parked(); cx.run_until_parked();

View file

@ -65,6 +65,12 @@ const RESPONSE_PADDING_X: Pixels = px(19.);
pub const MIN_EDITOR_LINES: usize = 4; pub const MIN_EDITOR_LINES: usize = 4;
pub const MAX_EDITOR_LINES: usize = 8; pub const MAX_EDITOR_LINES: usize = 8;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum ThreadFeedback {
Positive,
Negative,
}
enum ThreadError { enum ThreadError {
PaymentRequired, PaymentRequired,
ModelRequestLimitReached(cloud_llm_client::Plan), ModelRequestLimitReached(cloud_llm_client::Plan),
@ -106,6 +112,128 @@ impl ProfileProvider for Entity<agent2::Thread> {
} }
} }
#[derive(Default)]
struct ThreadFeedbackState {
feedback: Option<ThreadFeedback>,
comments_editor: Option<Entity<Editor>>,
}
impl ThreadFeedbackState {
pub fn submit(
&mut self,
thread: Entity<AcpThread>,
feedback: ThreadFeedback,
window: &mut Window,
cx: &mut App,
) {
let Some(telemetry) = thread.read(cx).connection().telemetry() else {
return;
};
if self.feedback == Some(feedback) {
return;
}
self.feedback = Some(feedback);
match feedback {
ThreadFeedback::Positive => {
self.comments_editor = None;
}
ThreadFeedback::Negative => {
self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));
}
}
let session_id = thread.read(cx).session_id().clone();
let agent_name = telemetry.agent_name();
let task = telemetry.thread_data(&session_id, cx);
let rating = match feedback {
ThreadFeedback::Positive => "positive",
ThreadFeedback::Negative => "negative",
};
cx.background_spawn(async move {
let thread = task.await?;
telemetry::event!(
"Agent Thread Rated",
session_id = session_id,
rating = rating,
agent = agent_name,
thread = thread
);
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {
let Some(telemetry) = thread.read(cx).connection().telemetry() else {
return;
};
let Some(comments) = self
.comments_editor
.as_ref()
.map(|editor| editor.read(cx).text(cx))
.filter(|text| !text.trim().is_empty())
else {
return;
};
self.comments_editor.take();
let session_id = thread.read(cx).session_id().clone();
let agent_name = telemetry.agent_name();
let task = telemetry.thread_data(&session_id, cx);
cx.background_spawn(async move {
let thread = task.await?;
telemetry::event!(
"Agent Thread Feedback Comments",
session_id = session_id,
comments = comments,
agent = agent_name,
thread = thread
);
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
pub fn clear(&mut self) {
*self = Self::default()
}
pub fn dismiss_comments(&mut self) {
self.comments_editor.take();
}
fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {
let buffer = cx.new(|cx| {
let empty_string = String::new();
MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
});
let editor = cx.new(|cx| {
let mut editor = Editor::new(
editor::EditorMode::AutoHeight {
min_lines: 1,
max_lines: Some(4),
},
buffer,
None,
window,
cx,
);
editor.set_placeholder_text(
"What went wrong? Share your feedback so we can improve.",
cx,
);
editor
});
editor.read(cx).focus_handle(cx).focus(window);
editor
}
}
pub struct AcpThreadView { pub struct AcpThreadView {
agent: Rc<dyn AgentServer>, agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
@ -120,6 +248,7 @@ pub struct AcpThreadView {
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>, notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
thread_retry_status: Option<RetryStatus>, thread_retry_status: Option<RetryStatus>,
thread_error: Option<ThreadError>, thread_error: Option<ThreadError>,
thread_feedback: ThreadFeedbackState,
list_state: ListState, list_state: ListState,
scrollbar_state: ScrollbarState, scrollbar_state: ScrollbarState,
auth_task: Option<Task<()>>, auth_task: Option<Task<()>>,
@ -218,6 +347,7 @@ impl AcpThreadView {
scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
thread_retry_status: None, thread_retry_status: None,
thread_error: None, thread_error: None,
thread_feedback: Default::default(),
auth_task: None, auth_task: None,
expanded_tool_calls: HashSet::default(), expanded_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(), expanded_thinking_blocks: HashSet::default(),
@ -362,6 +492,11 @@ impl AcpThreadView {
}) })
}); });
this.message_editor.update(cx, |message_editor, _cx| {
message_editor
.set_prompt_capabilities(connection.prompt_capabilities());
});
cx.notify(); cx.notify();
} }
Err(err) => { Err(err) => {
@ -609,18 +744,19 @@ impl AcpThreadView {
fn send_impl( fn send_impl(
&mut self, &mut self,
contents: Task<anyhow::Result<Vec<acp::ContentBlock>>>, contents: Task<anyhow::Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.thread_error.take(); self.thread_error.take();
self.editing_message.take(); self.editing_message.take();
self.thread_feedback.clear();
let Some(thread) = self.thread().cloned() else { let Some(thread) = self.thread().cloned() else {
return; return;
}; };
let task = cx.spawn_in(window, async move |this, cx| { let task = cx.spawn_in(window, async move |this, cx| {
let contents = contents.await?; let (contents, tracked_buffers) = contents.await?;
if contents.is_empty() { if contents.is_empty() {
return Ok(()); return Ok(());
@ -633,7 +769,14 @@ impl AcpThreadView {
message_editor.clear(window, cx); message_editor.clear(window, cx);
}); });
})?; })?;
let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?; let send = thread.update(cx, |thread, cx| {
thread.action_log().update(cx, |action_log, cx| {
for buffer in tracked_buffers {
action_log.buffer_read(buffer, cx)
}
});
thread.send(contents, cx)
})?;
send.await send.await
}); });
@ -793,7 +936,6 @@ impl AcpThreadView {
self.entry_view_state.update(cx, |view_state, cx| { self.entry_view_state.update(cx, |view_state, cx| {
view_state.sync_entry(*index, thread, window, cx) view_state.sync_entry(*index, thread, window, cx)
}); });
self.list_state.splice(*index..index + 1, 1);
} }
AcpThreadEvent::EntriesRemoved(range) => { AcpThreadEvent::EntriesRemoved(range) => {
self.entry_view_state self.entry_view_state
@ -1088,6 +1230,12 @@ impl AcpThreadView {
.w_full() .w_full()
.child(primary) .child(primary)
.child(self.render_thread_controls(cx)) .child(self.render_thread_controls(cx))
.when_some(
self.thread_feedback.comments_editor.clone(),
|this, editor| {
this.child(Self::render_feedback_feedback_editor(editor, window, cx))
},
)
.into_any_element() .into_any_element()
} else { } else {
primary primary
@ -3557,7 +3705,9 @@ impl AcpThreadView {
this.scroll_to_top(cx); this.scroll_to_top(cx);
})); }));
h_flex() let mut container = h_flex()
.id("thread-controls-container")
.group("thread-controls-container")
.w_full() .w_full()
.mr_1() .mr_1()
.pb_2() .pb_2()
@ -3565,9 +3715,145 @@ impl AcpThreadView {
.opacity(0.4) .opacity(0.4)
.hover(|style| style.opacity(1.)) .hover(|style| style.opacity(1.))
.flex_wrap() .flex_wrap()
.justify_end() .justify_end();
.child(open_as_markdown)
.child(scroll_to_top) if AgentSettings::get_global(cx).enable_feedback {
let feedback = self.thread_feedback.feedback;
container = container.child(
div().visible_on_hover("thread-controls-container").child(
Label::new(
match feedback {
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.",
None => "Rating the thread sends all of your current conversation to the Zed team.",
}
)
.color(Color::Muted)
.size(LabelSize::XSmall)
.truncate(),
),
).child(
h_flex()
.child(
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(match feedback {
Some(ThreadFeedback::Positive) => Color::Accent,
_ => Color::Ignored,
})
.tooltip(Tooltip::text("Helpful Response"))
.on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(
ThreadFeedback::Positive,
window,
cx,
);
})),
)
.child(
IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(match feedback {
Some(ThreadFeedback::Negative) => Color::Accent,
_ => Color::Ignored,
})
.tooltip(Tooltip::text("Not Helpful"))
.on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(
ThreadFeedback::Negative,
window,
cx,
);
})),
)
)
}
container.child(open_as_markdown).child(scroll_to_top)
}
fn render_feedback_feedback_editor(
editor: Entity<Editor>,
window: &mut Window,
cx: &Context<Self>,
) -> Div {
let focus_handle = editor.focus_handle(cx);
v_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
this.thread_feedback.dismiss_comments();
cx.notify();
}))
.on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| {
this.submit_feedback_message(cx);
}))
.mb_2()
.mx_4()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(editor)
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new("dismiss-feedback-message", "Cancel")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread_feedback.dismiss_comments();
cx.notify();
})),
)
.child(
Button::new("submit-feedback-message", "Share Feedback")
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(move |this, _, _window, cx| {
this.submit_feedback_message(cx);
})),
),
)
}
fn handle_feedback_click(
&mut self,
feedback: ThreadFeedback,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(thread) = self.thread().cloned() else {
return;
};
self.thread_feedback.submit(thread, feedback, window, cx);
cx.notify();
}
fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
let Some(thread) = self.thread().cloned() else {
return;
};
self.thread_feedback.submit_comments(thread, cx);
cx.notify();
} }
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> { fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
@ -4481,6 +4767,14 @@ pub(crate) mod tests {
&[] &[]
} }
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}
}
fn authenticate( fn authenticate(
&self, &self,
_method_id: acp::AuthMethodId, _method_id: acp::AuthMethodId,

View file

@ -2349,7 +2349,6 @@ impl ActiveThread {
this.submit_feedback_message(message_id, cx); this.submit_feedback_message(message_id, cx);
cx.notify(); cx.notify();
})) }))
.on_action(cx.listener(Self::confirm_editing_message))
.mb_2() .mb_2()
.mx_4() .mx_4()
.p_2() .p_2()

View file

@ -2024,8 +2024,8 @@ mod tests {
fn gen_working_copy(rng: &mut StdRng, head: &str) -> String { fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
let mut old_lines = { let mut old_lines = {
let mut old_lines = Vec::new(); let mut old_lines = Vec::new();
let mut old_lines_iter = head.lines(); let old_lines_iter = head.lines();
while let Some(line) = old_lines_iter.next() { for line in old_lines_iter {
assert!(!line.ends_with("\n")); assert!(!line.ends_with("\n"));
old_lines.push(line.to_owned()); old_lines.push(line.to_owned());
} }

View file

@ -3183,9 +3183,9 @@ mod tests {
// so we special case row 0 to assume a leading '\n'. // so we special case row 0 to assume a leading '\n'.
// //
// Linehood is the birthright of strings. // Linehood is the birthright of strings.
let mut input_text_lines = input_text.split('\n').enumerate().peekable(); let input_text_lines = input_text.split('\n').enumerate().peekable();
let mut block_row = 0; let mut block_row = 0;
while let Some((wrap_row, input_line)) = input_text_lines.next() { for (wrap_row, input_line) in input_text_lines {
let wrap_row = wrap_row as u32; let wrap_row = wrap_row as u32;
let multibuffer_row = wraps_snapshot let multibuffer_row = wraps_snapshot
.to_point(WrapPoint::new(wrap_row, 0), Bias::Left) .to_point(WrapPoint::new(wrap_row, 0), Bias::Left)

View file

@ -4876,11 +4876,7 @@ impl Editor {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> bool { ) -> bool {
let position = self.selections.newest_anchor().head(); let position = self.selections.newest_anchor().head();
let multibuffer = self.buffer.read(cx); let Some(buffer) = self.buffer.read(cx).buffer_for_anchor(position, cx) else {
let Some(buffer) = position
.buffer_id
.and_then(|buffer_id| multibuffer.buffer(buffer_id))
else {
return false; return false;
}; };
@ -5844,7 +5840,7 @@ impl Editor {
multibuffer_anchor.start.to_offset(&snapshot) multibuffer_anchor.start.to_offset(&snapshot)
..multibuffer_anchor.end.to_offset(&snapshot) ..multibuffer_anchor.end.to_offset(&snapshot)
}; };
if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { if snapshot.buffer_id_for_anchor(newest_anchor.head()) != Some(buffer.remote_id()) {
return None; return None;
} }
@ -11021,7 +11017,7 @@ impl Editor {
let mut col = 0; let mut col = 0;
let mut changed = false; let mut changed = false;
while let Some(ch) = chars.next() { for ch in chars.by_ref() {
match ch { match ch {
' ' => { ' ' => {
reindented_line.push(' '); reindented_line.push(' ');
@ -11077,7 +11073,7 @@ impl Editor {
let mut first_non_indent_char = None; let mut first_non_indent_char = None;
let mut changed = false; let mut changed = false;
while let Some(ch) = chars.next() { for ch in chars.by_ref() {
match ch { match ch {
' ' => { ' ' => {
// Keep track of spaces. Append \t when we reach tab_size // Keep track of spaces. Append \t when we reach tab_size

View file

@ -164,8 +164,8 @@ pub fn indent_guides_in_range(
let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset); let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset);
let mut fold_ranges = Vec::<Range<Point>>::new(); let mut fold_ranges = Vec::<Range<Point>>::new();
let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable(); let folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
while let Some(fold) = folds.next() { for fold in folds {
let start = fold.range.start.to_point(&snapshot.buffer_snapshot); let start = fold.range.start.to_point(&snapshot.buffer_snapshot);
let end = fold.range.end.to_point(&snapshot.buffer_snapshot); let end = fold.range.end.to_point(&snapshot.buffer_snapshot);
if let Some(last_range) = fold_ranges.last_mut() if let Some(last_range) = fold_ranges.last_mut()

View file

@ -103,9 +103,9 @@ impl FollowableItem for Editor {
multibuffer = MultiBuffer::new(project.read(cx).capability()); multibuffer = MultiBuffer::new(project.read(cx).capability());
let mut sorted_excerpts = state.excerpts.clone(); let mut sorted_excerpts = state.excerpts.clone();
sorted_excerpts.sort_by_key(|e| e.id); sorted_excerpts.sort_by_key(|e| e.id);
let mut sorted_excerpts = sorted_excerpts.into_iter().peekable(); let sorted_excerpts = sorted_excerpts.into_iter().peekable();
while let Some(excerpt) = sorted_excerpts.next() { for excerpt in sorted_excerpts {
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else { let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
continue; continue;
}; };

View file

@ -706,7 +706,7 @@ fn print_report(
println!("Average thread score: {average_thread_score}%"); println!("Average thread score: {average_thread_score}%");
} }
println!(""); println!();
print_h2("CUMULATIVE TOOL METRICS"); print_h2("CUMULATIVE TOOL METRICS");
println!("{}", cumulative_tool_metrics); println!("{}", cumulative_tool_metrics);

View file

@ -913,9 +913,9 @@ impl RequestMarkdown {
for tool in &request.tools { for tool in &request.tools {
write!(&mut tools, "# {}\n\n", tool.name).unwrap(); write!(&mut tools, "# {}\n\n", tool.name).unwrap();
write!(&mut tools, "{}\n\n", tool.description).unwrap(); write!(&mut tools, "{}\n\n", tool.description).unwrap();
write!( writeln!(
&mut tools, &mut tools,
"{}\n", "{}",
MarkdownCodeBlock { MarkdownCodeBlock {
tag: "json", tag: "json",
text: &format!("{:#}", tool.input_schema) text: &format!("{:#}", tool.input_schema)

View file

@ -916,7 +916,7 @@ impl GitRepository for RealGitRepository {
.context("no stdin for git cat-file subprocess")?; .context("no stdin for git cat-file subprocess")?;
let mut stdin = BufWriter::new(stdin); let mut stdin = BufWriter::new(stdin);
for rev in &revs { for rev in &revs {
write!(&mut stdin, "{rev}\n")?; writeln!(&mut stdin, "{rev}")?;
} }
stdin.flush()?; stdin.flush()?;
drop(stdin); drop(stdin);

View file

@ -164,7 +164,6 @@ impl TaffyLayoutEngine {
// for (a, b) in self.get_edges(id)? { // for (a, b) in self.get_edges(id)? {
// println!("N{} --> N{}", u64::from(a), u64::from(b)); // println!("N{} --> N{}", u64::from(a), u64::from(b));
// } // }
// println!("");
// //
if !self.computed_layouts.insert(id) { if !self.computed_layouts.insert(id) {

View file

@ -34,13 +34,6 @@ trait Transform: Clone {
/// Adds one to the value /// Adds one to the value
fn add_one(self) -> Self; fn add_one(self) -> Self;
/// cfg attributes are respected
#[cfg(all())]
fn cfg_included(self) -> Self;
#[cfg(any())]
fn cfg_omitted(self) -> Self;
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -70,10 +63,6 @@ impl Transform for Number {
fn add_one(self) -> Self { fn add_one(self) -> Self {
Number(self.0 + 1) Number(self.0 + 1)
} }
fn cfg_included(self) -> Self {
Number(self.0)
}
} }
#[test] #[test]
@ -83,14 +72,13 @@ fn test_derive_inspector_reflection() {
// Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self
let methods = methods::<Number>(); let methods = methods::<Number>();
assert_eq!(methods.len(), 6); assert_eq!(methods.len(), 5);
let method_names: Vec<_> = methods.iter().map(|m| m.name).collect(); let method_names: Vec<_> = methods.iter().map(|m| m.name).collect();
assert!(method_names.contains(&"double")); assert!(method_names.contains(&"double"));
assert!(method_names.contains(&"triple")); assert!(method_names.contains(&"triple"));
assert!(method_names.contains(&"increment")); assert!(method_names.contains(&"increment"));
assert!(method_names.contains(&"quadruple")); assert!(method_names.contains(&"quadruple"));
assert!(method_names.contains(&"add_one")); assert!(method_names.contains(&"add_one"));
assert!(method_names.contains(&"cfg_included"));
// Invoke methods by name // Invoke methods by name
let num = Number(5); let num = Number(5);

View file

@ -186,7 +186,7 @@ fn tokenize(text: &str, language_scope: Option<LanguageScope>) -> impl Iterator<
let mut prev = None; let mut prev = None;
let mut start_ix = 0; let mut start_ix = 0;
iter::from_fn(move || { iter::from_fn(move || {
while let Some((ix, c)) = chars.next() { for (ix, c) in chars.by_ref() {
let mut token = None; let mut token = None;
let kind = classifier.kind(c); let kind = classifier.kind(c);
if let Some((prev_char, prev_kind)) = prev if let Some((prev_char, prev_kind)) = prev

View file

@ -2196,6 +2196,15 @@ impl MultiBuffer {
}) })
} }
pub fn buffer_for_anchor(&self, anchor: Anchor, cx: &App) -> Option<Entity<Buffer>> {
if let Some(buffer_id) = anchor.buffer_id {
self.buffer(buffer_id)
} else {
let (_, buffer, _) = self.excerpt_containing(anchor, cx)?;
Some(buffer)
}
}
// If point is at the end of the buffer, the last excerpt is returned // If point is at the end of the buffer, the last excerpt is returned
pub fn point_to_buffer_offset<T: ToOffset>( pub fn point_to_buffer_offset<T: ToOffset>(
&self, &self,
@ -5228,15 +5237,6 @@ impl MultiBufferSnapshot {
excerpt_offset += ExcerptOffset::new(offset_in_transform); excerpt_offset += ExcerptOffset::new(offset_in_transform);
}; };
if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() {
return Anchor {
buffer_id: Some(buffer_id),
excerpt_id: *excerpt_id,
text_anchor: buffer.anchor_at(excerpt_offset.value, bias),
diff_base_anchor,
};
}
let mut excerpts = self let mut excerpts = self
.excerpts .excerpts
.cursor::<Dimensions<ExcerptOffset, Option<ExcerptId>>>(&()); .cursor::<Dimensions<ExcerptOffset, Option<ExcerptId>>>(&());
@ -5260,10 +5260,17 @@ impl MultiBufferSnapshot {
text_anchor, text_anchor,
diff_base_anchor, diff_base_anchor,
} }
} else if excerpt_offset.is_zero() && bias == Bias::Left {
Anchor::min()
} else { } else {
Anchor::max() let mut anchor = if excerpt_offset.is_zero() && bias == Bias::Left {
Anchor::min()
} else {
Anchor::max()
};
// TODO this is a hack, remove it
if let Some((excerpt_id, _, _)) = self.as_singleton() {
anchor.excerpt_id = *excerpt_id;
}
anchor
} }
} }
@ -6305,6 +6312,14 @@ impl MultiBufferSnapshot {
}) })
} }
pub fn buffer_id_for_anchor(&self, anchor: Anchor) -> Option<BufferId> {
if let Some(id) = anchor.buffer_id {
return Some(id);
}
let excerpt = self.excerpt_containing(anchor..anchor)?;
Some(excerpt.buffer_id())
}
pub fn selections_in_range<'a>( pub fn selections_in_range<'a>(
&'a self, &'a self,
range: &'a Range<Anchor>, range: &'a Range<Anchor>,
@ -6983,19 +6998,20 @@ impl Excerpt {
} }
fn contains(&self, anchor: &Anchor) -> bool { fn contains(&self, anchor: &Anchor) -> bool {
Some(self.buffer_id) == anchor.buffer_id anchor.buffer_id == None
&& self || anchor.buffer_id == Some(self.buffer_id)
.range && self
.context .range
.start .context
.cmp(&anchor.text_anchor, &self.buffer) .start
.is_le() .cmp(&anchor.text_anchor, &self.buffer)
&& self .is_le()
.range && self
.context .range
.end .context
.cmp(&anchor.text_anchor, &self.buffer) .end
.is_ge() .cmp(&anchor.text_anchor, &self.buffer)
.is_ge()
} }
/// The [`Excerpt`]'s start offset in its [`Buffer`] /// The [`Excerpt`]'s start offset in its [`Buffer`]

View file

@ -2250,11 +2250,11 @@ impl ReferenceMultibuffer {
let base_buffer = diff.base_text(); let base_buffer = diff.base_text();
let mut offset = buffer_range.start; let mut offset = buffer_range.start;
let mut hunks = diff let hunks = diff
.hunks_intersecting_range(excerpt.range.clone(), buffer, cx) .hunks_intersecting_range(excerpt.range.clone(), buffer, cx)
.peekable(); .peekable();
while let Some(hunk) = hunks.next() { for hunk in hunks {
// Ignore hunks that are outside the excerpt range. // Ignore hunks that are outside the excerpt range.
let mut hunk_range = hunk.buffer_range.to_offset(buffer); let mut hunk_range = hunk.buffer_range.to_offset(buffer);

View file

@ -42,8 +42,8 @@ impl<'a> GitTraversal<'a> {
// other_repo/ // other_repo/
// .git/ // .git/
// our_query.txt // our_query.txt
let mut query = path.ancestors(); let query = path.ancestors();
while let Some(query) = query.next() { for query in query {
let (_, snapshot) = self let (_, snapshot) = self
.repo_root_to_snapshot .repo_root_to_snapshot
.range(Path::new("")..=query) .range(Path::new("")..=query)

View file

@ -13149,10 +13149,10 @@ fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) {
let mut offset_map = vec![0; label.text.len() + 1]; let mut offset_map = vec![0; label.text.len() + 1];
let mut last_char_was_space = false; let mut last_char_was_space = false;
let mut new_idx = 0; let mut new_idx = 0;
let mut chars = label.text.char_indices().fuse(); let chars = label.text.char_indices().fuse();
let mut newlines_removed = false; let mut newlines_removed = false;
while let Some((idx, c)) = chars.next() { for (idx, c) in chars {
offset_map[idx] = new_idx; offset_map[idx] = new_idx;
match c { match c {

View file

@ -209,7 +209,7 @@ fn replace_value_in_json_text(
if ch == ',' { if ch == ',' {
removal_end = existing_value_range.end + offset + 1; removal_end = existing_value_range.end + offset + 1;
// Also consume whitespace after the comma // Also consume whitespace after the comma
while let Some((_, next_ch)) = chars.next() { for (_, next_ch) in chars.by_ref() {
if next_ch.is_whitespace() { if next_ch.is_whitespace() {
removal_end += next_ch.len_utf8(); removal_end += next_ch.len_utf8();
} else { } else {

View file

@ -307,7 +307,6 @@ impl TabSwitcherDelegate {
(Reverse(history.get(&item.item.item_id())), item.item_index) (Reverse(history.get(&item.item.item_id())), item.item_index)
) )
} }
eprintln!("");
all_items all_items
.sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index));
all_items all_items

View file

@ -1397,8 +1397,8 @@ fn possible_open_target(
let found_entry = worktree let found_entry = worktree
.update(cx, |worktree, _| { .update(cx, |worktree, _| {
let worktree_root = worktree.abs_path(); let worktree_root = worktree.abs_path();
let mut traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); let traversal = worktree.traverse_from_path(true, true, false, "".as_ref());
while let Some(entry) = traversal.next() { for entry in traversal {
if let Some(path_in_worktree) = worktree_paths_to_check if let Some(path_in_worktree) = worktree_paths_to_check
.iter() .iter()
.find(|path_to_check| entry.path.ends_with(&path_to_check.path)) .find(|path_to_check| entry.path.ends_with(&path_to_check.path))

View file

@ -1492,7 +1492,7 @@ impl OnMatchingLines {
let mut search = String::new(); let mut search = String::new();
let mut escaped = false; let mut escaped = false;
while let Some(c) = chars.next() { for c in chars.by_ref() {
if escaped { if escaped {
escaped = false; escaped = false;
// unescape escaped parens // unescape escaped parens

View file

@ -274,9 +274,9 @@ fn find_boolean(snapshot: &MultiBufferSnapshot, start: Point) -> Option<(Range<P
let mut end = None; let mut end = None;
let mut word = String::new(); let mut word = String::new();
let mut chars = snapshot.chars_at(offset); let chars = snapshot.chars_at(offset);
while let Some(ch) = chars.next() { for ch in chars {
if ch.is_ascii_alphabetic() { if ch.is_ascii_alphabetic() {
if begin.is_none() { if begin.is_none() {
begin = Some(offset); begin = Some(offset);