Update skill settings immediately after changes (#57447)

## Summary
- Hide deleted skills immediately in Settings while deletion completes
- Refresh the skill index after creating a skill so Settings updates
without reopening

Closes AI-299
Release Notes:

- Fixed skill management so newly created and deleted skills update in
Settings immediately.
This commit is contained in:
MartinYe1234 2026-05-21 15:51:57 -07:00 committed by GitHub
parent ba350974af
commit 6753eb1736
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 128 additions and 18 deletions

View file

@ -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<Project>, 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,

View file

@ -3070,10 +3070,42 @@ impl AgentPanel {
_window: &mut Window,
cx: &mut Context<Self>,
) {
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::<agent::NativeAgentConnection>()
{
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);

View file

@ -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<SettingsWindow>) -> 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();
},

View file

@ -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<Arc<SearchIndex>>,
list_state: ListState,
shown_errors: HashSet<String>,
pub(crate) hidden_deleted_skill_directory_paths: HashSet<PathBuf>,
pub(crate) regex_validation_error: Option<String>,
last_copied_link_path: Option<&'static str>,
}
@ -1542,6 +1545,28 @@ impl SettingsWindow {
})
.detach();
cx.observe_global_in::<SkillIndex>(window, |this, _window, cx| {
if let Some(skill_index) = cx.try_global::<SkillIndex>() {
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,
};

View file

@ -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<WeakEntity<Workspace>>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
on_saved: Option<Rc<dyn Fn(&mut App)>>,
cx: &mut App,
) -> Task<Result<WindowHandle<SkillCreator>>> {
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<Entity<PlatformTitleBar>>,
workspace: Option<WeakEntity<Workspace>>,
fs: Arc<dyn Fs>,
on_saved: Option<Rc<dyn Fn(&mut App)>>,
name_editor: Entity<InputField>,
description_editor: Entity<InputField>,
body_editor: Entity<Editor>,
@ -198,6 +203,7 @@ impl SkillCreator {
workspace: Option<WeakEntity<Workspace>>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
on_saved: Option<Rc<dyn Fn(&mut App)>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> 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(