context_pill.rs

  1use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
  2
  3use file_icons::FileIcons;
  4use futures::FutureExt as _;
  5use gpui::{
  6    Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
  7    pulsating_between,
  8};
  9use language_model::LanguageModelImage;
 10use project::Project;
 11use prompt_store::PromptStore;
 12use rope::Point;
 13use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
 14
 15use crate::context::{
 16    AgentContext, AgentContextHandle, ContextId, ContextKind, DirectoryContext,
 17    DirectoryContextHandle, FetchedUrlContext, FileContext, FileContextHandle, ImageContext,
 18    ImageStatus, RulesContext, RulesContextHandle, SelectionContext, SelectionContextHandle,
 19    SymbolContext, SymbolContextHandle, TextThreadContext, TextThreadContextHandle, ThreadContext,
 20    ThreadContextHandle,
 21};
 22
 23#[derive(IntoElement)]
 24pub enum ContextPill {
 25    Added {
 26        context: AddedContext,
 27        dupe_name: bool,
 28        focused: bool,
 29        on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
 30        on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
 31    },
 32    Suggested {
 33        name: SharedString,
 34        icon_path: Option<SharedString>,
 35        kind: ContextKind,
 36        focused: bool,
 37        on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
 38    },
 39}
 40
 41impl ContextPill {
 42    pub fn added(
 43        context: AddedContext,
 44        dupe_name: bool,
 45        focused: bool,
 46        on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
 47    ) -> Self {
 48        Self::Added {
 49            context,
 50            dupe_name,
 51            on_remove,
 52            focused,
 53            on_click: None,
 54        }
 55    }
 56
 57    pub fn suggested(
 58        name: SharedString,
 59        icon_path: Option<SharedString>,
 60        kind: ContextKind,
 61        focused: bool,
 62    ) -> Self {
 63        Self::Suggested {
 64            name,
 65            icon_path,
 66            kind,
 67            focused,
 68            on_click: None,
 69        }
 70    }
 71
 72    pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
 73        match &mut self {
 74            ContextPill::Added { on_click, .. } => {
 75                *on_click = Some(listener);
 76            }
 77            ContextPill::Suggested { on_click, .. } => {
 78                *on_click = Some(listener);
 79            }
 80        }
 81        self
 82    }
 83
 84    pub fn id(&self) -> ElementId {
 85        match self {
 86            Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
 87            Self::Suggested { .. } => "suggested-context-pill".into(),
 88        }
 89    }
 90
 91    pub fn icon(&self) -> Icon {
 92        match self {
 93            Self::Suggested {
 94                icon_path: Some(icon_path),
 95                ..
 96            }
 97            | Self::Added {
 98                context:
 99                    AddedContext {
100                        icon_path: Some(icon_path),
101                        ..
102                    },
103                ..
104            } => Icon::from_path(icon_path),
105            Self::Suggested { kind, .. }
106            | Self::Added {
107                context: AddedContext { kind, .. },
108                ..
109            } => Icon::new(kind.icon()),
110        }
111    }
112}
113
114impl RenderOnce for ContextPill {
115    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
116        let color = cx.theme().colors();
117
118        let base_pill = h_flex()
119            .id(self.id())
120            .pl_1()
121            .pb(px(1.))
122            .border_1()
123            .rounded_sm()
124            .gap_1()
125            .child(self.icon().size(IconSize::XSmall).color(Color::Muted));
126
127        match &self {
128            ContextPill::Added {
129                context,
130                dupe_name,
131                on_remove,
132                focused,
133                on_click,
134            } => {
135                let status_is_error = matches!(context.status, ContextStatus::Error { .. });
136
137                base_pill
138                    .pr(if on_remove.is_some() { px(2.) } else { px(4.) })
139                    .map(|pill| {
140                        if status_is_error {
141                            pill.bg(cx.theme().status().error_background)
142                                .border_color(cx.theme().status().error_border)
143                        } else if *focused {
144                            pill.bg(color.element_background)
145                                .border_color(color.border_focused)
146                        } else {
147                            pill.bg(color.element_background)
148                                .border_color(color.border.opacity(0.5))
149                        }
150                    })
151                    .child(
152                        h_flex()
153                            .id("context-data")
154                            .gap_1()
155                            .child(
156                                div().max_w_64().child(
157                                    Label::new(context.name.clone())
158                                        .size(LabelSize::Small)
159                                        .truncate(),
160                                ),
161                            )
162                            .when_some(context.parent.as_ref(), |element, parent_name| {
163                                if *dupe_name {
164                                    element.child(
165                                        Label::new(parent_name.clone())
166                                            .size(LabelSize::XSmall)
167                                            .color(Color::Muted),
168                                    )
169                                } else {
170                                    element
171                                }
172                            })
173                            .when_some(context.tooltip.as_ref(), |element, tooltip| {
174                                element.tooltip(Tooltip::text(tooltip.clone()))
175                            })
176                            .map(|element| match &context.status {
177                                ContextStatus::Ready => element
178                                    .when_some(
179                                        context.render_hover.as_ref(),
180                                        |element, render_hover| {
181                                            let render_hover = render_hover.clone();
182                                            element.hoverable_tooltip(move |window, cx| {
183                                                render_hover(window, cx)
184                                            })
185                                        },
186                                    )
187                                    .into_any(),
188                                ContextStatus::Loading { message } => element
189                                    .tooltip(ui::Tooltip::text(message.clone()))
190                                    .with_animation(
191                                        "pulsating-ctx-pill",
192                                        Animation::new(Duration::from_secs(2))
193                                            .repeat()
194                                            .with_easing(pulsating_between(0.4, 0.8)),
195                                        |label, delta| label.opacity(delta),
196                                    )
197                                    .into_any_element(),
198                                ContextStatus::Error { message } => element
199                                    .tooltip(ui::Tooltip::text(message.clone()))
200                                    .into_any_element(),
201                            }),
202                    )
203                    .when_some(on_remove.as_ref(), |element, on_remove| {
204                        element.child(
205                            IconButton::new(
206                                context.handle.element_id("remove".into()),
207                                IconName::Close,
208                            )
209                            .shape(IconButtonShape::Square)
210                            .icon_size(IconSize::XSmall)
211                            .tooltip(Tooltip::text("Remove Context"))
212                            .on_click({
213                                let on_remove = on_remove.clone();
214                                move |event, window, cx| on_remove(event, window, cx)
215                            }),
216                        )
217                    })
218                    .when_some(on_click.as_ref(), |element, on_click| {
219                        let on_click = on_click.clone();
220                        element.cursor_pointer().on_click(move |event, window, cx| {
221                            on_click(event, window, cx);
222                            cx.stop_propagation();
223                        })
224                    })
225                    .into_any_element()
226            }
227            ContextPill::Suggested {
228                name,
229                icon_path: _,
230                kind: _,
231                focused,
232                on_click,
233            } => base_pill
234                .cursor_pointer()
235                .pr_1()
236                .border_dashed()
237                .map(|pill| {
238                    if *focused {
239                        pill.border_color(color.border_focused)
240                            .bg(color.element_background.opacity(0.5))
241                    } else {
242                        pill.border_color(color.border)
243                    }
244                })
245                .hover(|style| style.bg(color.element_hover.opacity(0.5)))
246                .child(
247                    div().max_w_64().child(
248                        Label::new(name.clone())
249                            .size(LabelSize::Small)
250                            .color(Color::Muted)
251                            .truncate(),
252                    ),
253                )
254                .tooltip(|window, cx| {
255                    Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
256                })
257                .when_some(on_click.as_ref(), |element, on_click| {
258                    let on_click = on_click.clone();
259                    element.on_click(move |event, window, cx| {
260                        on_click(event, window, cx);
261                        cx.stop_propagation();
262                    })
263                })
264                .into_any(),
265        }
266    }
267}
268
269pub enum ContextStatus {
270    Ready,
271    Loading { message: SharedString },
272    Error { message: SharedString },
273}
274
275#[derive(RegisterComponent)]
276pub struct AddedContext {
277    pub handle: AgentContextHandle,
278    pub kind: ContextKind,
279    pub name: SharedString,
280    pub parent: Option<SharedString>,
281    pub tooltip: Option<SharedString>,
282    pub icon_path: Option<SharedString>,
283    pub status: ContextStatus,
284    pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
285}
286
287impl AddedContext {
288    /// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
289    /// `None` if `DirectoryContext` or `RulesContext` no longer exist.
290    ///
291    /// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
292    pub fn new_pending(
293        handle: AgentContextHandle,
294        prompt_store: Option<&Entity<PromptStore>>,
295        project: &Project,
296        cx: &App,
297    ) -> Option<AddedContext> {
298        match handle {
299            AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
300            AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
301            AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
302            AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
303            AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
304            AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
305            AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
306            AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
307            AgentContextHandle::Image(handle) => Some(Self::image(handle, cx)),
308        }
309    }
310
311    pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext {
312        match context {
313            AgentContext::File(context) => Self::attached_file(context, cx),
314            AgentContext::Directory(context) => Self::attached_directory(context),
315            AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
316            AgentContext::Selection(context) => Self::attached_selection(context, cx),
317            AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
318            AgentContext::Thread(context) => Self::attached_thread(context),
319            AgentContext::TextThread(context) => Self::attached_text_thread(context),
320            AgentContext::Rules(context) => Self::attached_rules(context),
321            AgentContext::Image(context) => Self::image(context.clone(), cx),
322        }
323    }
324
325    fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
326        let full_path = handle.buffer.read(cx).file()?.full_path(cx);
327        Some(Self::file(handle, &full_path, cx))
328    }
329
330    fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
331        Self::file(context.handle.clone(), &context.full_path, cx)
332    }
333
334    fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
335        let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
336        let (name, parent) =
337            extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
338        AddedContext {
339            kind: ContextKind::File,
340            name,
341            parent,
342            tooltip: Some(full_path_string),
343            icon_path: FileIcons::get_icon(&full_path, cx),
344            status: ContextStatus::Ready,
345            render_hover: None,
346            handle: AgentContextHandle::File(handle),
347        }
348    }
349
350    fn pending_directory(
351        handle: DirectoryContextHandle,
352        project: &Project,
353        cx: &App,
354    ) -> Option<AddedContext> {
355        let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
356        let entry = worktree.entry_for_id(handle.entry_id)?;
357        let full_path = worktree.full_path(&entry.path);
358        Some(Self::directory(handle, &full_path))
359    }
360
361    fn attached_directory(context: &DirectoryContext) -> AddedContext {
362        Self::directory(context.handle.clone(), &context.full_path)
363    }
364
365    fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
366        let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
367        let (name, parent) =
368            extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
369        AddedContext {
370            kind: ContextKind::Directory,
371            name,
372            parent,
373            tooltip: Some(full_path_string),
374            icon_path: None,
375            status: ContextStatus::Ready,
376            render_hover: None,
377            handle: AgentContextHandle::Directory(handle),
378        }
379    }
380
381    fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
382        let excerpt =
383            ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
384        Some(AddedContext {
385            kind: ContextKind::Symbol,
386            name: handle.symbol.clone(),
387            parent: Some(excerpt.file_name_and_range.clone()),
388            tooltip: None,
389            icon_path: None,
390            status: ContextStatus::Ready,
391            render_hover: {
392                let handle = handle.clone();
393                Some(Rc::new(move |_, cx| {
394                    excerpt.hover_view(handle.text(cx), cx).into()
395                }))
396            },
397            handle: AgentContextHandle::Symbol(handle),
398        })
399    }
400
401    fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
402        let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
403        AddedContext {
404            kind: ContextKind::Symbol,
405            name: context.handle.symbol.clone(),
406            parent: Some(excerpt.file_name_and_range.clone()),
407            tooltip: None,
408            icon_path: None,
409            status: ContextStatus::Ready,
410            render_hover: {
411                let text = context.text.clone();
412                Some(Rc::new(move |_, cx| {
413                    excerpt.hover_view(text.clone(), cx).into()
414                }))
415            },
416            handle: AgentContextHandle::Symbol(context.handle.clone()),
417        }
418    }
419
420    fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
421        let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
422        Some(AddedContext {
423            kind: ContextKind::Selection,
424            name: excerpt.file_name_and_range.clone(),
425            parent: excerpt.parent_name.clone(),
426            tooltip: None,
427            icon_path: excerpt.icon_path.clone(),
428            status: ContextStatus::Ready,
429            render_hover: {
430                let handle = handle.clone();
431                Some(Rc::new(move |_, cx| {
432                    excerpt.hover_view(handle.text(cx), cx).into()
433                }))
434            },
435            handle: AgentContextHandle::Selection(handle),
436        })
437    }
438
439    fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
440        let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
441        AddedContext {
442            kind: ContextKind::Selection,
443            name: excerpt.file_name_and_range.clone(),
444            parent: excerpt.parent_name.clone(),
445            tooltip: None,
446            icon_path: excerpt.icon_path.clone(),
447            status: ContextStatus::Ready,
448            render_hover: {
449                let text = context.text.clone();
450                Some(Rc::new(move |_, cx| {
451                    excerpt.hover_view(text.clone(), cx).into()
452                }))
453            },
454            handle: AgentContextHandle::Selection(context.handle.clone()),
455        }
456    }
457
458    fn fetched_url(context: FetchedUrlContext) -> AddedContext {
459        AddedContext {
460            kind: ContextKind::FetchedUrl,
461            name: context.url.clone(),
462            parent: None,
463            tooltip: None,
464            icon_path: None,
465            status: ContextStatus::Ready,
466            render_hover: None,
467            handle: AgentContextHandle::FetchedUrl(context),
468        }
469    }
470
471    fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
472        AddedContext {
473            kind: ContextKind::Thread,
474            name: handle.title(cx),
475            parent: None,
476            tooltip: None,
477            icon_path: None,
478            status: if handle.thread.read(cx).is_generating_detailed_summary() {
479                ContextStatus::Loading {
480                    message: "Summarizing…".into(),
481                }
482            } else {
483                ContextStatus::Ready
484            },
485            render_hover: {
486                let thread = handle.thread.clone();
487                Some(Rc::new(move |_, cx| {
488                    let text = thread.read(cx).latest_detailed_summary_or_text();
489                    ContextPillHover::new_text(text.clone(), cx).into()
490                }))
491            },
492            handle: AgentContextHandle::Thread(handle),
493        }
494    }
495
496    fn attached_thread(context: &ThreadContext) -> AddedContext {
497        AddedContext {
498            kind: ContextKind::Thread,
499            name: context.title.clone(),
500            parent: None,
501            tooltip: None,
502            icon_path: None,
503            status: ContextStatus::Ready,
504            render_hover: {
505                let text = context.text.clone();
506                Some(Rc::new(move |_, cx| {
507                    ContextPillHover::new_text(text.clone(), cx).into()
508                }))
509            },
510            handle: AgentContextHandle::Thread(context.handle.clone()),
511        }
512    }
513
514    fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
515        AddedContext {
516            kind: ContextKind::TextThread,
517            name: handle.title(cx),
518            parent: None,
519            tooltip: None,
520            icon_path: None,
521            status: ContextStatus::Ready,
522            render_hover: {
523                let context = handle.context.clone();
524                Some(Rc::new(move |_, cx| {
525                    let text = context.read(cx).to_xml(cx);
526                    ContextPillHover::new_text(text.into(), cx).into()
527                }))
528            },
529            handle: AgentContextHandle::TextThread(handle),
530        }
531    }
532
533    fn attached_text_thread(context: &TextThreadContext) -> AddedContext {
534        AddedContext {
535            kind: ContextKind::TextThread,
536            name: context.title.clone(),
537            parent: None,
538            tooltip: None,
539            icon_path: None,
540            status: ContextStatus::Ready,
541            render_hover: {
542                let text = context.text.clone();
543                Some(Rc::new(move |_, cx| {
544                    ContextPillHover::new_text(text.clone(), cx).into()
545                }))
546            },
547            handle: AgentContextHandle::TextThread(context.handle.clone()),
548        }
549    }
550
551    fn pending_rules(
552        handle: RulesContextHandle,
553        prompt_store: Option<&Entity<PromptStore>>,
554        cx: &App,
555    ) -> Option<AddedContext> {
556        let title = prompt_store
557            .as_ref()?
558            .read(cx)
559            .metadata(handle.prompt_id.into())?
560            .title
561            .unwrap_or_else(|| "Unnamed Rule".into());
562        Some(AddedContext {
563            kind: ContextKind::Rules,
564            name: title.clone(),
565            parent: None,
566            tooltip: None,
567            icon_path: None,
568            status: ContextStatus::Ready,
569            render_hover: None,
570            handle: AgentContextHandle::Rules(handle),
571        })
572    }
573
574    fn attached_rules(context: &RulesContext) -> AddedContext {
575        let title = context
576            .title
577            .clone()
578            .unwrap_or_else(|| "Unnamed Rule".into());
579        AddedContext {
580            kind: ContextKind::Rules,
581            name: title,
582            parent: None,
583            tooltip: None,
584            icon_path: None,
585            status: ContextStatus::Ready,
586            render_hover: {
587                let text = context.text.clone();
588                Some(Rc::new(move |_, cx| {
589                    ContextPillHover::new_text(text.clone(), cx).into()
590                }))
591            },
592            handle: AgentContextHandle::Rules(context.handle.clone()),
593        }
594    }
595
596    fn image(context: ImageContext, cx: &App) -> AddedContext {
597        let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
598            let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
599            let (name, parent) =
600                extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
601            let icon_path = FileIcons::get_icon(&full_path, cx);
602            (name, parent, icon_path)
603        } else {
604            ("Image".into(), None, None)
605        };
606
607        AddedContext {
608            kind: ContextKind::Image,
609            name,
610            parent,
611            tooltip: None,
612            icon_path,
613            status: match context.status() {
614                ImageStatus::Loading => ContextStatus::Loading {
615                    message: "Loading…".into(),
616                },
617                ImageStatus::Error => ContextStatus::Error {
618                    message: "Failed to load image".into(),
619                },
620                ImageStatus::Ready => ContextStatus::Ready,
621            },
622            render_hover: Some(Rc::new({
623                let image = context.original_image.clone();
624                move |_, cx| {
625                    let image = image.clone();
626                    ContextPillHover::new(cx, move |_, _| {
627                        gpui::img(image.clone())
628                            .max_w_96()
629                            .max_h_96()
630                            .into_any_element()
631                    })
632                    .into()
633                }
634            })),
635            handle: AgentContextHandle::Image(context),
636        }
637    }
638}
639
640fn extract_file_name_and_directory_from_full_path(
641    path: &Path,
642    name_fallback: &SharedString,
643) -> (SharedString, Option<SharedString>) {
644    let name = path
645        .file_name()
646        .map(|n| n.to_string_lossy().into_owned().into())
647        .unwrap_or_else(|| name_fallback.clone());
648    let parent = path
649        .parent()
650        .and_then(|p| p.file_name())
651        .map(|n| n.to_string_lossy().into_owned().into());
652
653    (name, parent)
654}
655
656#[derive(Debug, Clone)]
657struct ContextFileExcerpt {
658    pub file_name_and_range: SharedString,
659    pub full_path_and_range: SharedString,
660    pub parent_name: Option<SharedString>,
661    pub icon_path: Option<SharedString>,
662}
663
664impl ContextFileExcerpt {
665    pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
666        let full_path_string = full_path.to_string_lossy().into_owned();
667        let file_name = full_path
668            .file_name()
669            .map(|n| n.to_string_lossy().into_owned())
670            .unwrap_or_else(|| full_path_string.clone());
671
672        let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
673        let mut full_path_and_range = full_path_string;
674        full_path_and_range.push_str(&line_range_text);
675        let mut file_name_and_range = file_name;
676        file_name_and_range.push_str(&line_range_text);
677
678        let parent_name = full_path
679            .parent()
680            .and_then(|p| p.file_name())
681            .map(|n| n.to_string_lossy().into_owned().into());
682
683        let icon_path = FileIcons::get_icon(&full_path, cx);
684
685        ContextFileExcerpt {
686            file_name_and_range: file_name_and_range.into(),
687            full_path_and_range: full_path_and_range.into(),
688            parent_name,
689            icon_path,
690        }
691    }
692
693    fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
694        let icon_path = self.icon_path.clone();
695        let full_path_and_range = self.full_path_and_range.clone();
696        ContextPillHover::new(cx, move |_, cx| {
697            v_flex()
698                .child(
699                    h_flex()
700                        .gap_0p5()
701                        .w_full()
702                        .max_w_full()
703                        .border_b_1()
704                        .border_color(cx.theme().colors().border.opacity(0.6))
705                        .children(
706                            icon_path
707                                .clone()
708                                .map(Icon::from_path)
709                                .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
710                        )
711                        .child(
712                            // TODO: make this truncate on the left.
713                            Label::new(full_path_and_range.clone())
714                                .size(LabelSize::Small)
715                                .ml_1(),
716                        ),
717                )
718                .child(
719                    div()
720                        .id("context-pill-hover-contents")
721                        .overflow_scroll()
722                        .max_w_128()
723                        .max_h_96()
724                        .child(Label::new(text.clone()).buffer_font(cx)),
725                )
726                .into_any_element()
727        })
728    }
729}
730
731struct ContextPillHover {
732    render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
733}
734
735impl ContextPillHover {
736    fn new(
737        cx: &mut App,
738        render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
739    ) -> Entity<Self> {
740        cx.new(|_| Self {
741            render_hover: Box::new(render_hover),
742        })
743    }
744
745    fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
746        Self::new(cx, move |_, _| {
747            div()
748                .id("context-pill-hover-contents")
749                .overflow_scroll()
750                .max_w_128()
751                .max_h_96()
752                .child(content.clone())
753                .into_any_element()
754        })
755    }
756}
757
758impl Render for ContextPillHover {
759    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
760        tooltip_container(window, cx, move |this, window, cx| {
761            this.occlude()
762                .on_mouse_move(|_, _, cx| cx.stop_propagation())
763                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
764                .child((self.render_hover)(window, cx))
765        })
766    }
767}
768
769impl Component for AddedContext {
770    fn scope() -> ComponentScope {
771        ComponentScope::Agent
772    }
773
774    fn sort_name() -> &'static str {
775        "AddedContext"
776    }
777
778    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
779        let mut next_context_id = ContextId::zero();
780        let image_ready = (
781            "Ready",
782            AddedContext::image(
783                ImageContext {
784                    context_id: next_context_id.post_inc(),
785                    project_path: None,
786                    full_path: None,
787                    original_image: Arc::new(Image::empty()),
788                    image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
789                },
790                cx,
791            ),
792        );
793
794        let image_loading = (
795            "Loading",
796            AddedContext::image(
797                ImageContext {
798                    context_id: next_context_id.post_inc(),
799                    project_path: None,
800                    full_path: None,
801                    original_image: Arc::new(Image::empty()),
802                    image_task: cx
803                        .background_spawn(async move {
804                            smol::Timer::after(Duration::from_secs(60 * 5)).await;
805                            Some(LanguageModelImage::empty())
806                        })
807                        .shared(),
808                },
809                cx,
810            ),
811        );
812
813        let image_error = (
814            "Error",
815            AddedContext::image(
816                ImageContext {
817                    context_id: next_context_id.post_inc(),
818                    project_path: None,
819                    full_path: None,
820                    original_image: Arc::new(Image::empty()),
821                    image_task: Task::ready(None).shared(),
822                },
823                cx,
824            ),
825        );
826
827        Some(
828            v_flex()
829                .gap_6()
830                .children(
831                    vec![image_ready, image_loading, image_error]
832                        .into_iter()
833                        .map(|(text, context)| {
834                            single_example(
835                                text,
836                                ContextPill::added(context, false, false, None).into_any_element(),
837                            )
838                        }),
839                )
840                .into_any(),
841        )
842    }
843}