diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index f704e997ac0..8ef06de1649 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -1766,6 +1766,16 @@ impl NativeAgentConnection { .update(cx, |agent, cx| agent.ensure_skills_scan_started(cx)); } + pub fn refresh_skills_for_project(&self, project: Entity, cx: &mut App) { + self.0.update(cx, |agent, cx| { + let project_id = agent.get_or_create_project_state(&project, cx); + agent.ensure_skills_scan_started(cx); + if let Some(state) = agent.projects.get_mut(&project_id) { + state.project_context_needs_refresh.send(()).ok(); + } + }); + } + pub fn available_skills( &self, session_id: &acp::SessionId, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index f83025b7735..fb7441d186a 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -3070,10 +3070,42 @@ impl AgentPanel { _window: &mut Window, cx: &mut Context, ) { + let this = cx.weak_entity(); + let on_saved = Rc::new(move |cx: &mut App| { + this.update(cx, |this, cx| { + if !this.has_open_project(cx) { + return; + } + + this.ensure_native_agent_connection(cx); + let Some(connect_task) = this.connection_store.update(cx, |store, cx| { + store + .entry(&Agent::NativeAgent) + .map(|entry| entry.read(cx).wait_for_connection()) + }) else { + return; + }; + let project = this.project.clone(); + cx.spawn(async move |_this, cx| -> Result<()> { + let connected = connect_task.await?; + if let Some(native_connection) = connected + .connection + .downcast::() + { + cx.update(|cx| native_connection.refresh_skills_for_project(project, cx)); + } + Ok(()) + }) + .detach_and_log_err(cx); + }) + .ok(); + }); + open_skill_creator( Some(self.workspace.clone()), self.language_registry.clone(), self.fs.clone(), + Some(on_saved), cx, ) .detach_and_log_err(cx); diff --git a/crates/settings_ui/src/pages/skills_setup.rs b/crates/settings_ui/src/pages/skills_setup.rs index bce8b7e7f51..0208f7db47f 100644 --- a/crates/settings_ui/src/pages/skills_setup.rs +++ b/crates/settings_ui/src/pages/skills_setup.rs @@ -23,18 +23,26 @@ pub(crate) fn render_skills_setup_page( .map(|idx| idx.global_skills.clone()) .unwrap_or_default(), SettingsUiFile::Project((worktree_id, _)) => { - let wt_id = usize::from(*worktree_id); + let worktree_id = usize::from(*worktree_id); skill_index - .and_then(|idx| { - idx.project_skills + .and_then(|index| { + index + .project_skills .iter() - .find(|g| g.worktree_id.0 == wt_id) - .map(|g| g.skills.clone()) + .find(|group| group.worktree_id.0 == worktree_id) + .map(|group| group.skills.clone()) }) .unwrap_or_default() } _ => Vec::new(), - }; + } + .into_iter() + .filter(|skill| { + !settings_window + .hidden_deleted_skill_directory_paths + .contains(&skill.directory_path) + }) + .collect(); v_flex() .id("skills-page") @@ -129,20 +137,42 @@ fn render_skill_row(skill: &Skill, cx: &mut Context) -> AnyEleme .icon_size(IconSize::Small) .tooltip(Tooltip::text("Delete Skill")) .on_click(cx.listener( - move |_this, _event, _window, cx| { + move |settings_window, _event, _window, cx| { let directory_path = directory_path.clone(); + if !settings_window + .hidden_deleted_skill_directory_paths + .insert(directory_path.clone()) + { + return; + } + cx.notify(); + let app_state = workspace::AppState::global(cx); let fs = app_state.fs.clone(); - cx.spawn(async move |_this, _cx| { - fs.remove_dir( - &directory_path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .log_err(); + cx.spawn(async move |settings_window, cx| { + let remove_result = fs + .remove_dir( + &directory_path, + RemoveOptions { + recursive: true, + ignore_if_not_exists: true, + }, + ) + .await; + if let Err(error) = remove_result { + log::error!( + "failed to delete skill directory {}: {error:#}", + directory_path.display() + ); + settings_window + .update(cx, |settings_window, cx| { + settings_window + .hidden_deleted_skill_directory_paths + .remove(&directory_path); + cx.notify(); + }) + .ok(); + } }) .detach(); }, diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 08ac192c88b..2a49a95af2a 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -2,6 +2,7 @@ mod components; mod page_data; pub mod pages; +use agent_skills::SkillIndex; use anyhow::{Context as _, Result}; use editor::{Editor, EditorEvent}; use futures::{StreamExt, channel::mpsc}; @@ -29,6 +30,7 @@ use std::{ collections::{HashMap, HashSet}, num::{NonZero, NonZeroU32}, ops::Range, + path::PathBuf, rc::Rc, sync::{Arc, LazyLock, RwLock}, time::Duration, @@ -767,6 +769,7 @@ pub struct SettingsWindow { search_index: Option>, list_state: ListState, shown_errors: HashSet, + pub(crate) hidden_deleted_skill_directory_paths: HashSet, pub(crate) regex_validation_error: Option, last_copied_link_path: Option<&'static str>, } @@ -1542,6 +1545,28 @@ impl SettingsWindow { }) .detach(); + cx.observe_global_in::(window, |this, _window, cx| { + if let Some(skill_index) = cx.try_global::() { + this.hidden_deleted_skill_directory_paths + .retain(|directory_path| { + skill_index + .global_skills + .iter() + .chain( + skill_index + .project_skills + .iter() + .flat_map(|group| group.skills.iter()), + ) + .any(|skill| skill.directory_path.as_path() == directory_path.as_path()) + }); + } else { + this.hidden_deleted_skill_directory_paths.clear(); + } + cx.notify(); + }) + .detach(); + cx.on_window_closed(|cx, _window_id| { if let Some(existing_window) = cx .windows() @@ -1689,6 +1714,7 @@ impl SettingsWindow { .tab_stop(false), search_index: None, shown_errors: HashSet::default(), + hidden_deleted_skill_directory_paths: HashSet::default(), regex_validation_error: None, list_state, last_copied_link_path: None, @@ -4548,6 +4574,7 @@ pub mod test { search_index: None, list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)), shown_errors: HashSet::default(), + hidden_deleted_skill_directory_paths: HashSet::default(), regex_validation_error: None, last_copied_link_path: None, } @@ -4674,6 +4701,7 @@ pub mod test { search_index: None, list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)), shown_errors: HashSet::default(), + hidden_deleted_skill_directory_paths: HashSet::default(), regex_validation_error: None, last_copied_link_path: None, }; diff --git a/crates/skill_creator/src/skill_creator.rs b/crates/skill_creator/src/skill_creator.rs index 8a1ee24437d..b9365bbc34d 100644 --- a/crates/skill_creator/src/skill_creator.rs +++ b/crates/skill_creator/src/skill_creator.rs @@ -15,6 +15,7 @@ use platform_title_bar::PlatformTitleBar; use release_channel::ReleaseChannel; use settings::{ActionSequence, Settings}; use std::path::PathBuf; +use std::rc::Rc; use std::sync::Arc; use theme_settings::ThemeSettings; use ui::{ @@ -113,6 +114,7 @@ pub fn open_skill_creator( workspace: Option>, language_registry: Arc, fs: Arc, + on_saved: Option>, cx: &mut App, ) -> Task>> { cx.spawn(async move |cx| { @@ -161,7 +163,9 @@ pub fn open_skill_creator( ..Default::default() }, |window, cx| { - cx.new(|cx| SkillCreator::new(workspace, language_registry, fs, window, cx)) + cx.new(|cx| { + SkillCreator::new(workspace, language_registry, fs, on_saved, window, cx) + }) }, ) }) @@ -173,6 +177,7 @@ pub struct SkillCreator { title_bar: Option>, workspace: Option>, fs: Arc, + on_saved: Option>, name_editor: Entity, description_editor: Entity, body_editor: Entity, @@ -198,6 +203,7 @@ impl SkillCreator { workspace: Option>, language_registry: Arc, fs: Arc, + on_saved: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -321,6 +327,7 @@ impl SkillCreator { }, workspace, fs, + on_saved, name_editor, description_editor, body_editor, @@ -468,6 +475,9 @@ impl SkillCreator { this.save_task = None; match result { Ok(path) => { + if let Some(on_saved) = &this.on_saved { + on_saved(cx); + } if let Some(workspace) = workspace.as_ref().and_then(|w| w.upgrade()) { workspace.update(cx, |workspace, cx| { workspace.show_toast(