Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:47:03.932Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

skills_setup.rs

335 lines · 14.4 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.