Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:11:49.969Z 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

mention_crease.rs

393 lines · 12.8 KB · rust
1use std::{path::PathBuf, time::Duration};
2
3use acp_thread::MentionUri;
4use agent_client_protocol::schema::v1 as acp;
5use editor::Editor;
6use gpui::{
7    Animation, AnimationExt, AnyView, Context, IntoElement, TaskExt, WeakEntity, Window,
8    pulsating_between,
9};
10use language::Buffer;
11use rope::Point;
12use settings::Settings;
13use theme_settings::ThemeSettings;
14use ui::{ButtonLike, TintColor, Tooltip, prelude::*};
15use workspace::{OpenOptions, Workspace};
16
17use crate::open_abs_path_at_point;
18
19#[derive(IntoElement)]
20pub struct MentionCrease {
21    id: ElementId,
22    icon: SharedString,
23    label: SharedString,
24    mention_uri: Option<MentionUri>,
25    workspace: Option<WeakEntity<Workspace>>,
26    is_toggled: bool,
27    is_loading: bool,
28    tooltip: Option<SharedString>,
29    image_preview: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
30}
31
32impl MentionCrease {
33    pub fn new(
34        id: impl Into<ElementId>,
35        icon: impl Into<SharedString>,
36        label: impl Into<SharedString>,
37    ) -> Self {
38        Self {
39            id: id.into(),
40            icon: icon.into(),
41            label: label.into(),
42            mention_uri: None,
43            workspace: None,
44            is_toggled: false,
45            is_loading: false,
46            tooltip: None,
47            image_preview: None,
48        }
49    }
50
51    pub fn mention_uri(mut self, mention_uri: Option<MentionUri>) -> Self {
52        self.mention_uri = mention_uri;
53        self
54    }
55
56    pub fn workspace(mut self, workspace: Option<WeakEntity<Workspace>>) -> Self {
57        self.workspace = workspace;
58        self
59    }
60
61    pub fn is_toggled(mut self, is_toggled: bool) -> Self {
62        self.is_toggled = is_toggled;
63        self
64    }
65
66    pub fn is_loading(mut self, is_loading: bool) -> Self {
67        self.is_loading = is_loading;
68        self
69    }
70
71    pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
72        self.tooltip = Some(tooltip.into());
73        self
74    }
75
76    pub fn image_preview(
77        mut self,
78        builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
79    ) -> Self {
80        self.image_preview = Some(Box::new(builder));
81        self
82    }
83}
84
85impl RenderOnce for MentionCrease {
86    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
87        let settings = ThemeSettings::get_global(cx);
88        let font_size = settings.agent_buffer_font_size(cx);
89        let buffer_font = settings.buffer_font.clone();
90        let is_loading = self.is_loading;
91        let tooltip = self.tooltip;
92        let image_preview = self.image_preview;
93
94        let button_height = DefiniteLength::Absolute(AbsoluteLength::Pixels(
95            px(window.line_height().into()) - px(1.),
96        ));
97
98        ButtonLike::new(self.id)
99            .style(ButtonStyle::Outlined)
100            .size(ButtonSize::Compact)
101            .height(button_height)
102            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
103            .toggle_state(self.is_toggled)
104            .when_some(
105                self.mention_uri.clone().zip(self.workspace.clone()),
106                |this, (mention_uri, workspace)| {
107                    this.on_click(move |_event, window, cx| {
108                        open_mention_uri(mention_uri.clone(), &workspace, window, cx);
109                    })
110                },
111            )
112            .child(
113                h_flex()
114                    .pb_px()
115                    .gap_1()
116                    .font(buffer_font)
117                    .text_size(font_size)
118                    .child(
119                        Icon::from_path(self.icon.clone())
120                            .size(IconSize::XSmall)
121                            .color(Color::Muted),
122                    )
123                    .child(self.label.clone())
124                    .map(|this| {
125                        if is_loading {
126                            this.with_animation(
127                                "loading-context-crease",
128                                Animation::new(Duration::from_secs(2))
129                                    .repeat()
130                                    .with_easing(pulsating_between(0.4, 0.8)),
131                                |label, delta| label.opacity(delta),
132                            )
133                            .into_any()
134                        } else {
135                            this.into_any()
136                        }
137                    }),
138            )
139            .map(|button| {
140                if let Some(image_preview) = image_preview {
141                    button.hoverable_tooltip(image_preview)
142                } else {
143                    button.when_some(tooltip, |this, tooltip_text| {
144                        this.tooltip(Tooltip::text(tooltip_text))
145                    })
146                }
147            })
148    }
149}
150
151fn open_mention_uri(
152    mention_uri: MentionUri,
153    workspace: &WeakEntity<Workspace>,
154    window: &mut Window,
155    cx: &mut App,
156) {
157    let Some(workspace) = workspace.upgrade() else {
158        return;
159    };
160
161    workspace.update(cx, |workspace, cx| match mention_uri {
162        MentionUri::File { abs_path } => {
163            open_abs_path_at_point(workspace, abs_path, None, window, cx);
164        }
165        MentionUri::Symbol {
166            abs_path,
167            line_range,
168            ..
169        } => {
170            open_abs_path_at_point(
171                workspace,
172                abs_path,
173                Some(Point::new(*line_range.start(), 0)),
174                window,
175                cx,
176            );
177        }
178        MentionUri::Selection {
179            abs_path: Some(abs_path),
180            line_range,
181            column,
182        } => {
183            open_abs_path_at_point(
184                workspace,
185                abs_path,
186                Some(Point::new(*line_range.start(), column.unwrap_or(0))),
187                window,
188                cx,
189            );
190        }
191        MentionUri::Directory { abs_path } => {
192            reveal_in_project_panel(workspace, abs_path, cx);
193        }
194        MentionUri::Thread { id, name } => {
195            open_thread(workspace, id, name, window, cx);
196        }
197        MentionUri::Skill {
198            skill_file_path, ..
199        } => {
200            open_skill_file(workspace, skill_file_path, window, cx);
201        }
202        MentionUri::Rule { name, .. } => {
203            open_migrated_rule(workspace, &name, window, cx);
204        }
205        MentionUri::Fetch { url } => {
206            cx.open_url(url.as_str());
207        }
208        MentionUri::PastedImage { .. }
209        | MentionUri::Selection { abs_path: None, .. }
210        | MentionUri::Diagnostics { .. }
211        | MentionUri::TerminalSelection { .. }
212        | MentionUri::GitDiff { .. }
213        | MentionUri::MergeConflict { .. } => {}
214    });
215}
216
217/// Notify the user that rules became skills and open the skill the rule was
218/// migrated into. Migrated skills live in the local global skills dir, so the
219/// file is always resolved against the local filesystem (local, SSH, or
220/// collab). Does nothing else when no matching skill exists.
221pub(crate) fn open_migrated_rule(
222    workspace: &mut Workspace,
223    name: &str,
224    window: &mut Window,
225    cx: &mut Context<Workspace>,
226) {
227    struct RulesMigratedToSkillsToast;
228    workspace.show_toast(
229        workspace::Toast::new(
230            workspace::notifications::NotificationId::unique::<RulesMigratedToSkillsToast>(),
231            "Rules have been migrated to Skills.",
232        )
233        .on_click("View docs", |_, cx| {
234            cx.open_url("https://zed.dev/docs/ai/skills");
235        })
236        .autohide(),
237        cx,
238    );
239
240    let Some(slug) = agent_skills::slugify_skill_name(name) else {
241        return;
242    };
243    let skill_file_path = agent_skills::global_skills_dir()
244        .join(slug)
245        .join(agent_skills::SKILL_FILE_NAME);
246
247    if workspace.project().read(cx).is_local() {
248        // Local project: open the editable on-disk file if it exists.
249        if skill_file_path.exists() {
250            open_skill_file(workspace, skill_file_path, window, cx);
251        }
252        return;
253    }
254
255    // Remote/collab: `open_abs_path` targets the remote project, where this
256    // local file doesn't exist, so read it locally and show it read-only.
257    let fs = workspace.app_state().fs.clone();
258    cx.spawn_in(window, async move |workspace, cx| {
259        let Ok(content) = fs.load(&skill_file_path).await else {
260            return Ok(()); // No readable migrated skill: do nothing.
261        };
262        let title = skill_content_buffer_title(&skill_file_path);
263        workspace.update_in(cx, |workspace, window, cx| {
264            open_skill_content_buffer(workspace, title, content, window, cx);
265        })
266    })
267    .detach_and_log_err(cx);
268}
269
270fn open_skill_file(
271    workspace: &mut Workspace,
272    skill_file_path: PathBuf,
273    window: &mut Window,
274    cx: &mut Context<Workspace>,
275) {
276    // Built-in skills have synthetic paths with no on-disk file, so show their
277    // embedded content in a local buffer instead.
278    if let Some(content) = agent_skills::builtin_skill_content(&skill_file_path) {
279        let title = skill_content_buffer_title(&skill_file_path);
280        open_skill_content_buffer(workspace, title, content, window, cx);
281        return;
282    }
283
284    workspace
285        .open_abs_path(
286            skill_file_path,
287            OpenOptions {
288                focus: Some(true),
289                ..Default::default()
290            },
291            window,
292            cx,
293        )
294        .detach_and_log_err(cx);
295}
296
297fn skill_content_buffer_title(skill_file_path: &std::path::Path) -> String {
298    skill_file_path
299        .parent()
300        .and_then(|p| p.file_name())
301        .map(|n| n.to_string_lossy().into_owned())
302        .unwrap_or_else(|| "skill".into())
303}
304
305/// Open `content` as a local, read-only Markdown buffer, for skills with no
306/// openable file in the active project (built-in skills, and migrated rules on
307/// remote/collab projects). It's deliberately not registered with the project's
308/// buffer store: that keeps it out of search and avoids
309/// `Project::create_local_buffer` panicking on remote projects.
310fn open_skill_content_buffer(
311    workspace: &mut Workspace,
312    title: String,
313    content: impl Into<String>,
314    window: &mut Window,
315    cx: &mut Context<Workspace>,
316) {
317    let languages = workspace.project().read(cx).languages().clone();
318    let buffer = cx.new(|cx| Buffer::local(content, cx));
319    // Set markdown highlighting asynchronously — the buffer
320    // opens instantly and the highlighting appears once loaded.
321    cx.spawn({
322        let buffer = buffer.clone();
323        async move |_, cx| {
324            if let Ok(markdown) = languages.language_for_name("Markdown").await {
325                buffer.update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx));
326            }
327        }
328    })
329    .detach();
330    let editor = cx.new(|cx| {
331        let mut editor = Editor::for_buffer(buffer, None, window, cx);
332        editor.set_read_only(true);
333        editor
334            .buffer()
335            .update(cx, |buffer, cx| buffer.set_title(title, cx));
336        editor
337    });
338    let pane = workspace.active_pane().clone();
339    workspace.add_item(pane, Box::new(editor), None, true, true, window, cx);
340}
341
342fn reveal_in_project_panel(
343    workspace: &mut Workspace,
344    abs_path: PathBuf,
345    cx: &mut Context<Workspace>,
346) {
347    let project = workspace.project();
348    let Some(entry_id) = project.update(cx, |project, cx| {
349        let path = project.find_project_path(&abs_path, cx)?;
350        project.entry_for_path(&path, cx).map(|entry| entry.id)
351    }) else {
352        return;
353    };
354
355    project.update(cx, |_, cx| {
356        cx.emit(project::Event::RevealInProjectPanel(entry_id));
357    });
358}
359
360fn open_thread(
361    workspace: &mut Workspace,
362    id: acp::SessionId,
363    name: String,
364    window: &mut Window,
365    cx: &mut Context<Workspace>,
366) {
367    use crate::{Agent, AgentPanel, AgentThreadSource, thread_metadata_store::ThreadMetadataStore};
368
369    let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
370        return;
371    };
372
373    // Right now we only support loading threads in the native agent.
374    panel.update(cx, |panel, cx| {
375        let thread_id = ThreadMetadataStore::try_global(cx)
376            .and_then(|store| store.read(cx).entry_by_session(&id).map(|m| m.thread_id));
377        if let Some(thread_id) = thread_id {
378            panel.load_agent_thread(
379                Agent::NativeAgent,
380                thread_id,
381                None,
382                Some(name.into()),
383                true,
384                AgentThreadSource::AgentPanel,
385                window,
386                cx,
387            );
388        } else {
389            panel.open_thread(id, None, Some(name.into()), window, cx);
390        }
391    });
392}
393
Served at tenant.openagents/omega Member data and write actions are omitted.