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
220                            .cursor_pointer()
221                            .on_click(move |event, window, cx| on_click(event, window, cx))
222                    })
223                    .into_any_element()
224            }
225            ContextPill::Suggested {
226                name,
227                icon_path: _,
228                kind: _,
229                focused,
230                on_click,
231            } => base_pill
232                .cursor_pointer()
233                .pr_1()
234                .border_dashed()
235                .map(|pill| {
236                    if *focused {
237                        pill.border_color(color.border_focused)
238                            .bg(color.element_background.opacity(0.5))
239                    } else {
240                        pill.border_color(color.border)
241                    }
242                })
243                .hover(|style| style.bg(color.element_hover.opacity(0.5)))
244                .child(
245                    div().max_w_64().child(
246                        Label::new(name.clone())
247                            .size(LabelSize::Small)
248                            .color(Color::Muted)
249                            .truncate(),
250                    ),
251                )
252                .tooltip(|window, cx| {
253                    Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
254                })
255                .when_some(on_click.as_ref(), |element, on_click| {
256                    let on_click = on_click.clone();
257                    element.on_click(move |event, window, cx| on_click(event, window, cx))
258                })
259                .into_any(),
260        }
261    }
262}
263
264pub enum ContextStatus {
265    Ready,
266    Loading { message: SharedString },
267    Error { message: SharedString },
268}
269
270#[derive(RegisterComponent)]
271pub struct AddedContext {
272    pub handle: AgentContextHandle,
273    pub kind: ContextKind,
274    pub name: SharedString,
275    pub parent: Option<SharedString>,
276    pub tooltip: Option<SharedString>,
277    pub icon_path: Option<SharedString>,
278    pub status: ContextStatus,
279    pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
280}
281
282impl AddedContext {
283    /// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
284    /// `None` if `DirectoryContext` or `RulesContext` no longer exist.
285    ///
286    /// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
287    pub fn new_pending(
288        handle: AgentContextHandle,
289        prompt_store: Option<&Entity<PromptStore>>,
290        project: &Project,
291        cx: &App,
292    ) -> Option<AddedContext> {
293        match handle {
294            AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
295            AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
296            AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
297            AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
298            AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
299            AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
300            AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
301            AgentContextHandle::Image(handle) => Some(Self::image(handle)),
302        }
303    }
304
305    pub fn new_attached(context: &AgentContext, cx: &App) -> AddedContext {
306        match context {
307            AgentContext::File(context) => Self::attached_file(context, cx),
308            AgentContext::Directory(context) => Self::attached_directory(context),
309            AgentContext::Symbol(context) => Self::attached_symbol(context, cx),
310            AgentContext::Selection(context) => Self::attached_selection(context, cx),
311            AgentContext::FetchedUrl(context) => Self::fetched_url(context.clone()),
312            AgentContext::Thread(context) => Self::attached_thread(context),
313            AgentContext::Rules(context) => Self::attached_rules(context),
314            AgentContext::Image(context) => Self::image(context.clone()),
315        }
316    }
317
318    fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
319        let full_path = handle.buffer.read(cx).file()?.full_path(cx);
320        Some(Self::file(handle, &full_path, cx))
321    }
322
323    fn attached_file(context: &FileContext, cx: &App) -> AddedContext {
324        Self::file(context.handle.clone(), &context.full_path, cx)
325    }
326
327    fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
328        let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
329        let name = full_path
330            .file_name()
331            .map(|n| n.to_string_lossy().into_owned().into())
332            .unwrap_or_else(|| full_path_string.clone());
333        let parent = full_path
334            .parent()
335            .and_then(|p| p.file_name())
336            .map(|n| n.to_string_lossy().into_owned().into());
337        AddedContext {
338            kind: ContextKind::File,
339            name,
340            parent,
341            tooltip: Some(full_path_string),
342            icon_path: FileIcons::get_icon(&full_path, cx),
343            status: ContextStatus::Ready,
344            render_hover: None,
345            handle: AgentContextHandle::File(handle),
346        }
347    }
348
349    fn pending_directory(
350        handle: DirectoryContextHandle,
351        project: &Project,
352        cx: &App,
353    ) -> Option<AddedContext> {
354        let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
355        let entry = worktree.entry_for_id(handle.entry_id)?;
356        let full_path = worktree.full_path(&entry.path);
357        Some(Self::directory(handle, &full_path))
358    }
359
360    fn attached_directory(context: &DirectoryContext) -> AddedContext {
361        Self::directory(context.handle.clone(), &context.full_path)
362    }
363
364    fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
365        let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
366        let name = full_path
367            .file_name()
368            .map(|n| n.to_string_lossy().into_owned().into())
369            .unwrap_or_else(|| full_path_string.clone());
370        let parent = full_path
371            .parent()
372            .and_then(|p| p.file_name())
373            .map(|n| n.to_string_lossy().into_owned().into());
374        AddedContext {
375            kind: ContextKind::Directory,
376            name,
377            parent,
378            tooltip: Some(full_path_string),
379            icon_path: None,
380            status: ContextStatus::Ready,
381            render_hover: None,
382            handle: AgentContextHandle::Directory(handle),
383        }
384    }
385
386    fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
387        let excerpt =
388            ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
389        Some(AddedContext {
390            kind: ContextKind::Symbol,
391            name: handle.symbol.clone(),
392            parent: Some(excerpt.file_name_and_range.clone()),
393            tooltip: None,
394            icon_path: None,
395            status: ContextStatus::Ready,
396            render_hover: {
397                let handle = handle.clone();
398                Some(Rc::new(move |_, cx| {
399                    excerpt.hover_view(handle.text(cx), cx).into()
400                }))
401            },
402            handle: AgentContextHandle::Symbol(handle),
403        })
404    }
405
406    fn attached_symbol(context: &SymbolContext, cx: &App) -> AddedContext {
407        let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
408        AddedContext {
409            kind: ContextKind::Symbol,
410            name: context.handle.symbol.clone(),
411            parent: Some(excerpt.file_name_and_range.clone()),
412            tooltip: None,
413            icon_path: None,
414            status: ContextStatus::Ready,
415            render_hover: {
416                let text = context.text.clone();
417                Some(Rc::new(move |_, cx| {
418                    excerpt.hover_view(text.clone(), cx).into()
419                }))
420            },
421            handle: AgentContextHandle::Symbol(context.handle.clone()),
422        }
423    }
424
425    fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
426        let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
427        Some(AddedContext {
428            kind: ContextKind::Selection,
429            name: excerpt.file_name_and_range.clone(),
430            parent: excerpt.parent_name.clone(),
431            tooltip: None,
432            icon_path: excerpt.icon_path.clone(),
433            status: ContextStatus::Ready,
434            render_hover: {
435                let handle = handle.clone();
436                Some(Rc::new(move |_, cx| {
437                    excerpt.hover_view(handle.text(cx), cx).into()
438                }))
439            },
440            handle: AgentContextHandle::Selection(handle),
441        })
442    }
443
444    fn attached_selection(context: &SelectionContext, cx: &App) -> AddedContext {
445        let excerpt = ContextFileExcerpt::new(&context.full_path, context.line_range.clone(), cx);
446        AddedContext {
447            kind: ContextKind::Selection,
448            name: excerpt.file_name_and_range.clone(),
449            parent: excerpt.parent_name.clone(),
450            tooltip: None,
451            icon_path: excerpt.icon_path.clone(),
452            status: ContextStatus::Ready,
453            render_hover: {
454                let text = context.text.clone();
455                Some(Rc::new(move |_, cx| {
456                    excerpt.hover_view(text.clone(), cx).into()
457                }))
458            },
459            handle: AgentContextHandle::Selection(context.handle.clone()),
460        }
461    }
462
463    fn fetched_url(context: FetchedUrlContext) -> AddedContext {
464        AddedContext {
465            kind: ContextKind::FetchedUrl,
466            name: context.url.clone(),
467            parent: None,
468            tooltip: None,
469            icon_path: None,
470            status: ContextStatus::Ready,
471            render_hover: None,
472            handle: AgentContextHandle::FetchedUrl(context),
473        }
474    }
475
476    fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
477        AddedContext {
478            kind: ContextKind::Thread,
479            name: handle.title(cx),
480            parent: None,
481            tooltip: None,
482            icon_path: None,
483            status: if handle.thread.read(cx).is_generating_detailed_summary() {
484                ContextStatus::Loading {
485                    message: "Summarizing…".into(),
486                }
487            } else {
488                ContextStatus::Ready
489            },
490            render_hover: {
491                let thread = handle.thread.clone();
492                Some(Rc::new(move |_, cx| {
493                    let text = thread.read(cx).latest_detailed_summary_or_text();
494                    ContextPillHover::new_text(text.clone(), cx).into()
495                }))
496            },
497            handle: AgentContextHandle::Thread(handle),
498        }
499    }
500
501    fn attached_thread(context: &ThreadContext) -> AddedContext {
502        AddedContext {
503            kind: ContextKind::Thread,
504            name: context.title.clone(),
505            parent: None,
506            tooltip: None,
507            icon_path: None,
508            status: ContextStatus::Ready,
509            render_hover: {
510                let text = context.text.clone();
511                Some(Rc::new(move |_, cx| {
512                    ContextPillHover::new_text(text.clone(), cx).into()
513                }))
514            },
515            handle: AgentContextHandle::Thread(context.handle.clone()),
516        }
517    }
518
519    fn pending_rules(
520        handle: RulesContextHandle,
521        prompt_store: Option<&Entity<PromptStore>>,
522        cx: &App,
523    ) -> Option<AddedContext> {
524        let title = prompt_store
525            .as_ref()?
526            .read(cx)
527            .metadata(handle.prompt_id.into())?
528            .title
529            .unwrap_or_else(|| "Unnamed Rule".into());
530        Some(AddedContext {
531            kind: ContextKind::Rules,
532            name: title.clone(),
533            parent: None,
534            tooltip: None,
535            icon_path: None,
536            status: ContextStatus::Ready,
537            render_hover: None,
538            handle: AgentContextHandle::Rules(handle),
539        })
540    }
541
542    fn attached_rules(context: &RulesContext) -> AddedContext {
543        let title = context
544            .title
545            .clone()
546            .unwrap_or_else(|| "Unnamed Rule".into());
547        AddedContext {
548            kind: ContextKind::Rules,
549            name: title,
550            parent: None,
551            tooltip: None,
552            icon_path: None,
553            status: ContextStatus::Ready,
554            render_hover: {
555                let text = context.text.clone();
556                Some(Rc::new(move |_, cx| {
557                    ContextPillHover::new_text(text.clone(), cx).into()
558                }))
559            },
560            handle: AgentContextHandle::Rules(context.handle.clone()),
561        }
562    }
563
564    fn image(context: ImageContext) -> AddedContext {
565        AddedContext {
566            kind: ContextKind::Image,
567            name: "Image".into(),
568            parent: None,
569            tooltip: None,
570            icon_path: None,
571            status: match context.status() {
572                ImageStatus::Loading => ContextStatus::Loading {
573                    message: "Loading…".into(),
574                },
575                ImageStatus::Error => ContextStatus::Error {
576                    message: "Failed to load image".into(),
577                },
578                ImageStatus::Ready => ContextStatus::Ready,
579            },
580            render_hover: Some(Rc::new({
581                let image = context.original_image.clone();
582                move |_, cx| {
583                    let image = image.clone();
584                    ContextPillHover::new(cx, move |_, _| {
585                        gpui::img(image.clone())
586                            .max_w_96()
587                            .max_h_96()
588                            .into_any_element()
589                    })
590                    .into()
591                }
592            })),
593            handle: AgentContextHandle::Image(context),
594        }
595    }
596}
597
598#[derive(Debug, Clone)]
599struct ContextFileExcerpt {
600    pub file_name_and_range: SharedString,
601    pub full_path_and_range: SharedString,
602    pub parent_name: Option<SharedString>,
603    pub icon_path: Option<SharedString>,
604}
605
606impl ContextFileExcerpt {
607    pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
608        let full_path_string = full_path.to_string_lossy().into_owned();
609        let file_name = full_path
610            .file_name()
611            .map(|n| n.to_string_lossy().into_owned())
612            .unwrap_or_else(|| full_path_string.clone());
613
614        let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
615        let mut full_path_and_range = full_path_string;
616        full_path_and_range.push_str(&line_range_text);
617        let mut file_name_and_range = file_name;
618        file_name_and_range.push_str(&line_range_text);
619
620        let parent_name = full_path
621            .parent()
622            .and_then(|p| p.file_name())
623            .map(|n| n.to_string_lossy().into_owned().into());
624
625        let icon_path = FileIcons::get_icon(&full_path, cx);
626
627        ContextFileExcerpt {
628            file_name_and_range: file_name_and_range.into(),
629            full_path_and_range: full_path_and_range.into(),
630            parent_name,
631            icon_path,
632        }
633    }
634
635    fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
636        let icon_path = self.icon_path.clone();
637        let full_path_and_range = self.full_path_and_range.clone();
638        ContextPillHover::new(cx, move |_, cx| {
639            v_flex()
640                .child(
641                    h_flex()
642                        .gap_0p5()
643                        .w_full()
644                        .max_w_full()
645                        .border_b_1()
646                        .border_color(cx.theme().colors().border.opacity(0.6))
647                        .children(
648                            icon_path
649                                .clone()
650                                .map(Icon::from_path)
651                                .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
652                        )
653                        .child(
654                            // TODO: make this truncate on the left.
655                            Label::new(full_path_and_range.clone())
656                                .size(LabelSize::Small)
657                                .ml_1(),
658                        ),
659                )
660                .child(
661                    div()
662                        .id("context-pill-hover-contents")
663                        .overflow_scroll()
664                        .max_w_128()
665                        .max_h_96()
666                        .child(Label::new(text.clone()).buffer_font(cx)),
667                )
668                .into_any_element()
669        })
670    }
671}
672
673struct ContextPillHover {
674    render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
675}
676
677impl ContextPillHover {
678    fn new(
679        cx: &mut App,
680        render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
681    ) -> Entity<Self> {
682        cx.new(|_| Self {
683            render_hover: Box::new(render_hover),
684        })
685    }
686
687    fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
688        Self::new(cx, move |_, _| {
689            div()
690                .id("context-pill-hover-contents")
691                .overflow_scroll()
692                .max_w_128()
693                .max_h_96()
694                .child(content.clone())
695                .into_any_element()
696        })
697    }
698}
699
700impl Render for ContextPillHover {
701    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
702        tooltip_container(window, cx, move |this, window, cx| {
703            this.occlude()
704                .on_mouse_move(|_, _, cx| cx.stop_propagation())
705                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
706                .child((self.render_hover)(window, cx))
707        })
708    }
709}
710
711impl Component for AddedContext {
712    fn scope() -> ComponentScope {
713        ComponentScope::Agent
714    }
715
716    fn sort_name() -> &'static str {
717        "AddedContext"
718    }
719
720    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
721        let mut next_context_id = ContextId::zero();
722        let image_ready = (
723            "Ready",
724            AddedContext::image(ImageContext {
725                context_id: next_context_id.post_inc(),
726                original_image: Arc::new(Image::empty()),
727                image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
728            }),
729        );
730
731        let image_loading = (
732            "Loading",
733            AddedContext::image(ImageContext {
734                context_id: next_context_id.post_inc(),
735                original_image: Arc::new(Image::empty()),
736                image_task: cx
737                    .background_spawn(async move {
738                        smol::Timer::after(Duration::from_secs(60 * 5)).await;
739                        Some(LanguageModelImage::empty())
740                    })
741                    .shared(),
742            }),
743        );
744
745        let image_error = (
746            "Error",
747            AddedContext::image(ImageContext {
748                context_id: next_context_id.post_inc(),
749                original_image: Arc::new(Image::empty()),
750                image_task: Task::ready(None).shared(),
751            }),
752        );
753
754        Some(
755            v_flex()
756                .gap_6()
757                .children(
758                    vec![image_ready, image_loading, image_error]
759                        .into_iter()
760                        .map(|(text, context)| {
761                            single_example(
762                                text,
763                                ContextPill::added(context, false, false, None).into_any_element(),
764                            )
765                        }),
766                )
767                .into_any(),
768        )
769    }
770}