Skip to repository content393 lines · 12.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:11:49.969Z 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
mention_crease.rs
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