mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
agent_ui: Add @diagnostics mention to the thread (#42270)
Closes #31351 # Diagnostics Mention in New Threads ## Overview Adds the `@diagnostics` mention to the new Agent Panel threads so users can inject current LSP diagnostics (errors by default) without switching to a text thread. The diagnostics mention is fully integrated into ACP mention parsing, the context picker, and the message-editor pipeline so it round-trips cleanly and shows up in the standard `@` menu. ## Context - **Request:** bring `/diagnostics` parity to the “New Thread” assistant experience. - **Scope:** diagnostics only; `/terminal` mention would be implemented in a separate PR. - **Docs:** updated Agent Panel docs + changelog. ## Implementation Details 1. **Mention plumbing** - Added `MentionUri::Diagnostics` to `acp_thread`, including parsing (`zed:///agent/diagnostics?include_warnings=true`) and icon/name metadata. - Tests ensure diagnostics links round-trip via Markdown mention serialization. 2. **Context picker / completion** - New `ContextPickerMode::Diagnostics` exposes an `@diagnostics` entry in the mention menu. - Completions turn `@diagnostics` into a fully fledged mention, reusing the existing confirmation pipeline. 3. **Message editor + thread serialization** - Resolving the mention calls the existing diagnostics collector from `assistant_slash_commands`, embedding the tool output inline with other context blocks (`<diagnostics>…</diagnostics>`). - Thread-link handling ignores diagnostics backlinks so clicking them doesn’t try to reopen nonexistent resources. # How it looks <img width="800" height="480" alt="image" src="https://cf5gpe8lxo.ufs.sh/f/EmJ5Xl877qJO1mzC9Zrn8AmJZHeShC4RoUwvTMlF2tfPzj06" /> Release Notes: - Allow mentioning diagnostics in the agent panel via `@diagnostics` --------- Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
This commit is contained in:
parent
58051d6e84
commit
d71fe4cc7f
8 changed files with 401 additions and 22 deletions
|
|
@ -40,6 +40,12 @@ pub enum MentionUri {
|
|||
id: PromptId,
|
||||
name: String,
|
||||
},
|
||||
Diagnostics {
|
||||
#[serde(default = "default_include_errors")]
|
||||
include_errors: bool,
|
||||
#[serde(default)]
|
||||
include_warnings: bool,
|
||||
},
|
||||
Selection {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
abs_path: Option<PathBuf>,
|
||||
|
|
@ -135,6 +141,20 @@ impl MentionUri {
|
|||
id: rule_id.into(),
|
||||
name,
|
||||
})
|
||||
} else if path == "/agent/diagnostics" {
|
||||
let mut include_errors = default_include_errors();
|
||||
let mut include_warnings = false;
|
||||
for (key, value) in url.query_pairs() {
|
||||
match key.as_ref() {
|
||||
"include_warnings" => include_warnings = value == "true",
|
||||
"include_errors" => include_errors = value == "true",
|
||||
_ => bail!("invalid query parameter"),
|
||||
}
|
||||
}
|
||||
Ok(Self::Diagnostics {
|
||||
include_errors,
|
||||
include_warnings,
|
||||
})
|
||||
} else if path.starts_with("/agent/pasted-image") {
|
||||
Ok(Self::PastedImage)
|
||||
} else if path.starts_with("/agent/untitled-buffer") {
|
||||
|
|
@ -200,6 +220,7 @@ impl MentionUri {
|
|||
MentionUri::Thread { name, .. } => name.clone(),
|
||||
MentionUri::TextThread { name, .. } => name.clone(),
|
||||
MentionUri::Rule { name, .. } => name.clone(),
|
||||
MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
line_range,
|
||||
|
|
@ -221,6 +242,7 @@ impl MentionUri {
|
|||
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
||||
MentionUri::TextThread { .. } => IconName::Thread.path().into(),
|
||||
MentionUri::Rule { .. } => IconName::Reader.path().into(),
|
||||
MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
|
||||
MentionUri::Selection { .. } => IconName::Reader.path().into(),
|
||||
MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(),
|
||||
}
|
||||
|
|
@ -299,6 +321,21 @@ impl MentionUri {
|
|||
url.query_pairs_mut().append_pair("name", name);
|
||||
url
|
||||
}
|
||||
MentionUri::Diagnostics {
|
||||
include_errors,
|
||||
include_warnings,
|
||||
} => {
|
||||
let mut url = Url::parse("zed:///").unwrap();
|
||||
url.set_path("/agent/diagnostics");
|
||||
if *include_warnings {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("include_warnings", "true");
|
||||
}
|
||||
if !include_errors {
|
||||
url.query_pairs_mut().append_pair("include_errors", "false");
|
||||
}
|
||||
url
|
||||
}
|
||||
MentionUri::Fetch { url } => url.clone(),
|
||||
}
|
||||
}
|
||||
|
|
@ -312,6 +349,10 @@ impl fmt::Display for MentionLink<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
fn default_include_errors() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
|
||||
let pairs = url.query_pairs().collect::<Vec<_>>();
|
||||
match pairs.as_slice() {
|
||||
|
|
@ -504,6 +545,40 @@ mod tests {
|
|||
assert_eq!(parsed.to_uri().to_string(), https_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_diagnostics_uri() {
|
||||
let uri = "zed:///agent/diagnostics?include_warnings=true";
|
||||
let parsed = MentionUri::parse(uri, PathStyle::local()).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Diagnostics {
|
||||
include_errors,
|
||||
include_warnings,
|
||||
} => {
|
||||
assert!(include_errors);
|
||||
assert!(include_warnings);
|
||||
}
|
||||
_ => panic!("Expected Diagnostics variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri().to_string(), uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_diagnostics_uri_warnings_only() {
|
||||
let uri = "zed:///agent/diagnostics?include_warnings=true&include_errors=false";
|
||||
let parsed = MentionUri::parse(uri, PathStyle::local()).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Diagnostics {
|
||||
include_errors,
|
||||
include_warnings,
|
||||
} => {
|
||||
assert!(!include_errors);
|
||||
assert!(include_warnings);
|
||||
}
|
||||
_ => panic!("Expected Diagnostics variant"),
|
||||
}
|
||||
assert_eq!(parsed.to_uri().to_string(), uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_scheme() {
|
||||
assert!(MentionUri::parse("ftp://example.com", PathStyle::local()).is_err());
|
||||
|
|
|
|||
|
|
@ -223,6 +223,7 @@ impl UserMessage {
|
|||
const OPEN_FETCH_TAG: &str = "<fetched_urls>";
|
||||
const OPEN_RULES_TAG: &str =
|
||||
"<rules>\nThe user has specified the following rules that should be applied:\n";
|
||||
const OPEN_DIAGNOSTICS_TAG: &str = "<diagnostics>";
|
||||
|
||||
let mut file_context = OPEN_FILES_TAG.to_string();
|
||||
let mut directory_context = OPEN_DIRECTORIES_TAG.to_string();
|
||||
|
|
@ -231,6 +232,7 @@ impl UserMessage {
|
|||
let mut thread_context = OPEN_THREADS_TAG.to_string();
|
||||
let mut fetch_context = OPEN_FETCH_TAG.to_string();
|
||||
let mut rules_context = OPEN_RULES_TAG.to_string();
|
||||
let mut diagnostics_context = OPEN_DIAGNOSTICS_TAG.to_string();
|
||||
|
||||
for chunk in &self.content {
|
||||
let chunk = match chunk {
|
||||
|
|
@ -312,6 +314,9 @@ impl UserMessage {
|
|||
MentionUri::Fetch { url } => {
|
||||
write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
|
||||
}
|
||||
MentionUri::Diagnostics { .. } => {
|
||||
write!(&mut diagnostics_context, "\n{}\n", content).ok();
|
||||
}
|
||||
}
|
||||
|
||||
language_model::MessageContent::Text(uri.as_link().to_string())
|
||||
|
|
@ -372,6 +377,13 @@ impl UserMessage {
|
|||
.push(language_model::MessageContent::Text(rules_context));
|
||||
}
|
||||
|
||||
if diagnostics_context.len() > OPEN_DIAGNOSTICS_TAG.len() {
|
||||
diagnostics_context.push_str("</diagnostics>\n");
|
||||
message
|
||||
.content
|
||||
.push(language_model::MessageContent::Text(diagnostics_context));
|
||||
}
|
||||
|
||||
if message.content.len() > len_before_context {
|
||||
message.content.insert(
|
||||
len_before_context,
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@ impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
|
|||
if self.read(cx).thread_store.is_some() {
|
||||
supported.push(PromptContextType::Thread);
|
||||
}
|
||||
supported.extend(&[PromptContextType::Fetch, PromptContextType::Rules]);
|
||||
supported.extend(&[
|
||||
PromptContextType::Diagnostics,
|
||||
PromptContextType::Fetch,
|
||||
PromptContextType::Rules,
|
||||
]);
|
||||
}
|
||||
supported
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6574,6 +6574,7 @@ impl AcpThreadView {
|
|||
MentionUri::Fetch { url } => {
|
||||
cx.open_url(url.as_str());
|
||||
}
|
||||
MentionUri::Diagnostics { .. } => {}
|
||||
})
|
||||
} else {
|
||||
cx.open_url(&url);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use lsp::CompletionContext;
|
|||
use ordered_float::OrderedFloat;
|
||||
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse,
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, DiagnosticSummary,
|
||||
PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
|
||||
};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
|
|
@ -55,6 +55,7 @@ pub(crate) enum PromptContextType {
|
|||
Fetch,
|
||||
Thread,
|
||||
Rules,
|
||||
Diagnostics,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -92,6 +93,7 @@ impl TryFrom<&str> for PromptContextType {
|
|||
"fetch" => Ok(Self::Fetch),
|
||||
"thread" => Ok(Self::Thread),
|
||||
"rule" => Ok(Self::Rules),
|
||||
"diagnostics" => Ok(Self::Diagnostics),
|
||||
_ => Err(format!("Invalid context picker mode: {}", value)),
|
||||
}
|
||||
}
|
||||
|
|
@ -105,6 +107,7 @@ impl PromptContextType {
|
|||
Self::Fetch => "fetch",
|
||||
Self::Thread => "thread",
|
||||
Self::Rules => "rule",
|
||||
Self::Diagnostics => "diagnostics",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +118,7 @@ impl PromptContextType {
|
|||
Self::Fetch => "Fetch",
|
||||
Self::Thread => "Threads",
|
||||
Self::Rules => "Rules",
|
||||
Self::Diagnostics => "Diagnostics",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,6 +129,7 @@ impl PromptContextType {
|
|||
Self::Fetch => IconName::ToolWeb,
|
||||
Self::Thread => IconName::Thread,
|
||||
Self::Rules => IconName::Reader,
|
||||
Self::Diagnostics => IconName::Warning,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -583,6 +588,103 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
|
|||
})
|
||||
}
|
||||
|
||||
fn completion_for_diagnostics(
|
||||
source_range: Range<Anchor>,
|
||||
source: Arc<T>,
|
||||
editor: WeakEntity<Editor>,
|
||||
mention_set: WeakEntity<MentionSet>,
|
||||
workspace: Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Vec<Completion> {
|
||||
let summary = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.diagnostic_summary(false, cx);
|
||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let icon_path = MentionUri::Diagnostics {
|
||||
include_errors: true,
|
||||
include_warnings: false,
|
||||
}
|
||||
.icon_path(cx);
|
||||
|
||||
let mut completions = Vec::new();
|
||||
|
||||
let cases = [
|
||||
(summary.error_count > 0, true, false),
|
||||
(summary.warning_count > 0, false, true),
|
||||
(
|
||||
summary.error_count > 0 && summary.warning_count > 0,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
for (condition, include_errors, include_warnings) in cases {
|
||||
if condition {
|
||||
completions.push(Self::build_diagnostics_completion(
|
||||
diagnostics_submenu_label(summary, include_errors, include_warnings),
|
||||
source_range.clone(),
|
||||
source.clone(),
|
||||
editor.clone(),
|
||||
mention_set.clone(),
|
||||
workspace.clone(),
|
||||
icon_path.clone(),
|
||||
include_errors,
|
||||
include_warnings,
|
||||
summary,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
completions
|
||||
}
|
||||
|
||||
fn build_diagnostics_completion(
|
||||
menu_label: String,
|
||||
source_range: Range<Anchor>,
|
||||
source: Arc<T>,
|
||||
editor: WeakEntity<Editor>,
|
||||
mention_set: WeakEntity<MentionSet>,
|
||||
workspace: Entity<Workspace>,
|
||||
icon_path: SharedString,
|
||||
include_errors: bool,
|
||||
include_warnings: bool,
|
||||
summary: DiagnosticSummary,
|
||||
) -> Completion {
|
||||
let uri = MentionUri::Diagnostics {
|
||||
include_errors,
|
||||
include_warnings,
|
||||
};
|
||||
let crease_text = diagnostics_crease_label(summary, include_errors, include_warnings);
|
||||
let display_text = format!("@{}", crease_text);
|
||||
let new_text = format!("[{}]({}) ", display_text, uri.to_uri());
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label: CodeLabel::plain(menu_label, None),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(icon_path),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm: Some(confirm_completion_callback(
|
||||
crease_text,
|
||||
source_range.start,
|
||||
new_text_len - 1,
|
||||
uri,
|
||||
source,
|
||||
editor,
|
||||
mention_set,
|
||||
workspace,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn search_slash_commands(&self, query: String, cx: &mut App) -> Task<Vec<AvailableCommand>> {
|
||||
let commands = self.source.available_commands(cx);
|
||||
if commands.is_empty() {
|
||||
|
|
@ -684,6 +786,8 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
|
|||
}
|
||||
}
|
||||
|
||||
Some(PromptContextType::Diagnostics) => Task::ready(Vec::new()),
|
||||
|
||||
None if query.is_empty() => {
|
||||
let recent_task = self.recent_context_picker_entries(&workspace, cx);
|
||||
let entries = self
|
||||
|
|
@ -879,6 +983,20 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
|
|||
entries.push(PromptContextEntry::Mode(PromptContextType::Fetch));
|
||||
}
|
||||
|
||||
if self
|
||||
.source
|
||||
.supports_context(PromptContextType::Diagnostics, cx)
|
||||
{
|
||||
let summary = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.diagnostic_summary(false, cx);
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
entries.push(PromptContextEntry::Mode(PromptContextType::Diagnostics));
|
||||
}
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
|
@ -982,6 +1100,28 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
|||
})
|
||||
}
|
||||
PromptCompletion::Mention(MentionCompletion { mode, argument, .. }) => {
|
||||
if let Some(PromptContextType::Diagnostics) = mode {
|
||||
if argument.is_some() {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let completions = Self::completion_for_diagnostics(
|
||||
source_range.clone(),
|
||||
source.clone(),
|
||||
editor.clone(),
|
||||
mention_set.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
);
|
||||
if !completions.is_empty() {
|
||||
return Task::ready(Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]));
|
||||
}
|
||||
}
|
||||
|
||||
let query = argument.unwrap_or_default();
|
||||
let search_task =
|
||||
self.search_mentions(mode, query, Arc::<AtomicBool>::default(), cx);
|
||||
|
|
@ -1051,7 +1191,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
|||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
Match::Symbol(SymbolMatch { symbol, .. }) => {
|
||||
Self::completion_for_symbol(
|
||||
symbol,
|
||||
|
|
@ -1064,7 +1203,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
|||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
Match::Thread(thread) => Some(Self::completion_for_thread(
|
||||
thread,
|
||||
source_range.clone(),
|
||||
|
|
@ -1075,7 +1213,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
|||
workspace.clone(),
|
||||
cx,
|
||||
)),
|
||||
|
||||
Match::RecentThread(thread) => Some(Self::completion_for_thread(
|
||||
thread,
|
||||
source_range.clone(),
|
||||
|
|
@ -1086,7 +1223,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
|||
workspace.clone(),
|
||||
cx,
|
||||
)),
|
||||
|
||||
Match::Rules(user_rules) => Some(Self::completion_for_rules(
|
||||
user_rules,
|
||||
source_range.clone(),
|
||||
|
|
@ -1096,7 +1232,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
|||
workspace.clone(),
|
||||
cx,
|
||||
)),
|
||||
|
||||
Match::Fetch(url) => Self::completion_for_fetch(
|
||||
source_range.clone(),
|
||||
url,
|
||||
|
|
@ -1106,7 +1241,6 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
|
|||
workspace.clone(),
|
||||
cx,
|
||||
),
|
||||
|
||||
Match::Entry(EntryMatch { entry, .. }) => {
|
||||
Self::completion_for_entry(
|
||||
entry,
|
||||
|
|
@ -1380,6 +1514,87 @@ impl MentionCompletion {
|
|||
}
|
||||
}
|
||||
|
||||
fn diagnostics_label(
|
||||
summary: DiagnosticSummary,
|
||||
include_errors: bool,
|
||||
include_warnings: bool,
|
||||
) -> String {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
if include_errors && summary.error_count > 0 {
|
||||
parts.push(format!(
|
||||
"{} {}",
|
||||
summary.error_count,
|
||||
pluralize("error", summary.error_count)
|
||||
));
|
||||
}
|
||||
|
||||
if include_warnings && summary.warning_count > 0 {
|
||||
parts.push(format!(
|
||||
"{} {}",
|
||||
summary.warning_count,
|
||||
pluralize("warning", summary.warning_count)
|
||||
));
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
return "Diagnostics".into();
|
||||
}
|
||||
|
||||
let body = if parts.len() == 2 {
|
||||
format!("{} and {}", parts[0], parts[1])
|
||||
} else {
|
||||
parts
|
||||
.pop()
|
||||
.expect("at least one part present after non-empty check")
|
||||
};
|
||||
|
||||
format!("Diagnostics: {body}")
|
||||
}
|
||||
|
||||
fn diagnostics_submenu_label(
|
||||
summary: DiagnosticSummary,
|
||||
include_errors: bool,
|
||||
include_warnings: bool,
|
||||
) -> String {
|
||||
match (include_errors, include_warnings) {
|
||||
(true, true) => format!(
|
||||
"{} {} & {} {}",
|
||||
summary.error_count,
|
||||
pluralize("error", summary.error_count),
|
||||
summary.warning_count,
|
||||
pluralize("warning", summary.warning_count)
|
||||
),
|
||||
(true, _) => format!(
|
||||
"{} {}",
|
||||
summary.error_count,
|
||||
pluralize("error", summary.error_count)
|
||||
),
|
||||
(_, true) => format!(
|
||||
"{} {}",
|
||||
summary.warning_count,
|
||||
pluralize("warning", summary.warning_count)
|
||||
),
|
||||
_ => "Diagnostics".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostics_crease_label(
|
||||
summary: DiagnosticSummary,
|
||||
include_errors: bool,
|
||||
include_warnings: bool,
|
||||
) -> SharedString {
|
||||
diagnostics_label(summary, include_errors, include_warnings).into()
|
||||
}
|
||||
|
||||
fn pluralize(noun: &str, count: usize) -> String {
|
||||
if count == 1 {
|
||||
noun.to_string()
|
||||
} else {
|
||||
format!("{noun}s")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn search_files(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
|
|
@ -1833,6 +2048,11 @@ mod tests {
|
|||
#[test]
|
||||
fn test_mention_completion_parse() {
|
||||
let supported_modes = vec![PromptContextType::File, PromptContextType::Symbol];
|
||||
let supported_modes_with_diagnostics = vec![
|
||||
PromptContextType::File,
|
||||
PromptContextType::Symbol,
|
||||
PromptContextType::Diagnostics,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse("Lorem Ipsum", 0, &supported_modes),
|
||||
|
|
@ -1945,6 +2165,19 @@ mod tests {
|
|||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse(
|
||||
"Lorem @diagnostics",
|
||||
0,
|
||||
&supported_modes_with_diagnostics
|
||||
),
|
||||
Some(MentionCompletion {
|
||||
source_range: 6..18,
|
||||
mode: Some(PromptContextType::Diagnostics),
|
||||
argument: None,
|
||||
})
|
||||
);
|
||||
|
||||
// Disallowed non-file mentions
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse("Lorem @symbol main", 0, &[PromptContextType::File]),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use agent::{ThreadStore, outline};
|
|||
use agent_client_protocol as acp;
|
||||
use agent_servers::{AgentServer, AgentServerDelegate};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_commands::codeblock_fence_for_path;
|
||||
use assistant_slash_commands::{codeblock_fence_for_path, collect_diagnostics_output};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorSnapshot, ExcerptId, FoldPlaceholder, ToOffset,
|
||||
|
|
@ -235,6 +235,10 @@ impl MentionSet {
|
|||
..
|
||||
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
||||
MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
|
||||
MentionUri::Diagnostics {
|
||||
include_errors,
|
||||
include_warnings,
|
||||
} => self.confirm_mention_for_diagnostics(include_errors, include_warnings, cx),
|
||||
MentionUri::PastedImage => {
|
||||
debug_panic!("pasted image URI should not be included in completions");
|
||||
Task::ready(Err(anyhow!(
|
||||
|
|
@ -510,6 +514,37 @@ impl MentionSet {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm_mention_for_diagnostics(
|
||||
&self,
|
||||
include_errors: bool,
|
||||
include_warnings: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Mention>> {
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("project not found")));
|
||||
};
|
||||
|
||||
let diagnostics_task = collect_diagnostics_output(
|
||||
project,
|
||||
assistant_slash_commands::Options {
|
||||
include_errors,
|
||||
include_warnings,
|
||||
path_matcher: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |_, _| {
|
||||
let output = diagnostics_task.await?;
|
||||
let content = output
|
||||
.map(|output| output.text)
|
||||
.unwrap_or_else(|| "No diagnostics found.".into());
|
||||
Ok(Mention::Text {
|
||||
content,
|
||||
tracked_buffers: Vec::new(),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||
let path_style = project.read(cx).path_style(cx);
|
||||
let options = Options::parse(arguments, path_style);
|
||||
|
||||
let task = collect_diagnostics(project.clone(), options, cx);
|
||||
let task = collect_diagnostics_output(project.clone(), options, cx);
|
||||
|
||||
window.spawn(cx, async move |_| {
|
||||
task.await?
|
||||
|
|
@ -198,10 +198,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Options {
|
||||
include_warnings: bool,
|
||||
path_matcher: Option<PathMatcher>,
|
||||
pub struct Options {
|
||||
pub include_errors: bool,
|
||||
pub include_warnings: bool,
|
||||
pub path_matcher: Option<PathMatcher>,
|
||||
}
|
||||
|
||||
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
|
||||
|
|
@ -218,6 +218,7 @@ impl Options {
|
|||
}
|
||||
}
|
||||
Self {
|
||||
include_errors: true,
|
||||
include_warnings,
|
||||
path_matcher,
|
||||
}
|
||||
|
|
@ -228,7 +229,7 @@ impl Options {
|
|||
}
|
||||
}
|
||||
|
||||
fn collect_diagnostics(
|
||||
pub fn collect_diagnostics_output(
|
||||
project: Entity<Project>,
|
||||
options: Options,
|
||||
cx: &mut App,
|
||||
|
|
@ -282,11 +283,17 @@ fn collect_diagnostics(
|
|||
continue;
|
||||
}
|
||||
|
||||
project_summary.error_count += summary.error_count;
|
||||
let has_errors = options.include_errors && summary.error_count > 0;
|
||||
let has_warnings = options.include_warnings && summary.warning_count > 0;
|
||||
if !has_errors && !has_warnings {
|
||||
continue;
|
||||
}
|
||||
|
||||
if options.include_errors {
|
||||
project_summary.error_count += summary.error_count;
|
||||
}
|
||||
if options.include_warnings {
|
||||
project_summary.warning_count += summary.warning_count;
|
||||
} else if summary.error_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let last_end = output.text.len();
|
||||
|
|
@ -301,7 +308,12 @@ fn collect_diagnostics(
|
|||
.log_err()
|
||||
{
|
||||
let snapshot = cx.read_entity(&buffer, |buffer, _| buffer.snapshot());
|
||||
collect_buffer_diagnostics(&mut output, &snapshot, options.include_warnings);
|
||||
collect_buffer_diagnostics(
|
||||
&mut output,
|
||||
&snapshot,
|
||||
options.include_warnings,
|
||||
options.include_errors,
|
||||
);
|
||||
}
|
||||
|
||||
if !glob_is_exact_file_match {
|
||||
|
|
@ -358,10 +370,11 @@ pub fn collect_buffer_diagnostics(
|
|||
output: &mut SlashCommandOutput,
|
||||
snapshot: &BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
include_errors: bool,
|
||||
) {
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
collect_diagnostic(output, entry, snapshot, include_warnings)
|
||||
collect_diagnostic(output, entry, snapshot, include_warnings, include_errors)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -370,6 +383,7 @@ fn collect_diagnostic(
|
|||
entry: &DiagnosticEntryRef<'_, Anchor>,
|
||||
snapshot: &BufferSnapshot,
|
||||
include_warnings: bool,
|
||||
include_errors: bool,
|
||||
) {
|
||||
const EXCERPT_EXPANSION_SIZE: u32 = 2;
|
||||
const MAX_MESSAGE_LENGTH: usize = 2000;
|
||||
|
|
@ -381,7 +395,12 @@ fn collect_diagnostic(
|
|||
}
|
||||
("warning", IconName::Warning)
|
||||
}
|
||||
DiagnosticSeverity::ERROR => ("error", IconName::XCircle),
|
||||
DiagnosticSeverity::ERROR => {
|
||||
if !include_errors {
|
||||
return;
|
||||
}
|
||||
("error", IconName::XCircle)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
let prev_len = output.text.len();
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ You can turn this off, though, through the `agent.single_file_review` setting.
|
|||
Although Zed's agent is very efficient at reading through your code base to autonomously pick up relevant context, manually adding whatever would be useful to fulfill your prompt is still encouraged as a way to not only improve the AI's response quality but also to speed up its response time.
|
||||
|
||||
In Zed's Agent Panel, all pieces of context are added as mentions in the panel's message editor.
|
||||
You can type `@` to mention files, directories, symbols, previous threads, and rules files.
|
||||
You can type `@` to mention files, directories, symbols, previous threads, rules files, and diagnostics.
|
||||
|
||||
Copying images and pasting them in the panel's message editor is also supported.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue