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::{Tooltip, prelude::*, tooltip_container};
14
15use agent::context::{
16 AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
17 FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
18 SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
19};
20use util::paths::PathStyle;
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 } => Icon::from_path(icon_path),
96 Self::Suggested { kind, .. } => Icon::new(kind.icon()),
97 Self::Added { context, .. } => context.icon(),
98 }
99 }
100}
101
102impl RenderOnce for ContextPill {
103 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
104 let color = cx.theme().colors();
105
106 let base_pill = h_flex()
107 .id(self.id())
108 .pl_1()
109 .pb(px(1.))
110 .border_1()
111 .rounded_sm()
112 .gap_1()
113 .child(self.icon().size(IconSize::XSmall).color(Color::Muted));
114
115 match &self {
116 ContextPill::Added {
117 context,
118 dupe_name,
119 on_remove,
120 focused,
121 on_click,
122 } => {
123 let status_is_error = matches!(context.status, ContextStatus::Error { .. });
124 let status_is_warning = matches!(context.status, ContextStatus::Warning { .. });
125
126 base_pill
127 .pr(if on_remove.is_some() { px(2.) } else { px(4.) })
128 .map(|pill| {
129 if status_is_error {
130 pill.bg(cx.theme().status().error_background)
131 .border_color(cx.theme().status().error_border)
132 } else if status_is_warning {
133 pill.bg(cx.theme().status().warning_background)
134 .border_color(cx.theme().status().warning_border)
135 } else if *focused {
136 pill.bg(color.element_background)
137 .border_color(color.border_focused)
138 } else {
139 pill.bg(color.element_background)
140 .border_color(color.border.opacity(0.5))
141 }
142 })
143 .child(
144 h_flex()
145 .id("context-data")
146 .gap_1()
147 .child(
148 div().max_w_64().child(
149 Label::new(context.name.clone())
150 .size(LabelSize::Small)
151 .truncate(),
152 ),
153 )
154 .when_some(context.parent.as_ref(), |element, parent_name| {
155 if *dupe_name {
156 element.child(
157 Label::new(parent_name.clone())
158 .size(LabelSize::XSmall)
159 .color(Color::Muted),
160 )
161 } else {
162 element
163 }
164 })
165 .when_some(context.tooltip.as_ref(), |element, tooltip| {
166 element.tooltip(Tooltip::text(tooltip.clone()))
167 })
168 .map(|element| match &context.status {
169 ContextStatus::Ready => element
170 .when_some(
171 context.render_hover.as_ref(),
172 |element, render_hover| {
173 let render_hover = render_hover.clone();
174 element.hoverable_tooltip(move |window, cx| {
175 render_hover(window, cx)
176 })
177 },
178 )
179 .into_any(),
180 ContextStatus::Loading { message } => element
181 .tooltip(ui::Tooltip::text(message.clone()))
182 .with_animation(
183 "pulsating-ctx-pill",
184 Animation::new(Duration::from_secs(2))
185 .repeat()
186 .with_easing(pulsating_between(0.4, 0.8)),
187 |label, delta| label.opacity(delta),
188 )
189 .into_any_element(),
190 ContextStatus::Warning { message }
191 | ContextStatus::Error { message } => element
192 .tooltip(ui::Tooltip::text(message.clone()))
193 .into_any_element(),
194 }),
195 )
196 .when_some(on_remove.as_ref(), |element, on_remove| {
197 element.child(
198 IconButton::new(
199 context.handle.element_id("remove".into()),
200 IconName::Close,
201 )
202 .icon_size(IconSize::XSmall)
203 .tooltip(Tooltip::text("Remove Context"))
204 .on_click({
205 let on_remove = on_remove.clone();
206 move |event, window, cx| on_remove(event, window, cx)
207 }),
208 )
209 })
210 .when_some(on_click.as_ref(), |element, on_click| {
211 let on_click = on_click.clone();
212 element.cursor_pointer().on_click(move |event, window, cx| {
213 on_click(event, window, cx);
214 cx.stop_propagation();
215 })
216 })
217 .into_any_element()
218 }
219 ContextPill::Suggested {
220 name,
221 icon_path: _,
222 kind: _,
223 focused,
224 on_click,
225 } => base_pill
226 .cursor_pointer()
227 .pr_1()
228 .border_dashed()
229 .map(|pill| {
230 if *focused {
231 pill.border_color(color.border_focused)
232 .bg(color.element_background.opacity(0.5))
233 } else {
234 pill.border_color(color.border)
235 }
236 })
237 .hover(|style| style.bg(color.element_hover.opacity(0.5)))
238 .child(
239 div().max_w_64().child(
240 Label::new(name.clone())
241 .size(LabelSize::Small)
242 .color(Color::Muted)
243 .truncate(),
244 ),
245 )
246 .tooltip(|window, cx| {
247 Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
248 })
249 .when_some(on_click.as_ref(), |element, on_click| {
250 let on_click = on_click.clone();
251 element.on_click(move |event, window, cx| {
252 on_click(event, window, cx);
253 cx.stop_propagation();
254 })
255 })
256 .into_any(),
257 }
258 }
259}
260
261pub enum ContextStatus {
262 Ready,
263 Loading { message: SharedString },
264 Error { message: SharedString },
265 Warning { message: SharedString },
266}
267
268#[derive(RegisterComponent)]
269pub struct AddedContext {
270 pub handle: AgentContextHandle,
271 pub kind: ContextKind,
272 pub name: SharedString,
273 pub parent: Option<SharedString>,
274 pub tooltip: Option<SharedString>,
275 pub icon_path: Option<SharedString>,
276 pub status: ContextStatus,
277 pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
278}
279
280impl AddedContext {
281 pub fn icon(&self) -> Icon {
282 match &self.status {
283 ContextStatus::Warning { .. } => Icon::new(IconName::Warning).color(Color::Warning),
284 ContextStatus::Error { .. } => Icon::new(IconName::XCircle).color(Color::Error),
285 _ => {
286 if let Some(icon_path) = &self.icon_path {
287 Icon::from_path(icon_path)
288 } else {
289 Icon::new(self.kind.icon())
290 }
291 }
292 }
293 }
294 /// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
295 /// `None` if `DirectoryContext` or `RulesContext` no longer exist.
296 ///
297 /// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
298 pub fn new_pending(
299 handle: AgentContextHandle,
300 prompt_store: Option<&Entity<PromptStore>>,
301 project: &Project,
302 model: Option<&Arc<dyn language_model::LanguageModel>>,
303 cx: &App,
304 ) -> Option<AddedContext> {
305 match handle {
306 AgentContextHandle::File(handle) => {
307 Self::pending_file(handle, project.path_style(cx), cx)
308 }
309 AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
310 AgentContextHandle::Symbol(handle) => {
311 Self::pending_symbol(handle, project.path_style(cx), cx)
312 }
313 AgentContextHandle::Selection(handle) => {
314 Self::pending_selection(handle, project.path_style(cx), cx)
315 }
316 AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
317 AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
318 AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
319 AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
320 AgentContextHandle::Image(handle) => {
321 Some(Self::image(handle, model, project.path_style(cx), cx))
322 }
323 }
324 }
325
326 fn pending_file(
327 handle: FileContextHandle,
328 path_style: PathStyle,
329 cx: &App,
330 ) -> Option<AddedContext> {
331 let full_path = handle
332 .buffer
333 .read(cx)
334 .file()?
335 .full_path(cx)
336 .to_string_lossy()
337 .to_string();
338 Some(Self::file(handle, &full_path, path_style, cx))
339 }
340
341 fn file(
342 handle: FileContextHandle,
343 full_path: &str,
344 path_style: PathStyle,
345 cx: &App,
346 ) -> AddedContext {
347 let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
348 AddedContext {
349 kind: ContextKind::File,
350 name,
351 parent,
352 tooltip: Some(SharedString::new(full_path)),
353 icon_path: FileIcons::get_icon(Path::new(full_path), cx),
354 status: ContextStatus::Ready,
355 render_hover: None,
356 handle: AgentContextHandle::File(handle),
357 }
358 }
359
360 fn pending_directory(
361 handle: DirectoryContextHandle,
362 project: &Project,
363 cx: &App,
364 ) -> Option<AddedContext> {
365 let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
366 let entry = worktree.entry_for_id(handle.entry_id)?;
367 let full_path = worktree
368 .full_path(&entry.path)
369 .to_string_lossy()
370 .to_string();
371 Some(Self::directory(handle, &full_path, project.path_style(cx)))
372 }
373
374 fn directory(
375 handle: DirectoryContextHandle,
376 full_path: &str,
377 path_style: PathStyle,
378 ) -> AddedContext {
379 let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
380 AddedContext {
381 kind: ContextKind::Directory,
382 name,
383 parent,
384 tooltip: Some(SharedString::new(full_path)),
385 icon_path: None,
386 status: ContextStatus::Ready,
387 render_hover: None,
388 handle: AgentContextHandle::Directory(handle),
389 }
390 }
391
392 fn pending_symbol(
393 handle: SymbolContextHandle,
394 path_style: PathStyle,
395 cx: &App,
396 ) -> Option<AddedContext> {
397 let excerpt = ContextFileExcerpt::new(
398 &handle.full_path(cx)?.to_string_lossy(),
399 handle.enclosing_line_range(cx),
400 path_style,
401 cx,
402 );
403 Some(AddedContext {
404 kind: ContextKind::Symbol,
405 name: 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 handle = handle.clone();
412 Some(Rc::new(move |_, cx| {
413 excerpt.hover_view(handle.text(cx), cx).into()
414 }))
415 },
416 handle: AgentContextHandle::Symbol(handle),
417 })
418 }
419
420 fn pending_selection(
421 handle: SelectionContextHandle,
422 path_style: PathStyle,
423 cx: &App,
424 ) -> Option<AddedContext> {
425 let excerpt = ContextFileExcerpt::new(
426 &handle.full_path(cx)?.to_string_lossy(),
427 handle.line_range(cx),
428 path_style,
429 cx,
430 );
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 fetched_url(context: FetchedUrlContext) -> AddedContext {
449 AddedContext {
450 kind: ContextKind::FetchedUrl,
451 name: context.url.clone(),
452 parent: None,
453 tooltip: None,
454 icon_path: None,
455 status: ContextStatus::Ready,
456 render_hover: None,
457 handle: AgentContextHandle::FetchedUrl(context),
458 }
459 }
460
461 fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
462 AddedContext {
463 kind: ContextKind::Thread,
464 name: handle.title(cx),
465 parent: None,
466 tooltip: None,
467 icon_path: None,
468 status: if handle.thread.read(cx).is_generating_detailed_summary() {
469 ContextStatus::Loading {
470 message: "Summarizing…".into(),
471 }
472 } else {
473 ContextStatus::Ready
474 },
475 render_hover: {
476 let thread = handle.thread.clone();
477 Some(Rc::new(move |_, cx| {
478 let text = thread.read(cx).latest_detailed_summary_or_text();
479 ContextPillHover::new_text(text, cx).into()
480 }))
481 },
482 handle: AgentContextHandle::Thread(handle),
483 }
484 }
485
486 fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
487 AddedContext {
488 kind: ContextKind::TextThread,
489 name: handle.title(cx),
490 parent: None,
491 tooltip: None,
492 icon_path: None,
493 status: ContextStatus::Ready,
494 render_hover: {
495 let context = handle.context.clone();
496 Some(Rc::new(move |_, cx| {
497 let text = context.read(cx).to_xml(cx);
498 ContextPillHover::new_text(text.into(), cx).into()
499 }))
500 },
501 handle: AgentContextHandle::TextThread(handle),
502 }
503 }
504
505 fn pending_rules(
506 handle: RulesContextHandle,
507 prompt_store: Option<&Entity<PromptStore>>,
508 cx: &App,
509 ) -> Option<AddedContext> {
510 let title = prompt_store
511 .as_ref()?
512 .read(cx)
513 .metadata(handle.prompt_id.into())?
514 .title
515 .unwrap_or_else(|| "Unnamed Rule".into());
516 Some(AddedContext {
517 kind: ContextKind::Rules,
518 name: title,
519 parent: None,
520 tooltip: None,
521 icon_path: None,
522 status: ContextStatus::Ready,
523 render_hover: None,
524 handle: AgentContextHandle::Rules(handle),
525 })
526 }
527
528 fn image(
529 context: ImageContext,
530 model: Option<&Arc<dyn language_model::LanguageModel>>,
531 path_style: PathStyle,
532 cx: &App,
533 ) -> AddedContext {
534 let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
535 let (name, parent) =
536 extract_file_name_and_directory_from_full_path(full_path, path_style);
537 let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
538 (name, parent, icon_path)
539 } else {
540 ("Image".into(), None, None)
541 };
542
543 let status = match context.status(model) {
544 ImageStatus::Loading => ContextStatus::Loading {
545 message: "Loading…".into(),
546 },
547 ImageStatus::Error => ContextStatus::Error {
548 message: "Failed to load Image".into(),
549 },
550 ImageStatus::Warning => ContextStatus::Warning {
551 message: format!(
552 "{} doesn't support attaching Images as Context",
553 model.map(|m| m.name().0).unwrap_or_else(|| "Model".into())
554 )
555 .into(),
556 },
557 ImageStatus::Ready => ContextStatus::Ready,
558 };
559
560 AddedContext {
561 kind: ContextKind::Image,
562 name,
563 parent,
564 tooltip: None,
565 icon_path,
566 status,
567 render_hover: Some(Rc::new({
568 let image = context.original_image.clone();
569 move |_, cx| {
570 let image = image.clone();
571 ContextPillHover::new(cx, move |_, _| {
572 gpui::img(image.clone())
573 .max_w_96()
574 .max_h_96()
575 .into_any_element()
576 })
577 .into()
578 }
579 })),
580 handle: AgentContextHandle::Image(context),
581 }
582 }
583}
584
585fn extract_file_name_and_directory_from_full_path(
586 path: &str,
587 path_style: PathStyle,
588) -> (SharedString, Option<SharedString>) {
589 let (parent, file_name) = path_style.split(path);
590 let parent = parent.and_then(|parent| {
591 let parent = parent.trim_end_matches(path_style.separator());
592 let (_, parent) = path_style.split(parent);
593 if parent.is_empty() {
594 None
595 } else {
596 Some(SharedString::new(parent))
597 }
598 });
599 (SharedString::new(file_name), parent)
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: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
612 let (parent, file_name) = path_style.split(full_path);
613 let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
614 let mut full_path_and_range = full_path.to_owned();
615 full_path_and_range.push_str(&line_range_text);
616 let mut file_name_and_range = file_name.to_owned();
617 file_name_and_range.push_str(&line_range_text);
618
619 let parent_name = parent.and_then(|parent| {
620 let parent = parent.trim_end_matches(path_style.separator());
621 let (_, parent) = path_style.split(parent);
622 if parent.is_empty() {
623 None
624 } else {
625 Some(SharedString::new(parent))
626 }
627 });
628
629 let icon_path = FileIcons::get_icon(Path::new(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(cx, move |this, 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(
729 ImageContext {
730 context_id: next_context_id.post_inc(),
731 project_path: None,
732 full_path: None,
733 original_image: Arc::new(Image::empty()),
734 image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
735 },
736 None,
737 PathStyle::local(),
738 cx,
739 ),
740 );
741
742 let image_loading = (
743 "Loading",
744 AddedContext::image(
745 ImageContext {
746 context_id: next_context_id.post_inc(),
747 project_path: None,
748 full_path: None,
749 original_image: Arc::new(Image::empty()),
750 image_task: cx
751 .background_spawn(async move {
752 smol::Timer::after(Duration::from_secs(60 * 5)).await;
753 Some(LanguageModelImage::empty())
754 })
755 .shared(),
756 },
757 None,
758 PathStyle::local(),
759 cx,
760 ),
761 );
762
763 let image_error = (
764 "Error",
765 AddedContext::image(
766 ImageContext {
767 context_id: next_context_id.post_inc(),
768 project_path: None,
769 full_path: None,
770 original_image: Arc::new(Image::empty()),
771 image_task: Task::ready(None).shared(),
772 },
773 None,
774 PathStyle::local(),
775 cx,
776 ),
777 );
778
779 Some(
780 v_flex()
781 .gap_6()
782 .children(
783 vec![image_ready, image_loading, image_error]
784 .into_iter()
785 .map(|(text, context)| {
786 single_example(
787 text,
788 ContextPill::added(context, false, false, None).into_any_element(),
789 )
790 }),
791 )
792 .into_any(),
793 )
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800 use gpui::App;
801 use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
802 use std::sync::Arc;
803
804 #[gpui::test]
805 fn test_image_context_warning_for_unsupported_model(cx: &mut App) {
806 let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel::default());
807 assert!(!model.supports_images());
808
809 let image_context = ImageContext {
810 context_id: ContextId::zero(),
811 project_path: None,
812 original_image: Arc::new(Image::empty()),
813 image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
814 full_path: None,
815 };
816
817 let added_context =
818 AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
819
820 assert!(matches!(
821 added_context.status,
822 ContextStatus::Warning { .. }
823 ));
824
825 assert!(matches!(added_context.kind, ContextKind::Image));
826 assert_eq!(added_context.name.as_ref(), "Image");
827 assert!(added_context.parent.is_none());
828 assert!(added_context.icon_path.is_none());
829 }
830
831 #[gpui::test]
832 fn test_image_context_ready_for_no_model(cx: &mut App) {
833 let image_context = ImageContext {
834 context_id: ContextId::zero(),
835 project_path: None,
836 original_image: Arc::new(Image::empty()),
837 image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
838 full_path: None,
839 };
840
841 let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
842
843 assert!(
844 matches!(added_context.status, ContextStatus::Ready),
845 "Expected ready status when no model provided"
846 );
847
848 assert!(matches!(added_context.kind, ContextKind::Image));
849 assert_eq!(added_context.name.as_ref(), "Image");
850 assert!(added_context.parent.is_none());
851 assert!(added_context.icon_path.is_none());
852 }
853}