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