Skip to repository content335 lines · 14.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:43:11.437Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
skills_setup.rs
1use agent_skills::{Skill, SkillIndex, SkillSource, encode_skill_share_link};
2use fs::RemoveOptions;
3use gpui::{App, ClipboardItem, PromptLevel, ScrollHandle, SharedString, prelude::*};
4
5use ui::{Divider, Tooltip, prelude::*};
6use util::ResultExt as _;
7use util::paths::PathExt as _;
8
9use crate::pages::SkillCreatorOpenMode;
10use crate::{SettingsUiFile, SettingsWindow};
11
12/// Skills shown on the Skills page for the currently selected settings file:
13/// - User file → global skills only
14/// - Project file → project-local skills for that worktree only
15pub(crate) fn displayed_skills(settings_window: &SettingsWindow, cx: &App) -> Vec<Skill> {
16 let skill_index = cx.try_global::<SkillIndex>();
17
18 match &settings_window.current_file {
19 SettingsUiFile::User => skill_index
20 .map(|idx| idx.global_skills.clone())
21 .unwrap_or_default(),
22 SettingsUiFile::Project((worktree_id, _)) => {
23 let worktree_id = usize::from(*worktree_id);
24 skill_index
25 .and_then(|index| {
26 index
27 .project_skills
28 .iter()
29 .find(|group| group.worktree_id.0 == worktree_id)
30 .map(|group| group.skills.clone())
31 })
32 .unwrap_or_default()
33 }
34 _ => Vec::new(),
35 }
36 .into_iter()
37 .filter(|skill| {
38 !settings_window
39 .hidden_deleted_skill_directory_paths
40 .contains(&skill.directory_path)
41 })
42 .collect()
43}
44
45pub(crate) fn render_skills_setup_page(
46 settings_window: &SettingsWindow,
47 scroll_handle: &ScrollHandle,
48 _window: &mut Window,
49 cx: &mut Context<SettingsWindow>,
50) -> AnyElement {
51 let skills: Vec<Skill> = displayed_skills(settings_window, cx);
52
53 v_flex()
54 .id("skills-page")
55 .size_full()
56 .pt_2()
57 .pb_16()
58 .map(|this| {
59 if skills.is_empty() {
60 let message = match &settings_window.current_file {
61 SettingsUiFile::User => "No global skills installed.",
62 SettingsUiFile::Project(_) => "No project skills found.",
63 _ => "No skills available for this context.",
64 };
65
66 this.px_8().items_center().justify_center().child(
67 v_flex()
68 .items_center()
69 .gap_2()
70 .child(Label::new(message).color(Color::Muted))
71 .child(
72 Button::new("open-skill-creator-empty", "Create a Skill")
73 .tab_index(0_isize)
74 .style(ButtonStyle::Outlined)
75 .start_icon(
76 Icon::new(IconName::Plus)
77 .size(IconSize::Small)
78 .color(Color::Muted),
79 )
80 .on_click(cx.listener(move |this, _event, window, cx| {
81 this.open_skill_creator_sub_page(
82 SkillCreatorOpenMode::Form,
83 window,
84 cx,
85 );
86 })),
87 ),
88 )
89 } else {
90 this.track_scroll(scroll_handle)
91 .overflow_y_scroll()
92 .children(skills.iter().enumerate().flat_map(|(i, skill)| {
93 let mut elements: Vec<AnyElement> =
94 vec![render_skill_row(skill, settings_window, cx)];
95
96 if i + 1 < skills.len() {
97 elements.push(
98 div()
99 .px_8()
100 .child(Divider::horizontal().flex_grow_1())
101 .into_any_element(),
102 );
103 }
104
105 elements
106 }))
107 }
108 })
109 .into_any_element()
110}
111
112fn render_skill_row(
113 skill: &Skill,
114 settings_window: &SettingsWindow,
115 cx: &mut Context<SettingsWindow>,
116) -> AnyElement {
117 let skill_file_path = skill.skill_file_path.clone();
118 let directory_path = skill.directory_path.clone();
119 let skill_name = skill.name.clone();
120
121 let (skill_scope, shared_scope) = match &skill.source {
122 SkillSource::ProjectLocal { .. } => ("project", "used in this project"),
123 _ => ("global", "on this machine"),
124 };
125
126 let share_copied = settings_window.last_copied_skill_directory_path.as_deref()
127 == Some(skill.directory_path.as_path());
128 let warning_message = skill.load_warnings.first().map(|warning| warning.message());
129
130 let (share_icon, share_icon_color) = if share_copied {
131 (IconName::Check, Color::Success)
132 } else {
133 (IconName::Link, Color::Muted)
134 };
135
136 let group = format!("group-{}", skill.name);
137
138 let title = h_flex()
139 .ml(rems_from_px(-22.))
140 .gap_1()
141 .child({
142 let share_skill_file_path = skill.skill_file_path.clone();
143 let share_directory_path = skill.directory_path.clone();
144 IconButton::new(
145 SharedString::from(format!("share-{}", skill.name)),
146 share_icon,
147 )
148 .tab_index(0_isize)
149 .shape(ui::IconButtonShape::Square)
150 .icon_size(IconSize::Small)
151 .icon_color(share_icon_color)
152 .tooltip(Tooltip::text("Copy Share Link"))
153 .visible_on_hover(&group)
154 .on_click(cx.listener(move |_settings_window, _event, _window, cx| {
155 let skill_file_path = share_skill_file_path.clone();
156 let directory_path = share_directory_path.clone();
157 let app_state = workspace::AppState::global(cx);
158 let fs = app_state.fs.clone();
159 cx.spawn(
160 async move |settings_window, cx| match fs.load(&skill_file_path).await {
161 Ok(content) => {
162 let link = encode_skill_share_link(&content);
163 settings_window
164 .update(cx, |settings_window, cx| {
165 cx.write_to_clipboard(ClipboardItem::new_string(link));
166 settings_window.last_copied_skill_directory_path =
167 Some(directory_path.clone());
168 cx.notify();
169 })
170 .ok();
171 }
172 Err(error) => {
173 log::error!(
174 "failed to read skill file {} for sharing: {error:#}",
175 skill_file_path.display()
176 );
177 }
178 },
179 )
180 .detach();
181 }))
182 })
183 .child(Label::new(skill.name.clone()))
184 .when_some(warning_message, |this, warning_message| {
185 this.child(
186 h_flex()
187 .id(SharedString::from(format!("warning-{}", skill.name)))
188 .child(
189 Icon::new(IconName::Warning)
190 .size(IconSize::XSmall)
191 .color(Color::Warning),
192 )
193 .tooltip(Tooltip::text(warning_message)),
194 )
195 });
196
197 h_flex()
198 .group(group)
199 .w_full()
200 .justify_between()
201 .py_3()
202 .px_8()
203 .gap_4()
204 .child(
205 v_flex().gap_0p5().min_w_0().flex_1().child(title).child(
206 Label::new(skill.description.clone())
207 .size(LabelSize::Small)
208 .color(Color::Muted)
209 .line_clamp(5),
210 ),
211 )
212 .child(
213 h_flex()
214 .gap_2()
215 .child(
216 IconButton::new(
217 SharedString::from(format!("delete-{}", skill.name)),
218 IconName::Trash,
219 )
220 .tab_index(0_isize)
221 .icon_size(IconSize::Small)
222 .tooltip(Tooltip::text("Delete Skill"))
223 .on_click(cx.listener(
224 move |settings_window, _event, window, cx| {
225 let directory_path = directory_path.clone();
226 if settings_window
227 .hidden_deleted_skill_directory_paths
228 .contains(&directory_path)
229 {
230 return;
231 }
232
233 let prompt_message =
234 format!("Delete the {skill_scope} skill \"{skill_name}\"?");
235 let prompt_detail = format!(
236 "This will move {} to the trash. This skill is shared with other \
237 agent tools {shared_scope}, so it will no longer be available to \
238 them either.",
239 directory_path.compact().display(),
240 );
241 let answer = window.prompt(
242 PromptLevel::Info,
243 &prompt_message,
244 Some(&prompt_detail),
245 &["Delete", "Cancel"],
246 cx,
247 );
248
249 let app_state = workspace::AppState::global(cx);
250 let fs = app_state.fs.clone();
251 cx.spawn(async move |settings_window, cx| {
252 if answer.await != Ok(0) {
253 return;
254 }
255
256 let confirmed = settings_window
257 .update(cx, |settings_window, cx| {
258 let inserted = settings_window
259 .hidden_deleted_skill_directory_paths
260 .insert(directory_path.clone());
261 if inserted {
262 cx.notify();
263 }
264 inserted
265 })
266 .unwrap_or(false);
267 if !confirmed {
268 return;
269 }
270
271 let trash_result = fs
272 .trash(
273 &directory_path,
274 RemoveOptions {
275 recursive: true,
276 ignore_if_not_exists: true,
277 },
278 )
279 .await;
280 if let Err(error) = trash_result {
281 log::error!(
282 "failed to move skill directory {} to trash: {error:#}",
283 directory_path.display()
284 );
285 settings_window
286 .update(cx, |settings_window, cx| {
287 settings_window
288 .hidden_deleted_skill_directory_paths
289 .remove(&directory_path);
290 cx.notify();
291 })
292 .ok();
293 }
294 })
295 .detach();
296 },
297 )),
298 )
299 .child(
300 Button::new(SharedString::from(format!("open-{}", skill.name)), "Open")
301 .tab_index(0_isize)
302 .style(ButtonStyle::OutlinedGhost)
303 .size(ButtonSize::Medium)
304 .end_icon(
305 Icon::new(IconName::ArrowUpRight)
306 .size(IconSize::Small)
307 .color(Color::Muted),
308 )
309 .on_click(cx.listener(move |settings_window, _event, window, cx| {
310 let skill_file_path = skill_file_path.clone();
311 let Some(original_window) = settings_window.original_window else {
312 return;
313 };
314 original_window
315 .update(cx, |multi_workspace, original_window, cx| {
316 let workspace = multi_workspace.workspace().clone();
317 workspace.update(cx, |workspace, cx| {
318 workspace
319 .open_abs_path(
320 skill_file_path,
321 Default::default(),
322 original_window,
323 cx,
324 )
325 .detach_and_log_err(cx);
326 });
327 })
328 .log_err();
329 window.remove_window();
330 })),
331 ),
332 )
333 .into_any_element()
334}
335