This PR tackles how we communicate errors/warnings relative to skills. It touches three major surfaces: callouts in the thread view, menu items in the completion menu, and settings UI. These are all the places where we display something to tell the user a skill might have a problem. In the settings UI, we now clamp the skill description to five lines. And if there's a warning, we show the warning icon and the description in a tooltip when hovering it: <img width="600" alt="Screenshot 2026-06-08 at 10 51@2x" src="https://github.com/user-attachments/assets/1a1233b0-c24a-4082-9fe6-2ab1d00ba9fd" /> In the thread view, we display callouts with the warnings. I simplified the long-description one specifically given we can click on each skill item right there, so no need for a "manage global skills" button or descriptions in each item: <img width="600" alt="Screenshot 2026-06-08 at 10 52@2x" src="https://github.com/user-attachments/assets/4b9aad23-7362-40cd-8ab5-6dfceb46badd" /> In the code completion menu, I removed the icon that appeared at the far right and instead added the ability for it to render a colored warning icon instead of the icon it'd already display in the left; I think this simplifies it nicely: <img width="350" alt="Screenshot 2026-06-08 at 10 53@2x" src="https://github.com/user-attachments/assets/8d0c731a-31a0-4d30-abab-1eb40f61031e" /> Release Notes: - Agent: Improved how we communicate problems relative to skills, in both the agent panel and the settings UI. --------- Co-authored-by: Martin Ye <martin@zed.dev>
138 lines
4.5 KiB
Rust
138 lines
4.5 KiB
Rust
use collections::HashMap;
|
|
use command_palette;
|
|
use editor::{CompletionProvider, Editor};
|
|
use fuzzy::StringMatchCandidate;
|
|
use gpui::{Context, Entity, SharedString, Window};
|
|
use language::{self, ToOffset};
|
|
use project::{self, CompletionDisplayOptions};
|
|
|
|
pub struct ActionCompletionProvider {
|
|
action_names: Vec<&'static str>,
|
|
humanized_names: HashMap<&'static str, SharedString>,
|
|
}
|
|
|
|
impl ActionCompletionProvider {
|
|
pub fn new(
|
|
action_names: Vec<&'static str>,
|
|
humanized_names: HashMap<&'static str, SharedString>,
|
|
) -> Self {
|
|
Self {
|
|
action_names,
|
|
humanized_names,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CompletionProvider for ActionCompletionProvider {
|
|
fn completions(
|
|
&self,
|
|
buffer: &Entity<language::Buffer>,
|
|
buffer_position: language::Anchor,
|
|
_trigger: editor::CompletionContext,
|
|
_window: &mut Window,
|
|
cx: &mut Context<Editor>,
|
|
) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
|
|
let buffer = buffer.read(cx);
|
|
let mut count_back = 0;
|
|
|
|
for char in buffer.reversed_chars_at(buffer_position) {
|
|
if char.is_ascii_alphanumeric() || char == '_' || char == ':' {
|
|
count_back += 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let start_anchor = buffer.anchor_before(
|
|
buffer_position
|
|
.to_offset(&buffer)
|
|
.saturating_sub(count_back),
|
|
);
|
|
|
|
let replace_range = start_anchor..buffer_position;
|
|
let snapshot = buffer.text_snapshot();
|
|
let query: String = snapshot.text_for_range(replace_range.clone()).collect();
|
|
let normalized_query = command_palette::normalize_action_query(&query);
|
|
|
|
let candidates: Vec<StringMatchCandidate> = self
|
|
.action_names
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(ix, &name)| {
|
|
let humanized = self
|
|
.humanized_names
|
|
.get(name)
|
|
.cloned()
|
|
.unwrap_or_else(|| name.into());
|
|
StringMatchCandidate::new(ix, &humanized)
|
|
})
|
|
.collect();
|
|
|
|
let executor = cx.background_executor().clone();
|
|
let executor_for_fuzzy = executor.clone();
|
|
let action_names = self.action_names.clone();
|
|
let humanized_names = self.humanized_names.clone();
|
|
|
|
executor.spawn(async move {
|
|
let matches = fuzzy::match_strings(
|
|
&candidates,
|
|
&normalized_query,
|
|
true,
|
|
true,
|
|
action_names.len(),
|
|
&Default::default(),
|
|
executor_for_fuzzy,
|
|
)
|
|
.await;
|
|
|
|
let completions: Vec<project::Completion> = matches
|
|
.iter()
|
|
.take(50)
|
|
.map(|m| {
|
|
let action_name = action_names[m.candidate_id];
|
|
let humanized = humanized_names
|
|
.get(action_name)
|
|
.cloned()
|
|
.unwrap_or_else(|| action_name.into());
|
|
|
|
project::Completion {
|
|
replace_range: replace_range.clone(),
|
|
label: language::CodeLabel::plain(humanized.to_string(), None),
|
|
new_text: action_name.to_string(),
|
|
documentation: None,
|
|
source: project::CompletionSource::Custom,
|
|
icon_path: None,
|
|
icon_color: None,
|
|
match_start: None,
|
|
snippet_deduplication_key: None,
|
|
insert_text_mode: None,
|
|
confirm: None,
|
|
group: None,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(vec![project::CompletionResponse {
|
|
completions,
|
|
display_options: CompletionDisplayOptions {
|
|
dynamic_width: true,
|
|
},
|
|
is_incomplete: false,
|
|
}])
|
|
})
|
|
}
|
|
|
|
fn is_completion_trigger(
|
|
&self,
|
|
_buffer: &Entity<language::Buffer>,
|
|
_position: language::Anchor,
|
|
text: &str,
|
|
_trigger_in_words: bool,
|
|
_cx: &mut Context<Editor>,
|
|
) -> bool {
|
|
text.chars().last().is_some_and(|last_char| {
|
|
last_char.is_ascii_alphanumeric() || last_char == '_' || last_char == ':'
|
|
})
|
|
}
|
|
}
|