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)),
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()),
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 = full_path
337            .file_name()
338            .map(|n| n.to_string_lossy().into_owned().into())
339            .unwrap_or_else(|| full_path_string.clone());
340        let parent = full_path
341            .parent()
342            .and_then(|p| p.file_name())
343            .map(|n| n.to_string_lossy().into_owned().into());
344        AddedContext {
345            kind: ContextKind::File,
346            name,
347            parent,
348            tooltip: Some(full_path_string),
349            icon_path: FileIcons::get_icon(&full_path, cx),
350            status: ContextStatus::Ready,
351            render_hover: None,
352            handle: AgentContextHandle::File(handle),
353        }
354    }
355
356    fn pending_directory(
357        handle: DirectoryContextHandle,
358        project: &Project,
359        cx: &App,
360    ) -> Option<AddedContext> {
361        let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
362        let entry = worktree.entry_for_id(handle.entry_id)?;
363        let full_path = worktree.full_path(&entry.path);
364        Some(Self::directory(handle, &full_path))
365    }
366
367    fn attached_directory(context: &DirectoryContext) -> AddedContext {
368        Self::directory(context.handle.clone(), &context.full_path)
369    }
370
371    fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
372        let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
373        let name = full_path
374            .file_name()
375            .map(|n| n.to_string_lossy().into_owned().into())
376            .unwrap_or_else(|| full_path_string.clone());
377        let parent = full_path
378            .parent()
379            .and_then(|p| p.file_name())
380            .map(|n| n.to_string_lossy().into_owned().into());
381        AddedContext {
382            kind: ContextKind::Directory,
383            name,
384            parent,
385            tooltip: Some(full_path_string),
386            icon_path: None,
387            status: ContextStatus::Ready,
388            render_hover: None,
389            handle: AgentContextHandle::Directory(handle),
390        }
391    }
392
393    fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
394        let excerpt =
395            ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
396        Some(AddedContext {
397            kind: ContextKind::Symbol,
398            name: handle.symbol.clone(),
399            parent: Some(excerpt.file_name_and_range.clone()),
400            tooltip: None,
401            icon_path: None,
402            status: ContextStatus::Ready,
403            render_hover: {
404                let handle = handle.clone();
405                Some(Rc::new(move |_, cx| {
406                    excerpt.hover_view(handle.text(cx), cx).into()
407                }))
408            },
409            handle: AgentContextHandle::Symbol(handle),
410        })
411    }
412
413    fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
414        let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
415        AddedContext {
416            kind: ContextKind::Symbol,
417            name: context.handle.symbol.clone(),
418            parent: Some(excerpt.file_name_and_range.clone()),
419            tooltip: None,
420            icon_path: None,
421            status: ContextStatus::Ready,
422            render_hover: {
423                let text = context.text.clone();
424                Some(Rc::new(move |_, cx| {
425                    excerpt.hover_view(text.clone(), cx).into()
426                }))
427            },
428            handle: AgentContextHandle::Symbol(context.handle.clone()),
429        }
430    }
431
432    fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
433        let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
434        Some(AddedContext {
435            kind: ContextKind::Selection,
436            name: excerpt.file_name_and_range.clone(),
437            parent: excerpt.parent_name.clone(),
438            tooltip: None,
439            icon_path: excerpt.icon_path.clone(),
440            status: ContextStatus::Ready,
441            render_hover: {
442                let handle = handle.clone();
443                Some(Rc::new(move |_, cx| {
444                    excerpt.hover_view(handle.text(cx), cx).into()
445                }))
446            },
447            handle: AgentContextHandle::Selection(handle),
448        })
449    }
450
451    fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
452        let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
453        AddedContext {
454            kind: ContextKind::Selection,
455            name: excerpt.file_name_and_range.clone(),
456            parent: excerpt.parent_name.clone(),
457            tooltip: None,
458            icon_path: excerpt.icon_path.clone(),
459            status: ContextStatus::Ready,
460            render_hover: {
461                let text = context.text.clone();
462                Some(Rc::new(move |_, cx| {
463                    excerpt.hover_view(text.clone(), cx).into()
464                }))
465            },
466            handle: AgentContextHandle::Selection(context.handle.clone()),
467        }
468    }
469
470    fn fetched_url(context: FetchedUrlContext) -> AddedContext {
471        AddedContext {
472            kind: ContextKind::FetchedUrl,
473            name: context.url.clone(),
474            parent: None,
475            tooltip: None,
476            icon_path: None,
477            status: ContextStatus::Ready,
478            render_hover: None,
479            handle: AgentContextHandle::FetchedUrl(context),
480        }
481    }
482
483    fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
484        AddedContext {
485            kind: ContextKind::Thread,
486            name: handle.title(cx),
487            parent: None,
488            tooltip: None,
489            icon_path: None,
490            status: if handle.thread.read(cx).is_generating_detailed_summary() {
491                ContextStatus::Loading {
492                    message: "Summarizing…".into(),
493                }
494            } else {
495                ContextStatus::Ready
496            },
497            render_hover: {
498                let thread = handle.thread.clone();
499                Some(Rc::new(move |_, cx| {
500                    let text = thread.read(cx).latest_detailed_summary_or_text();
501                    ContextPillHover::new_text(text.clone(), cx).into()
502                }))
503            },
504            handle: AgentContextHandle::Thread(handle),
505        }
506    }
507
508    fn attached_thread(context: &ThreadContext) -> AddedContext {
509        AddedContext {
510            kind: ContextKind::Thread,
511            name: context.title.clone(),
512            parent: None,
513            tooltip: None,
514            icon_path: None,
515            status: ContextStatus::Ready,
516            render_hover: {
517                let text = context.text.clone();
518                Some(Rc::new(move |_, cx| {
519                    ContextPillHover::new_text(text.clone(), cx).into()
520                }))
521            },
522            handle: AgentContextHandle::Thread(context.handle.clone()),
523        }
524    }
525
526    fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
527        AddedContext {
528            kind: ContextKind::TextThread,
529            name: handle.title(cx),
530            parent: None,
531            tooltip: None,
532            icon_path: None,
533            status: ContextStatus::Ready,
534            render_hover: {
535                let context = handle.context.clone();
536                Some(Rc::new(move |_, cx| {
537                    let text = context.read(cx).to_xml(cx);
538                    ContextPillHover::new_text(text.into(), cx).into()
539                }))
540            },
541            handle: AgentContextHandle::TextThread(handle),
542        }
543    }
544
545    fn attached_text_thread(context: &TextThreadContext) -> AddedContext {
546        AddedContext {
547            kind: ContextKind::TextThread,
548            name: context.title.clone(),
549            parent: None,
550            tooltip: None,
551            icon_path: None,
552            status: ContextStatus::Ready,
553            render_hover: {
554                let text = context.text.clone();
555                Some(Rc::new(move |_, cx| {
556                    ContextPillHover::new_text(text.clone(), cx).into()
557                }))
558            },
559            handle: AgentContextHandle::TextThread(context.handle.clone()),
560        }
561    }
562
563    fn pending_rules(
564        handle: RulesContextHandle,
565        prompt_store: Option<&Entity<PromptStore>>,
566        cx: &App,
567    ) -> Option<AddedContext> {
568        let title = prompt_store
569            .as_ref()?
570            .read(cx)
571            .metadata(handle.prompt_id.into())?
572            .title
573            .unwrap_or_else(|| "Unnamed Rule".into());
574        Some(AddedContext {
575            kind: ContextKind::Rules,
576            name: title.clone(),
577            parent: None,
578            tooltip: None,
579            icon_path: None,
580            status: ContextStatus::Ready,
581            render_hover: None,
582            handle: AgentContextHandle::Rules(handle),
583        })
584    }
585
586    fn attached_rules(context: &RulesContext) -> AddedContext {
587        let title = context
588            .title
589            .clone()
590            .unwrap_or_else(|| "Unnamed Rule".into());
591        AddedContext {
592            kind: ContextKind::Rules,
593            name: title,
594            parent: None,
595            tooltip: None,
596            icon_path: None,
597            status: ContextStatus::Ready,
598            render_hover: {
599                let text = context.text.clone();
600                Some(Rc::new(move |_, cx| {
601                    ContextPillHover::new_text(text.clone(), cx).into()
602                }))
603            },
604            handle: AgentContextHandle::Rules(context.handle.clone()),
605        }
606    }
607
608    fn image(context: ImageContext) -> AddedContext {
609        AddedContext {
610            kind: ContextKind::Image,
611            name: "Image".into(),
612            parent: None,
613            tooltip: None,
614            icon_path: None,
615            status: match context.status() {
616                ImageStatus::Loading => ContextStatus::Loading {
617                    message: "Loading…".into(),
618                },
619                ImageStatus::Error => ContextStatus::Error {
620                    message: "Failed to load image".into(),
621                },
622                ImageStatus::Ready => ContextStatus::Ready,
623            },
624            render_hover: Some(Rc::new({
625                let image = context.original_image.clone();
626                move |_, cx| {
627                    let image = image.clone();
628                    ContextPillHover::new(cx, move |_, _| {
629                        gpui::img(image.clone())
630                            .max_w_96()
631                            .max_h_96()
632                            .into_any_element()
633                    })
634                    .into()
635                }
636            })),
637            handle: AgentContextHandle::Image(context),
638        }
639    }
640}
641
642#[derive(Debug, Clone)]
643struct ContextFileExcerpt {
644    pub file_name_and_range: SharedString,
645    pub full_path_and_range: SharedString,
646    pub parent_name: Option<SharedString>,
647    pub icon_path: Option<SharedString>,
648}
649
650impl ContextFileExcerpt {
651    pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
652        let full_path_string = full_path.to_string_lossy().into_owned();
653        let file_name = full_path
654            .file_name()
655            .map(|n| n.to_string_lossy().into_owned())
656            .unwrap_or_else(|| full_path_string.clone());
657
658        let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
659        let mut full_path_and_range = full_path_string;
660        full_path_and_range.push_str(&line_range_text);
661        let mut file_name_and_range = file_name;
662        file_name_and_range.push_str(&line_range_text);
663
664        let parent_name = full_path
665            .parent()
666            .and_then(|p| p.file_name())
667            .map(|n| n.to_string_lossy().into_owned().into());
668
669        let icon_path = FileIcons::get_icon(&full_path, cx);
670
671        ContextFileExcerpt {
672            file_name_and_range: file_name_and_range.into(),
673            full_path_and_range: full_path_and_range.into(),
674            parent_name,
675            icon_path,
676        }
677    }
678
679    fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
680        let icon_path = self.icon_path.clone();
681        let full_path_and_range = self.full_path_and_range.clone();
682        ContextPillHover::new(cx, move |_, cx| {
683            v_flex()
684                .child(
685                    h_flex()
686                        .gap_0p5()
687                        .w_full()
688                        .max_w_full()
689                        .border_b_1()
690                        .border_color(cx.theme().colors().border.opacity(0.6))
691                        .children(
692                            icon_path
693                                .clone()
694                                .map(Icon::from_path)
695                                .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
696                        )
697                        .child(
698                            // TODO: make this truncate on the left.
699                            Label::new(full_path_and_range.clone())
700                                .size(LabelSize::Small)
701                                .ml_1(),
702                        ),
703                )
704                .child(
705                    div()
706                        .id("context-pill-hover-contents")
707                        .overflow_scroll()
708                        .max_w_128()
709                        .max_h_96()
710                        .child(Label::new(text.clone()).buffer_font(cx)),
711                )
712                .into_any_element()
713        })
714    }
715}
716
717struct ContextPillHover {
718    render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
719}
720
721impl ContextPillHover {
722    fn new(
723        cx: &mut App,
724        render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
725    ) -> Entity<Self> {
726        cx.new(|_| Self {
727            render_hover: Box::new(render_hover),
728        })
729    }
730
731    fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
732        Self::new(cx, move |_, _| {
733            div()
734                .id("context-pill-hover-contents")
735                .overflow_scroll()
736                .max_w_128()
737                .max_h_96()
738                .child(content.clone())
739                .into_any_element()
740        })
741    }
742}
743
744impl Render for ContextPillHover {
745    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
746        tooltip_container(window, cx, move |this, window, cx| {
747            this.occlude()
748                .on_mouse_move(|_, _, cx| cx.stop_propagation())
749                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
750                .child((self.render_hover)(window, cx))
751        })
752    }
753}
754
755impl Component for AddedContext {
756    fn scope() -> ComponentScope {
757        ComponentScope::Agent
758    }
759
760    fn sort_name() -> &'static str {
761        "AddedContext"
762    }
763
764    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
765        let mut next_context_id = ContextId::zero();
766        let image_ready = (
767            "Ready",
768            AddedContext::image(ImageContext {
769                context_id: next_context_id.post_inc(),
770                project_path: None,
771                original_image: Arc::new(Image::empty()),
772                image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
773            }),
774        );
775
776        let image_loading = (
777            "Loading",
778            AddedContext::image(ImageContext {
779                context_id: next_context_id.post_inc(),
780                project_path: None,
781                original_image: Arc::new(Image::empty()),
782                image_task: cx
783                    .background_spawn(async move {
784                        smol::Timer::after(Duration::from_secs(60 * 5)).await;
785                        Some(LanguageModelImage::empty())
786                    })
787                    .shared(),
788            }),
789        );
790
791        let image_error = (
792            "Error",
793            AddedContext::image(ImageContext {
794                context_id: next_context_id.post_inc(),
795                project_path: None,
796                original_image: Arc::new(Image::empty()),
797                image_task: Task::ready(None).shared(),
798            }),
799        );
800
801        Some(
802            v_flex()
803                .gap_6()
804                .children(
805                    vec![image_ready, image_loading, image_error]
806                        .into_iter()
807                        .map(|(text, context)| {
808                            single_example(
809                                text,
810                                ContextPill::added(context, false, false, None).into_any_element(),
811                            )
812                        }),
813                )
814                .into_any(),
815        )
816    }
817}