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}