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