1use crate::AssistantPanel;
2use crate::context::{AssistantContext, ContextId};
3use crate::context_picker::MentionLink;
4use crate::thread::{
5 LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
6 ThreadEvent, ThreadFeedback,
7};
8use crate::thread_store::ThreadStore;
9use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
10use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
11use anyhow::Context as _;
12use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
13use collections::{HashMap, HashSet};
14use editor::scroll::Autoscroll;
15use editor::{Editor, MultiBuffer};
16use gpui::{
17 AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardItem,
18 DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Hsla, ListAlignment, ListState,
19 MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
20 TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
21 linear_color_stop, linear_gradient, list, percentage, pulsating_between,
22};
23use language::{Buffer, LanguageRegistry};
24use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelToolUseId, Role};
25use markdown::parser::CodeBlockKind;
26use markdown::{Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, without_fences};
27use project::ProjectItem as _;
28use rope::Point;
29use settings::{Settings as _, update_settings_file};
30use std::ops::Range;
31use std::path::Path;
32use std::rc::Rc;
33use std::sync::Arc;
34use std::time::Duration;
35use text::ToPoint;
36use theme::ThemeSettings;
37use ui::{Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*};
38use util::ResultExt as _;
39use workspace::{OpenOptions, Workspace};
40
41use crate::context_store::ContextStore;
42
43pub struct ActiveThread {
44 language_registry: Arc<LanguageRegistry>,
45 thread_store: Entity<ThreadStore>,
46 thread: Entity<Thread>,
47 context_store: Entity<ContextStore>,
48 workspace: WeakEntity<Workspace>,
49 save_thread_task: Option<Task<()>>,
50 messages: Vec<MessageId>,
51 list_state: ListState,
52 scrollbar_state: ScrollbarState,
53 show_scrollbar: bool,
54 hide_scrollbar_task: Option<Task<()>>,
55 rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
56 rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,
57 editing_message: Option<(MessageId, EditMessageState)>,
58 expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
59 expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
60 expanded_code_blocks: HashMap<(MessageId, usize), bool>,
61 last_error: Option<ThreadError>,
62 notifications: Vec<WindowHandle<AgentNotification>>,
63 copied_code_block_ids: HashSet<(MessageId, usize)>,
64 _subscriptions: Vec<Subscription>,
65 notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
66 open_feedback_editors: HashMap<MessageId, Entity<Editor>>,
67}
68
69struct RenderedMessage {
70 language_registry: Arc<LanguageRegistry>,
71 segments: Vec<RenderedMessageSegment>,
72}
73
74#[derive(Clone)]
75struct RenderedToolUse {
76 label: Entity<Markdown>,
77 input: Entity<Markdown>,
78 output: Entity<Markdown>,
79}
80
81impl RenderedMessage {
82 fn from_segments(
83 segments: &[MessageSegment],
84 language_registry: Arc<LanguageRegistry>,
85 cx: &mut App,
86 ) -> Self {
87 let mut this = Self {
88 language_registry,
89 segments: Vec::with_capacity(segments.len()),
90 };
91 for segment in segments {
92 this.push_segment(segment, cx);
93 }
94 this
95 }
96
97 fn append_thinking(&mut self, text: &String, cx: &mut App) {
98 if let Some(RenderedMessageSegment::Thinking {
99 content,
100 scroll_handle,
101 }) = self.segments.last_mut()
102 {
103 content.update(cx, |markdown, cx| {
104 markdown.append(text, cx);
105 });
106 scroll_handle.scroll_to_bottom();
107 } else {
108 self.segments.push(RenderedMessageSegment::Thinking {
109 content: parse_markdown(text.into(), self.language_registry.clone(), cx),
110 scroll_handle: ScrollHandle::default(),
111 });
112 }
113 }
114
115 fn append_text(&mut self, text: &String, cx: &mut App) {
116 if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {
117 markdown.update(cx, |markdown, cx| markdown.append(text, cx));
118 } else {
119 self.segments
120 .push(RenderedMessageSegment::Text(parse_markdown(
121 SharedString::from(text),
122 self.language_registry.clone(),
123 cx,
124 )));
125 }
126 }
127
128 fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {
129 let rendered_segment = match segment {
130 MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
131 content: parse_markdown(text.into(), self.language_registry.clone(), cx),
132 scroll_handle: ScrollHandle::default(),
133 },
134 MessageSegment::Text(text) => RenderedMessageSegment::Text(parse_markdown(
135 text.into(),
136 self.language_registry.clone(),
137 cx,
138 )),
139 };
140 self.segments.push(rendered_segment);
141 }
142}
143
144enum RenderedMessageSegment {
145 Thinking {
146 content: Entity<Markdown>,
147 scroll_handle: ScrollHandle,
148 },
149 Text(Entity<Markdown>),
150}
151
152fn parse_markdown(
153 text: SharedString,
154 language_registry: Arc<LanguageRegistry>,
155 cx: &mut App,
156) -> Entity<Markdown> {
157 cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))
158}
159
160fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
161 let theme_settings = ThemeSettings::get_global(cx);
162 let colors = cx.theme().colors();
163 let ui_font_size = TextSize::Default.rems(cx);
164 let buffer_font_size = TextSize::Small.rems(cx);
165 let mut text_style = window.text_style();
166
167 text_style.refine(&TextStyleRefinement {
168 font_family: Some(theme_settings.ui_font.family.clone()),
169 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
170 font_features: Some(theme_settings.ui_font.features.clone()),
171 font_size: Some(ui_font_size.into()),
172 color: Some(cx.theme().colors().text),
173 ..Default::default()
174 });
175
176 MarkdownStyle {
177 base_text_style: text_style,
178 syntax: cx.theme().syntax().clone(),
179 selection_background_color: cx.theme().players().local().selection,
180 code_block_overflow_x_scroll: true,
181 table_overflow_x_scroll: true,
182 code_block: StyleRefinement {
183 padding: EdgesRefinement {
184 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
185 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
186 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
187 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
188 },
189 background: Some(colors.editor_background.into()),
190 text: Some(TextStyleRefinement {
191 font_family: Some(theme_settings.buffer_font.family.clone()),
192 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
193 font_features: Some(theme_settings.buffer_font.features.clone()),
194 font_size: Some(buffer_font_size.into()),
195 ..Default::default()
196 }),
197 ..Default::default()
198 },
199 inline_code: TextStyleRefinement {
200 font_family: Some(theme_settings.buffer_font.family.clone()),
201 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
202 font_features: Some(theme_settings.buffer_font.features.clone()),
203 font_size: Some(buffer_font_size.into()),
204 background_color: Some(colors.editor_foreground.opacity(0.08)),
205 ..Default::default()
206 },
207 link: TextStyleRefinement {
208 background_color: Some(colors.editor_foreground.opacity(0.025)),
209 underline: Some(UnderlineStyle {
210 color: Some(colors.text_accent.opacity(0.5)),
211 thickness: px(1.),
212 ..Default::default()
213 }),
214 ..Default::default()
215 },
216 link_callback: Some(Rc::new(move |url, cx| {
217 if MentionLink::is_valid(url) {
218 let colors = cx.theme().colors();
219 Some(TextStyleRefinement {
220 background_color: Some(colors.element_background),
221 ..Default::default()
222 })
223 } else {
224 None
225 }
226 })),
227 ..Default::default()
228 }
229}
230
231fn render_tool_use_markdown(
232 text: SharedString,
233 language_registry: Arc<LanguageRegistry>,
234 cx: &mut App,
235) -> Entity<Markdown> {
236 cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))
237}
238
239fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
240 let theme_settings = ThemeSettings::get_global(cx);
241 let colors = cx.theme().colors();
242 let ui_font_size = TextSize::Default.rems(cx);
243 let buffer_font_size = TextSize::Small.rems(cx);
244 let mut text_style = window.text_style();
245
246 text_style.refine(&TextStyleRefinement {
247 font_family: Some(theme_settings.ui_font.family.clone()),
248 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
249 font_features: Some(theme_settings.ui_font.features.clone()),
250 font_size: Some(ui_font_size.into()),
251 color: Some(cx.theme().colors().text),
252 ..Default::default()
253 });
254
255 MarkdownStyle {
256 base_text_style: text_style,
257 syntax: cx.theme().syntax().clone(),
258 selection_background_color: cx.theme().players().local().selection,
259 code_block_overflow_x_scroll: true,
260 code_block: StyleRefinement {
261 margin: EdgesRefinement::default(),
262 padding: EdgesRefinement::default(),
263 background: Some(colors.editor_background.into()),
264 border_color: None,
265 border_widths: EdgesRefinement::default(),
266 text: Some(TextStyleRefinement {
267 font_family: Some(theme_settings.buffer_font.family.clone()),
268 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
269 font_features: Some(theme_settings.buffer_font.features.clone()),
270 font_size: Some(buffer_font_size.into()),
271 ..Default::default()
272 }),
273 ..Default::default()
274 },
275 inline_code: TextStyleRefinement {
276 font_family: Some(theme_settings.buffer_font.family.clone()),
277 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
278 font_features: Some(theme_settings.buffer_font.features.clone()),
279 font_size: Some(TextSize::XSmall.rems(cx).into()),
280 ..Default::default()
281 },
282 heading: StyleRefinement {
283 text: Some(TextStyleRefinement {
284 font_size: Some(ui_font_size.into()),
285 ..Default::default()
286 }),
287 ..Default::default()
288 },
289 ..Default::default()
290 }
291}
292
293fn render_markdown_code_block(
294 message_id: MessageId,
295 ix: usize,
296 kind: &CodeBlockKind,
297 parsed_markdown: &ParsedMarkdown,
298 codeblock_range: Range<usize>,
299 active_thread: Entity<ActiveThread>,
300 workspace: WeakEntity<Workspace>,
301 _window: &Window,
302 cx: &App,
303) -> Div {
304 let label = match kind {
305 CodeBlockKind::Indented => None,
306 CodeBlockKind::Fenced => Some(
307 h_flex()
308 .gap_1()
309 .child(
310 Icon::new(IconName::Code)
311 .color(Color::Muted)
312 .size(IconSize::XSmall),
313 )
314 .child(Label::new("untitled").size(LabelSize::Small))
315 .into_any_element(),
316 ),
317 CodeBlockKind::FencedLang(raw_language_name) => Some(
318 h_flex()
319 .gap_1()
320 .children(
321 parsed_markdown
322 .languages_by_name
323 .get(raw_language_name)
324 .and_then(|language| {
325 language
326 .config()
327 .matcher
328 .path_suffixes
329 .iter()
330 .find_map(|extension| {
331 file_icons::FileIcons::get_icon(Path::new(extension), cx)
332 })
333 .map(Icon::from_path)
334 .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
335 }),
336 )
337 .child(
338 Label::new(
339 parsed_markdown
340 .languages_by_name
341 .get(raw_language_name)
342 .map(|language| language.name().into())
343 .clone()
344 .unwrap_or_else(|| raw_language_name.clone()),
345 )
346 .size(LabelSize::Small),
347 )
348 .into_any_element(),
349 ),
350 CodeBlockKind::FencedSrc(path_range) => path_range.path.file_name().map(|file_name| {
351 let content = if let Some(parent) = path_range.path.parent() {
352 h_flex()
353 .ml_1()
354 .gap_1()
355 .child(
356 Label::new(file_name.to_string_lossy().to_string()).size(LabelSize::Small),
357 )
358 .child(
359 Label::new(parent.to_string_lossy().to_string())
360 .color(Color::Muted)
361 .size(LabelSize::Small),
362 )
363 .into_any_element()
364 } else {
365 Label::new(path_range.path.to_string_lossy().to_string())
366 .size(LabelSize::Small)
367 .ml_1()
368 .into_any_element()
369 };
370
371 h_flex()
372 .id(("code-block-header-label", ix))
373 .w_full()
374 .max_w_full()
375 .px_1()
376 .gap_0p5()
377 .cursor_pointer()
378 .rounded_sm()
379 .hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
380 .tooltip(Tooltip::text("Jump to File"))
381 .child(
382 h_flex()
383 .gap_0p5()
384 .children(
385 file_icons::FileIcons::get_icon(&path_range.path, cx)
386 .map(Icon::from_path)
387 .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
388 )
389 .child(content)
390 .child(
391 Icon::new(IconName::ArrowUpRight)
392 .size(IconSize::XSmall)
393 .color(Color::Ignored),
394 ),
395 )
396 .on_click({
397 let path_range = path_range.clone();
398 move |_, window, cx| {
399 workspace
400 .update(cx, {
401 |workspace, cx| {
402 if let Some(project_path) = workspace
403 .project()
404 .read(cx)
405 .find_project_path(&path_range.path, cx)
406 {
407 let target = path_range.range.as_ref().map(|range| {
408 Point::new(
409 // Line number is 1-based
410 range.start.line.saturating_sub(1),
411 range.start.col.unwrap_or(0),
412 )
413 });
414 let open_task = workspace.open_path(
415 project_path,
416 None,
417 true,
418 window,
419 cx,
420 );
421 window
422 .spawn(cx, async move |cx| {
423 let item = open_task.await?;
424 if let Some(target) = target {
425 if let Some(active_editor) =
426 item.downcast::<Editor>()
427 {
428 active_editor
429 .downgrade()
430 .update_in(cx, |editor, window, cx| {
431 editor
432 .go_to_singleton_buffer_point(
433 target, window, cx,
434 );
435 })
436 .log_err();
437 }
438 }
439 anyhow::Ok(())
440 })
441 .detach_and_log_err(cx);
442 }
443 }
444 })
445 .ok();
446 }
447 })
448 .into_any_element()
449 }),
450 };
451
452 let codeblock_was_copied = active_thread
453 .read(cx)
454 .copied_code_block_ids
455 .contains(&(message_id, ix));
456
457 let is_expanded = active_thread
458 .read(cx)
459 .expanded_code_blocks
460 .get(&(message_id, ix))
461 .copied()
462 .unwrap_or(false);
463
464 let codeblock_header_bg = cx
465 .theme()
466 .colors()
467 .element_background
468 .blend(cx.theme().colors().editor_foreground.opacity(0.01));
469
470 let line_count = without_fences(&parsed_markdown.source()[codeblock_range.clone()])
471 .lines()
472 .count();
473
474 const MAX_COLLAPSED_LINES: usize = 5;
475
476 let codeblock_header = h_flex()
477 .group("codeblock_header")
478 .p_1()
479 .gap_1()
480 .justify_between()
481 .border_b_1()
482 .border_color(cx.theme().colors().border_variant)
483 .bg(codeblock_header_bg)
484 .rounded_t_md()
485 .children(label)
486 .child(
487 h_flex()
488 .gap_1()
489 .child(
490 div().visible_on_hover("codeblock_header").child(
491 IconButton::new(
492 ("copy-markdown-code", ix),
493 if codeblock_was_copied {
494 IconName::Check
495 } else {
496 IconName::Copy
497 },
498 )
499 .icon_color(Color::Muted)
500 .shape(ui::IconButtonShape::Square)
501 .tooltip(Tooltip::text("Copy Code"))
502 .on_click({
503 let active_thread = active_thread.clone();
504 let parsed_markdown = parsed_markdown.clone();
505 move |_event, _window, cx| {
506 active_thread.update(cx, |this, cx| {
507 this.copied_code_block_ids.insert((message_id, ix));
508
509 let code = without_fences(
510 &parsed_markdown.source()[codeblock_range.clone()],
511 )
512 .to_string();
513
514 cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
515
516 cx.spawn(async move |this, cx| {
517 cx.background_executor()
518 .timer(Duration::from_secs(2))
519 .await;
520
521 cx.update(|cx| {
522 this.update(cx, |this, cx| {
523 this.copied_code_block_ids
524 .remove(&(message_id, ix));
525 cx.notify();
526 })
527 })
528 .ok();
529 })
530 .detach();
531 });
532 }
533 }),
534 ),
535 )
536 .when(line_count > MAX_COLLAPSED_LINES, |header| {
537 header.child(
538 IconButton::new(
539 ("expand-collapse-code", ix),
540 if is_expanded {
541 IconName::ChevronUp
542 } else {
543 IconName::ChevronDown
544 },
545 )
546 .icon_color(Color::Muted)
547 .shape(ui::IconButtonShape::Square)
548 .tooltip(Tooltip::text(if is_expanded {
549 "Collapse Code"
550 } else {
551 "Expand Code"
552 }))
553 .on_click({
554 let active_thread = active_thread.clone();
555 move |_event, _window, cx| {
556 active_thread.update(cx, |this, cx| {
557 let is_expanded = this
558 .expanded_code_blocks
559 .entry((message_id, ix))
560 .or_insert(false);
561 *is_expanded = !*is_expanded;
562 cx.notify();
563 });
564 }
565 }),
566 )
567 }),
568 );
569
570 v_flex()
571 .my_2()
572 .overflow_hidden()
573 .rounded_lg()
574 .border_1()
575 .border_color(cx.theme().colors().border_variant)
576 .bg(cx.theme().colors().editor_background)
577 .child(codeblock_header)
578 .when(line_count > MAX_COLLAPSED_LINES, |this| {
579 if is_expanded {
580 this.h_full()
581 } else {
582 this.max_h_40()
583 }
584 })
585}
586
587fn open_markdown_link(
588 text: SharedString,
589 workspace: WeakEntity<Workspace>,
590 window: &mut Window,
591 cx: &mut App,
592) {
593 let Some(workspace) = workspace.upgrade() else {
594 cx.open_url(&text);
595 return;
596 };
597
598 match MentionLink::try_parse(&text, &workspace, cx) {
599 Some(MentionLink::File(path, entry)) => workspace.update(cx, |workspace, cx| {
600 if entry.is_dir() {
601 workspace.project().update(cx, |_, cx| {
602 cx.emit(project::Event::RevealInProjectPanel(entry.id));
603 })
604 } else {
605 workspace
606 .open_path(path, None, true, window, cx)
607 .detach_and_log_err(cx);
608 }
609 }),
610 Some(MentionLink::Symbol(path, symbol_name)) => {
611 let open_task = workspace.update(cx, |workspace, cx| {
612 workspace.open_path(path, None, true, window, cx)
613 });
614 window
615 .spawn(cx, async move |cx| {
616 let active_editor = open_task
617 .await?
618 .downcast::<Editor>()
619 .context("Item is not an editor")?;
620 active_editor.update_in(cx, |editor, window, cx| {
621 let symbol_range = editor
622 .buffer()
623 .read(cx)
624 .snapshot(cx)
625 .outline(None)
626 .and_then(|outline| {
627 outline
628 .find_most_similar(&symbol_name)
629 .map(|(_, item)| item.range.clone())
630 })
631 .context("Could not find matching symbol")?;
632
633 editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
634 s.select_anchor_ranges([symbol_range.start..symbol_range.start])
635 });
636 anyhow::Ok(())
637 })
638 })
639 .detach_and_log_err(cx);
640 }
641 Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
642 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
643 panel.update(cx, |panel, cx| {
644 panel
645 .open_thread(&thread_id, window, cx)
646 .detach_and_log_err(cx)
647 });
648 }
649 }),
650 Some(MentionLink::Fetch(url)) => cx.open_url(&url),
651 None => cx.open_url(&text),
652 }
653}
654
655struct EditMessageState {
656 editor: Entity<Editor>,
657}
658
659impl ActiveThread {
660 pub fn new(
661 thread: Entity<Thread>,
662 thread_store: Entity<ThreadStore>,
663 language_registry: Arc<LanguageRegistry>,
664 context_store: Entity<ContextStore>,
665 workspace: WeakEntity<Workspace>,
666 window: &mut Window,
667 cx: &mut Context<Self>,
668 ) -> Self {
669 let subscriptions = vec![
670 cx.observe(&thread, |_, _, cx| cx.notify()),
671 cx.subscribe_in(&thread, window, Self::handle_thread_event),
672 ];
673
674 let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
675 let this = cx.entity().downgrade();
676 move |ix, window: &mut Window, cx: &mut App| {
677 this.update(cx, |this, cx| this.render_message(ix, window, cx))
678 .unwrap()
679 }
680 });
681
682 let mut this = Self {
683 language_registry,
684 thread_store,
685 thread: thread.clone(),
686 context_store,
687 workspace,
688 save_thread_task: None,
689 messages: Vec::new(),
690 rendered_messages_by_id: HashMap::default(),
691 rendered_tool_uses: HashMap::default(),
692 expanded_tool_uses: HashMap::default(),
693 expanded_thinking_segments: HashMap::default(),
694 expanded_code_blocks: HashMap::default(),
695 list_state: list_state.clone(),
696 scrollbar_state: ScrollbarState::new(list_state),
697 show_scrollbar: false,
698 hide_scrollbar_task: None,
699 editing_message: None,
700 last_error: None,
701 copied_code_block_ids: HashSet::default(),
702 notifications: Vec::new(),
703 _subscriptions: subscriptions,
704 notification_subscriptions: HashMap::default(),
705 open_feedback_editors: HashMap::default(),
706 };
707
708 for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
709 this.push_message(&message.id, &message.segments, window, cx);
710
711 for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
712 this.render_tool_use_markdown(
713 tool_use.id.clone(),
714 tool_use.ui_text.clone(),
715 &tool_use.input,
716 tool_use.status.text(),
717 cx,
718 );
719 }
720 }
721
722 this
723 }
724
725 pub fn thread(&self) -> &Entity<Thread> {
726 &self.thread
727 }
728
729 pub fn is_empty(&self) -> bool {
730 self.messages.is_empty()
731 }
732
733 pub fn summary(&self, cx: &App) -> Option<SharedString> {
734 self.thread.read(cx).summary()
735 }
736
737 pub fn summary_or_default(&self, cx: &App) -> SharedString {
738 self.thread.read(cx).summary_or_default()
739 }
740
741 pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
742 self.last_error.take();
743 self.thread
744 .update(cx, |thread, cx| thread.cancel_last_completion(cx))
745 }
746
747 pub fn last_error(&self) -> Option<ThreadError> {
748 self.last_error.clone()
749 }
750
751 pub fn clear_last_error(&mut self) {
752 self.last_error.take();
753 }
754
755 fn push_message(
756 &mut self,
757 id: &MessageId,
758 segments: &[MessageSegment],
759 _window: &mut Window,
760 cx: &mut Context<Self>,
761 ) {
762 let old_len = self.messages.len();
763 self.messages.push(*id);
764 self.list_state.splice(old_len..old_len, 1);
765
766 let rendered_message =
767 RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
768 self.rendered_messages_by_id.insert(*id, rendered_message);
769 }
770
771 fn edited_message(
772 &mut self,
773 id: &MessageId,
774 segments: &[MessageSegment],
775 _window: &mut Window,
776 cx: &mut Context<Self>,
777 ) {
778 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
779 return;
780 };
781 self.list_state.splice(index..index + 1, 1);
782 let rendered_message =
783 RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
784 self.rendered_messages_by_id.insert(*id, rendered_message);
785 }
786
787 fn deleted_message(&mut self, id: &MessageId) {
788 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
789 return;
790 };
791 self.messages.remove(index);
792 self.list_state.splice(index..index + 1, 0);
793 self.rendered_messages_by_id.remove(id);
794 }
795
796 fn render_tool_use_markdown(
797 &mut self,
798 tool_use_id: LanguageModelToolUseId,
799 tool_label: impl Into<SharedString>,
800 tool_input: &serde_json::Value,
801 tool_output: SharedString,
802 cx: &mut Context<Self>,
803 ) {
804 let rendered = RenderedToolUse {
805 label: render_tool_use_markdown(tool_label.into(), self.language_registry.clone(), cx),
806 input: render_tool_use_markdown(
807 format!(
808 "```json\n{}\n```",
809 serde_json::to_string_pretty(tool_input).unwrap_or_default()
810 )
811 .into(),
812 self.language_registry.clone(),
813 cx,
814 ),
815 output: render_tool_use_markdown(tool_output, self.language_registry.clone(), cx),
816 };
817 self.rendered_tool_uses
818 .insert(tool_use_id.clone(), rendered);
819 }
820
821 fn handle_thread_event(
822 &mut self,
823 _thread: &Entity<Thread>,
824 event: &ThreadEvent,
825 window: &mut Window,
826 cx: &mut Context<Self>,
827 ) {
828 match event {
829 ThreadEvent::ShowError(error) => {
830 self.last_error = Some(error.clone());
831 }
832 ThreadEvent::StreamedCompletion
833 | ThreadEvent::SummaryGenerated
834 | ThreadEvent::SummaryChanged => {
835 self.save_thread(cx);
836 }
837 ThreadEvent::DoneStreaming => {
838 let thread = self.thread.read(cx);
839
840 if !thread.is_generating() {
841 self.show_notification(
842 if thread.used_tools_since_last_user_message() {
843 "Finished running tools"
844 } else {
845 "New message"
846 },
847 IconName::ZedAssistant,
848 window,
849 cx,
850 );
851 }
852 }
853 ThreadEvent::ToolConfirmationNeeded => {
854 self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx);
855 }
856 ThreadEvent::StreamedAssistantText(message_id, text) => {
857 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
858 rendered_message.append_text(text, cx);
859 }
860 }
861 ThreadEvent::StreamedAssistantThinking(message_id, text) => {
862 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
863 rendered_message.append_thinking(text, cx);
864 }
865 }
866 ThreadEvent::MessageAdded(message_id) => {
867 if let Some(message_segments) = self
868 .thread
869 .read(cx)
870 .message(*message_id)
871 .map(|message| message.segments.clone())
872 {
873 self.push_message(message_id, &message_segments, window, cx);
874 }
875
876 self.save_thread(cx);
877 cx.notify();
878 }
879 ThreadEvent::MessageEdited(message_id) => {
880 if let Some(message_segments) = self
881 .thread
882 .read(cx)
883 .message(*message_id)
884 .map(|message| message.segments.clone())
885 {
886 self.edited_message(message_id, &message_segments, window, cx);
887 }
888
889 self.save_thread(cx);
890 cx.notify();
891 }
892 ThreadEvent::MessageDeleted(message_id) => {
893 self.deleted_message(message_id);
894 self.save_thread(cx);
895 cx.notify();
896 }
897 ThreadEvent::UsePendingTools => {
898 let tool_uses = self
899 .thread
900 .update(cx, |thread, cx| thread.use_pending_tools(cx));
901
902 for tool_use in tool_uses {
903 self.render_tool_use_markdown(
904 tool_use.id.clone(),
905 tool_use.ui_text.clone(),
906 &tool_use.input,
907 "".into(),
908 cx,
909 );
910 }
911 }
912 ThreadEvent::ToolFinished {
913 pending_tool_use,
914 canceled,
915 ..
916 } => {
917 let canceled = *canceled;
918 if let Some(tool_use) = pending_tool_use {
919 self.render_tool_use_markdown(
920 tool_use.id.clone(),
921 tool_use.ui_text.clone(),
922 &tool_use.input,
923 self.thread
924 .read(cx)
925 .tool_result(&tool_use.id)
926 .map(|result| result.content.clone().into())
927 .unwrap_or("".into()),
928 cx,
929 );
930 }
931
932 if self.thread.read(cx).all_tools_finished() {
933 let model_registry = LanguageModelRegistry::read_global(cx);
934 if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
935 self.thread.update(cx, |thread, cx| {
936 thread.attach_tool_results(cx);
937 if !canceled {
938 thread.send_to_model(model, RequestKind::Chat, cx);
939 }
940 });
941 }
942 }
943 }
944 ThreadEvent::CheckpointChanged => cx.notify(),
945 }
946 }
947
948 fn show_notification(
949 &mut self,
950 caption: impl Into<SharedString>,
951 icon: IconName,
952 window: &mut Window,
953 cx: &mut Context<ActiveThread>,
954 ) {
955 if window.is_window_active() || !self.notifications.is_empty() {
956 return;
957 }
958
959 let title = self
960 .thread
961 .read(cx)
962 .summary()
963 .unwrap_or("Agent Panel".into());
964
965 match AssistantSettings::get_global(cx).notify_when_agent_waiting {
966 NotifyWhenAgentWaiting::PrimaryScreen => {
967 if let Some(primary) = cx.primary_display() {
968 self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
969 }
970 }
971 NotifyWhenAgentWaiting::AllScreens => {
972 let caption = caption.into();
973 for screen in cx.displays() {
974 self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
975 }
976 }
977 NotifyWhenAgentWaiting::Never => {
978 // Don't show anything
979 }
980 }
981 }
982
983 fn pop_up(
984 &mut self,
985 icon: IconName,
986 caption: SharedString,
987 title: SharedString,
988 window: &mut Window,
989 screen: Rc<dyn PlatformDisplay>,
990 cx: &mut Context<'_, ActiveThread>,
991 ) {
992 let options = AgentNotification::window_options(screen, cx);
993
994 if let Some(screen_window) = cx
995 .open_window(options, |_, cx| {
996 cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
997 })
998 .log_err()
999 {
1000 if let Some(pop_up) = screen_window.entity(cx).log_err() {
1001 self.notification_subscriptions
1002 .entry(screen_window)
1003 .or_insert_with(Vec::new)
1004 .push(cx.subscribe_in(&pop_up, window, {
1005 |this, _, event, window, cx| match event {
1006 AgentNotificationEvent::Accepted => {
1007 let handle = window.window_handle();
1008 cx.activate(true);
1009
1010 let workspace_handle = this.workspace.clone();
1011
1012 // If there are multiple Zed windows, activate the correct one.
1013 cx.defer(move |cx| {
1014 handle
1015 .update(cx, |_view, window, _cx| {
1016 window.activate_window();
1017
1018 if let Some(workspace) = workspace_handle.upgrade() {
1019 workspace.update(_cx, |workspace, cx| {
1020 workspace
1021 .focus_panel::<AssistantPanel>(window, cx);
1022 });
1023 }
1024 })
1025 .log_err();
1026 });
1027
1028 this.dismiss_notifications(cx);
1029 }
1030 AgentNotificationEvent::Dismissed => {
1031 this.dismiss_notifications(cx);
1032 }
1033 }
1034 }));
1035
1036 self.notifications.push(screen_window);
1037
1038 // If the user manually refocuses the original window, dismiss the popup.
1039 self.notification_subscriptions
1040 .entry(screen_window)
1041 .or_insert_with(Vec::new)
1042 .push({
1043 let pop_up_weak = pop_up.downgrade();
1044
1045 cx.observe_window_activation(window, move |_, window, cx| {
1046 if window.is_window_active() {
1047 if let Some(pop_up) = pop_up_weak.upgrade() {
1048 pop_up.update(cx, |_, cx| {
1049 cx.emit(AgentNotificationEvent::Dismissed);
1050 });
1051 }
1052 }
1053 })
1054 });
1055 }
1056 }
1057 }
1058
1059 /// Spawns a task to save the active thread.
1060 ///
1061 /// Only one task to save the thread will be in flight at a time.
1062 fn save_thread(&mut self, cx: &mut Context<Self>) {
1063 let thread = self.thread.clone();
1064 self.save_thread_task = Some(cx.spawn(async move |this, cx| {
1065 let task = this
1066 .update(cx, |this, cx| {
1067 this.thread_store
1068 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
1069 })
1070 .ok();
1071
1072 if let Some(task) = task {
1073 task.await.log_err();
1074 }
1075 }));
1076 }
1077
1078 fn start_editing_message(
1079 &mut self,
1080 message_id: MessageId,
1081 message_segments: &[MessageSegment],
1082 window: &mut Window,
1083 cx: &mut Context<Self>,
1084 ) {
1085 // User message should always consist of a single text segment,
1086 // therefore we can skip returning early if it's not a text segment.
1087 let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
1088 return;
1089 };
1090
1091 let buffer = cx.new(|cx| {
1092 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
1093 });
1094 let editor = cx.new(|cx| {
1095 let mut editor = Editor::new(
1096 editor::EditorMode::AutoHeight { max_lines: 8 },
1097 buffer,
1098 None,
1099 window,
1100 cx,
1101 );
1102 editor.focus_handle(cx).focus(window);
1103 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
1104 editor
1105 });
1106 self.editing_message = Some((
1107 message_id,
1108 EditMessageState {
1109 editor: editor.clone(),
1110 },
1111 ));
1112 cx.notify();
1113 }
1114
1115 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
1116 self.editing_message.take();
1117 cx.notify();
1118 }
1119
1120 fn confirm_editing_message(
1121 &mut self,
1122 _: &menu::Confirm,
1123 _: &mut Window,
1124 cx: &mut Context<Self>,
1125 ) {
1126 let Some((message_id, state)) = self.editing_message.take() else {
1127 return;
1128 };
1129 let edited_text = state.editor.read(cx).text(cx);
1130 self.thread.update(cx, |thread, cx| {
1131 thread.edit_message(
1132 message_id,
1133 Role::User,
1134 vec![MessageSegment::Text(edited_text)],
1135 cx,
1136 );
1137 for message_id in self.messages_after(message_id) {
1138 thread.delete_message(*message_id, cx);
1139 }
1140 });
1141
1142 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1143 return;
1144 };
1145
1146 if model.provider.must_accept_terms(cx) {
1147 cx.notify();
1148 return;
1149 }
1150
1151 self.thread.update(cx, |thread, cx| {
1152 thread.send_to_model(model.model, RequestKind::Chat, cx)
1153 });
1154 cx.notify();
1155 }
1156
1157 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
1158 self.messages
1159 .iter()
1160 .position(|id| *id == message_id)
1161 .map(|index| &self.messages[index + 1..])
1162 .unwrap_or(&[])
1163 }
1164
1165 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1166 self.cancel_editing_message(&menu::Cancel, window, cx);
1167 }
1168
1169 fn handle_regenerate_click(
1170 &mut self,
1171 _: &ClickEvent,
1172 window: &mut Window,
1173 cx: &mut Context<Self>,
1174 ) {
1175 self.confirm_editing_message(&menu::Confirm, window, cx);
1176 }
1177
1178 fn handle_feedback_click(
1179 &mut self,
1180 message_id: MessageId,
1181 feedback: ThreadFeedback,
1182 window: &mut Window,
1183 cx: &mut Context<Self>,
1184 ) {
1185 let report = self.thread.update(cx, |thread, cx| {
1186 thread.report_message_feedback(message_id, feedback, cx)
1187 });
1188
1189 cx.spawn(async move |this, cx| {
1190 report.await?;
1191 this.update(cx, |_this, cx| cx.notify())
1192 })
1193 .detach_and_log_err(cx);
1194
1195 match feedback {
1196 ThreadFeedback::Positive => {
1197 self.open_feedback_editors.remove(&message_id);
1198 }
1199 ThreadFeedback::Negative => {
1200 self.handle_show_feedback_comments(message_id, window, cx);
1201 }
1202 }
1203 }
1204
1205 fn handle_show_feedback_comments(
1206 &mut self,
1207 message_id: MessageId,
1208 window: &mut Window,
1209 cx: &mut Context<Self>,
1210 ) {
1211 let buffer = cx.new(|cx| {
1212 let empty_string = String::new();
1213 MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
1214 });
1215
1216 let editor = cx.new(|cx| {
1217 let mut editor = Editor::new(
1218 editor::EditorMode::AutoHeight { max_lines: 4 },
1219 buffer,
1220 None,
1221 window,
1222 cx,
1223 );
1224 editor.set_placeholder_text(
1225 "What went wrong? Share your feedback so we can improve.",
1226 cx,
1227 );
1228 editor
1229 });
1230
1231 editor.read(cx).focus_handle(cx).focus(window);
1232 self.open_feedback_editors.insert(message_id, editor);
1233 cx.notify();
1234 }
1235
1236 fn submit_feedback_message(&mut self, message_id: MessageId, cx: &mut Context<Self>) {
1237 let Some(editor) = self.open_feedback_editors.get(&message_id) else {
1238 return;
1239 };
1240
1241 let report_task = self.thread.update(cx, |thread, cx| {
1242 thread.report_message_feedback(message_id, ThreadFeedback::Negative, cx)
1243 });
1244
1245 let comments = editor.read(cx).text(cx);
1246 if !comments.is_empty() {
1247 let thread_id = self.thread.read(cx).id().clone();
1248 let comments_value = String::from(comments.as_str());
1249
1250 let message_content = self
1251 .thread
1252 .read(cx)
1253 .message(message_id)
1254 .map(|msg| msg.to_string())
1255 .unwrap_or_default();
1256
1257 telemetry::event!(
1258 "Assistant Thread Feedback Comments",
1259 thread_id,
1260 message_id = message_id.0,
1261 message_content,
1262 comments = comments_value
1263 );
1264
1265 self.open_feedback_editors.remove(&message_id);
1266
1267 cx.spawn(async move |this, cx| {
1268 report_task.await?;
1269 this.update(cx, |_this, cx| cx.notify())
1270 })
1271 .detach_and_log_err(cx);
1272 }
1273 }
1274
1275 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1276 let message_id = self.messages[ix];
1277 let Some(message) = self.thread.read(cx).message(message_id) else {
1278 return Empty.into_any();
1279 };
1280
1281 let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
1282 return Empty.into_any();
1283 };
1284
1285 let context_store = self.context_store.clone();
1286 let workspace = self.workspace.clone();
1287 let thread = self.thread.read(cx);
1288
1289 // Get all the data we need from thread before we start using it in closures
1290 let checkpoint = thread.checkpoint_for_message(message_id);
1291 let context = thread.context_for_message(message_id).collect::<Vec<_>>();
1292
1293 let tool_uses = thread.tool_uses_for_message(message_id, cx);
1294 let has_tool_uses = !tool_uses.is_empty();
1295 let is_generating = thread.is_generating();
1296
1297 let is_first_message = ix == 0;
1298 let is_last_message = ix == self.messages.len() - 1;
1299
1300 let show_feedback = (!is_generating && is_last_message && message.role != Role::User)
1301 || self.messages.get(ix + 1).map_or(false, |next_id| {
1302 self.thread
1303 .read(cx)
1304 .message(*next_id)
1305 .map_or(false, |next_message| {
1306 next_message.role == Role::User
1307 && thread.tool_uses_for_message(*next_id, cx).is_empty()
1308 && thread.tool_results_for_message(*next_id).is_empty()
1309 })
1310 });
1311
1312 let needs_confirmation = tool_uses.iter().any(|tool_use| tool_use.needs_confirmation);
1313
1314 let generating_label = (is_generating && is_last_message).then(|| {
1315 Label::new("Generating")
1316 .color(Color::Muted)
1317 .size(LabelSize::Small)
1318 .with_animations(
1319 "generating-label",
1320 vec![
1321 Animation::new(Duration::from_secs(1)),
1322 Animation::new(Duration::from_secs(1)).repeat(),
1323 ],
1324 |mut label, animation_ix, delta| {
1325 match animation_ix {
1326 0 => {
1327 let chars_to_show = (delta * 10.).ceil() as usize;
1328 let text = &"Generating"[0..chars_to_show];
1329 label.set_text(text);
1330 }
1331 1 => {
1332 let text = match delta {
1333 d if d < 0.25 => "Generating",
1334 d if d < 0.5 => "Generating.",
1335 d if d < 0.75 => "Generating..",
1336 _ => "Generating...",
1337 };
1338 label.set_text(text);
1339 }
1340 _ => {}
1341 }
1342 label
1343 },
1344 )
1345 .with_animation(
1346 "pulsating-label",
1347 Animation::new(Duration::from_secs(2))
1348 .repeat()
1349 .with_easing(pulsating_between(0.6, 1.)),
1350 |label, delta| label.map_element(|label| label.alpha(delta)),
1351 )
1352 });
1353
1354 // Don't render user messages that are just there for returning tool results.
1355 if message.role == Role::User && thread.message_has_tool_results(message_id) {
1356 if let Some(generating_label) = generating_label {
1357 return h_flex()
1358 .w_full()
1359 .h_10()
1360 .py_1p5()
1361 .pl_4()
1362 .pb_3()
1363 .child(generating_label)
1364 .into_any_element();
1365 }
1366
1367 return Empty.into_any();
1368 }
1369
1370 let allow_editing_message = message.role == Role::User;
1371
1372 let edit_message_editor = self
1373 .editing_message
1374 .as_ref()
1375 .filter(|(id, _)| *id == message_id)
1376 .map(|(_, state)| state.editor.clone());
1377
1378 let colors = cx.theme().colors();
1379 let active_color = colors.element_active;
1380 let editor_bg_color = colors.editor_background;
1381 let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
1382
1383 let feedback_container = h_flex().py_2().px_4().gap_1().justify_between();
1384
1385 let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
1386 Some(feedback) => feedback_container
1387 .child(
1388 Label::new(match feedback {
1389 ThreadFeedback::Positive => "Thanks for your feedback!",
1390 ThreadFeedback::Negative => {
1391 "We appreciate your feedback and will use it to improve."
1392 }
1393 })
1394 .color(Color::Muted)
1395 .size(LabelSize::XSmall),
1396 )
1397 .child(
1398 h_flex()
1399 .pr_1()
1400 .gap_1()
1401 .child(
1402 IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
1403 .shape(ui::IconButtonShape::Square)
1404 .icon_size(IconSize::XSmall)
1405 .icon_color(match feedback {
1406 ThreadFeedback::Positive => Color::Accent,
1407 ThreadFeedback::Negative => Color::Ignored,
1408 })
1409 .tooltip(Tooltip::text("Helpful Response"))
1410 .on_click(cx.listener(move |this, _, window, cx| {
1411 this.handle_feedback_click(
1412 message_id,
1413 ThreadFeedback::Positive,
1414 window,
1415 cx,
1416 );
1417 })),
1418 )
1419 .child(
1420 IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
1421 .shape(ui::IconButtonShape::Square)
1422 .icon_size(IconSize::XSmall)
1423 .icon_color(match feedback {
1424 ThreadFeedback::Positive => Color::Ignored,
1425 ThreadFeedback::Negative => Color::Accent,
1426 })
1427 .tooltip(Tooltip::text("Not Helpful"))
1428 .on_click(cx.listener(move |this, _, window, cx| {
1429 this.handle_feedback_click(
1430 message_id,
1431 ThreadFeedback::Negative,
1432 window,
1433 cx,
1434 );
1435 })),
1436 ),
1437 )
1438 .into_any_element(),
1439 None => feedback_container
1440 .child(
1441 Label::new(
1442 "Rating the thread sends all of your current conversation to the Zed team.",
1443 )
1444 .color(Color::Muted)
1445 .size(LabelSize::XSmall),
1446 )
1447 .child(
1448 h_flex()
1449 .gap_1()
1450 .child(
1451 IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
1452 .icon_size(IconSize::XSmall)
1453 .icon_color(Color::Ignored)
1454 .shape(ui::IconButtonShape::Square)
1455 .tooltip(Tooltip::text("Helpful Response"))
1456 .on_click(cx.listener(move |this, _, window, cx| {
1457 this.handle_feedback_click(
1458 message_id,
1459 ThreadFeedback::Positive,
1460 window,
1461 cx,
1462 );
1463 })),
1464 )
1465 .child(
1466 IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
1467 .icon_size(IconSize::XSmall)
1468 .icon_color(Color::Ignored)
1469 .shape(ui::IconButtonShape::Square)
1470 .tooltip(Tooltip::text("Not Helpful"))
1471 .on_click(cx.listener(move |this, _, window, cx| {
1472 this.handle_feedback_click(
1473 message_id,
1474 ThreadFeedback::Negative,
1475 window,
1476 cx,
1477 );
1478 })),
1479 ),
1480 )
1481 .into_any_element(),
1482 };
1483
1484 let message_is_empty = message.should_display_content();
1485 let has_content = !message_is_empty || !context.is_empty();
1486
1487 let message_content =
1488 has_content.then(|| {
1489 v_flex()
1490 .gap_1p5()
1491 .when(!message_is_empty, |parent| {
1492 parent.child(
1493 if let Some(edit_message_editor) = edit_message_editor.clone() {
1494 div()
1495 .key_context("EditMessageEditor")
1496 .on_action(cx.listener(Self::cancel_editing_message))
1497 .on_action(cx.listener(Self::confirm_editing_message))
1498 .min_h_6()
1499 .child(edit_message_editor)
1500 .into_any()
1501 } else {
1502 div()
1503 .min_h_6()
1504 .text_ui(cx)
1505 .child(self.render_message_content(
1506 message_id,
1507 rendered_message,
1508 has_tool_uses,
1509 workspace.clone(),
1510 window,
1511 cx,
1512 ))
1513 .into_any()
1514 },
1515 )
1516 })
1517 .when(!context.is_empty(), |parent| {
1518 parent.child(h_flex().flex_wrap().gap_1().children(
1519 context.into_iter().map(|context| {
1520 let context_id = context.id();
1521 ContextPill::added(
1522 AddedContext::new(context, cx),
1523 false,
1524 false,
1525 None,
1526 )
1527 .on_click(Rc::new(cx.listener({
1528 let workspace = workspace.clone();
1529 let context_store = context_store.clone();
1530 move |_, _, window, cx| {
1531 if let Some(workspace) = workspace.upgrade() {
1532 open_context(
1533 context_id,
1534 context_store.clone(),
1535 workspace,
1536 window,
1537 cx,
1538 );
1539 cx.notify();
1540 }
1541 }
1542 })))
1543 }),
1544 ))
1545 })
1546 });
1547
1548 let styled_message = match message.role {
1549 Role::User => v_flex()
1550 .id(("message-container", ix))
1551 .map(|this| {
1552 if is_first_message {
1553 this.pt_2()
1554 } else {
1555 this.pt_4()
1556 }
1557 })
1558 .pb_4()
1559 .pl_2()
1560 .pr_2p5()
1561 .child(
1562 v_flex()
1563 .bg(colors.editor_background)
1564 .rounded_lg()
1565 .border_1()
1566 .border_color(colors.border)
1567 .shadow_md()
1568 .child(
1569 h_flex()
1570 .py_1()
1571 .pl_2()
1572 .pr_1()
1573 .bg(bg_user_message_header)
1574 .border_b_1()
1575 .border_color(colors.border)
1576 .justify_between()
1577 .rounded_t_md()
1578 .child(
1579 h_flex()
1580 .gap_1p5()
1581 .child(
1582 Icon::new(IconName::PersonCircle)
1583 .size(IconSize::XSmall)
1584 .color(Color::Muted),
1585 )
1586 .child(
1587 Label::new("You")
1588 .size(LabelSize::Small)
1589 .color(Color::Muted),
1590 ),
1591 )
1592 .child(
1593 h_flex()
1594 .gap_1()
1595 .when_some(
1596 edit_message_editor.clone(),
1597 |this, edit_message_editor| {
1598 let focus_handle =
1599 edit_message_editor.focus_handle(cx);
1600 this.child(
1601 Button::new("cancel-edit-message", "Cancel")
1602 .label_size(LabelSize::Small)
1603 .key_binding(
1604 KeyBinding::for_action_in(
1605 &menu::Cancel,
1606 &focus_handle,
1607 window,
1608 cx,
1609 )
1610 .map(|kb| kb.size(rems_from_px(12.))),
1611 )
1612 .on_click(
1613 cx.listener(Self::handle_cancel_click),
1614 ),
1615 )
1616 .child(
1617 Button::new(
1618 "confirm-edit-message",
1619 "Regenerate",
1620 )
1621 .label_size(LabelSize::Small)
1622 .key_binding(
1623 KeyBinding::for_action_in(
1624 &menu::Confirm,
1625 &focus_handle,
1626 window,
1627 cx,
1628 )
1629 .map(|kb| kb.size(rems_from_px(12.))),
1630 )
1631 .on_click(
1632 cx.listener(Self::handle_regenerate_click),
1633 ),
1634 )
1635 },
1636 )
1637 .when(
1638 edit_message_editor.is_none() && allow_editing_message,
1639 |this| {
1640 this.child(
1641 Button::new("edit-message", "Edit")
1642 .label_size(LabelSize::Small)
1643 .on_click(cx.listener({
1644 let message_segments =
1645 message.segments.clone();
1646 move |this, _, window, cx| {
1647 this.start_editing_message(
1648 message_id,
1649 &message_segments,
1650 window,
1651 cx,
1652 );
1653 }
1654 })),
1655 )
1656 },
1657 ),
1658 ),
1659 )
1660 .child(div().p_2().children(message_content)),
1661 ),
1662 Role::Assistant => v_flex()
1663 .id(("message-container", ix))
1664 .ml_2()
1665 .pl_2()
1666 .pr_4()
1667 .border_l_1()
1668 .border_color(cx.theme().colors().border_variant)
1669 .children(message_content)
1670 .when(has_tool_uses, |parent| {
1671 parent.children(
1672 tool_uses
1673 .into_iter()
1674 .map(|tool_use| self.render_tool_use(tool_use, window, cx)),
1675 )
1676 }),
1677 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1678 v_flex()
1679 .bg(colors.editor_background)
1680 .rounded_sm()
1681 .child(div().p_4().children(message_content)),
1682 ),
1683 };
1684
1685 v_flex()
1686 .w_full()
1687 .when_some(checkpoint, |parent, checkpoint| {
1688 let mut is_pending = false;
1689 let mut error = None;
1690 if let Some(last_restore_checkpoint) =
1691 self.thread.read(cx).last_restore_checkpoint()
1692 {
1693 if last_restore_checkpoint.message_id() == message_id {
1694 match last_restore_checkpoint {
1695 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1696 LastRestoreCheckpoint::Error { error: err, .. } => {
1697 error = Some(err.clone());
1698 }
1699 }
1700 }
1701 }
1702
1703 let restore_checkpoint_button =
1704 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1705 .icon(if error.is_some() {
1706 IconName::XCircle
1707 } else {
1708 IconName::Undo
1709 })
1710 .icon_size(IconSize::XSmall)
1711 .icon_position(IconPosition::Start)
1712 .icon_color(if error.is_some() {
1713 Some(Color::Error)
1714 } else {
1715 None
1716 })
1717 .label_size(LabelSize::XSmall)
1718 .disabled(is_pending)
1719 .on_click(cx.listener(move |this, _, _window, cx| {
1720 this.thread.update(cx, |thread, cx| {
1721 thread
1722 .restore_checkpoint(checkpoint.clone(), cx)
1723 .detach_and_log_err(cx);
1724 });
1725 }));
1726
1727 let restore_checkpoint_button = if is_pending {
1728 restore_checkpoint_button
1729 .with_animation(
1730 ("pulsating-restore-checkpoint-button", ix),
1731 Animation::new(Duration::from_secs(2))
1732 .repeat()
1733 .with_easing(pulsating_between(0.6, 1.)),
1734 |label, delta| label.alpha(delta),
1735 )
1736 .into_any_element()
1737 } else if let Some(error) = error {
1738 restore_checkpoint_button
1739 .tooltip(Tooltip::text(error.to_string()))
1740 .into_any_element()
1741 } else {
1742 restore_checkpoint_button.into_any_element()
1743 };
1744
1745 parent.child(
1746 h_flex()
1747 .pt_2p5()
1748 .px_2p5()
1749 .w_full()
1750 .gap_1()
1751 .child(ui::Divider::horizontal())
1752 .child(restore_checkpoint_button)
1753 .child(ui::Divider::horizontal()),
1754 )
1755 })
1756 .when(is_first_message, |parent| {
1757 parent.child(self.render_rules_item(cx))
1758 })
1759 .child(styled_message)
1760 .when(!needs_confirmation && generating_label.is_some(), |this| {
1761 this.child(
1762 h_flex()
1763 .h_8()
1764 .mt_2()
1765 .mb_4()
1766 .ml_4()
1767 .py_1p5()
1768 .child(generating_label.unwrap()),
1769 )
1770 })
1771 .when(show_feedback, move |parent| {
1772 parent.child(feedback_items).when_some(
1773 self.open_feedback_editors.get(&message_id),
1774 move |parent, feedback_editor| {
1775 let focus_handle = feedback_editor.focus_handle(cx);
1776 parent.child(
1777 v_flex()
1778 .key_context("AgentFeedbackMessageEditor")
1779 .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
1780 this.open_feedback_editors.remove(&message_id);
1781 cx.notify();
1782 }))
1783 .on_action(cx.listener(move |this, _: &menu::Confirm, _, cx| {
1784 this.submit_feedback_message(message_id, cx);
1785 cx.notify();
1786 }))
1787 .on_action(cx.listener(Self::confirm_editing_message))
1788 .mb_2()
1789 .mx_4()
1790 .p_2()
1791 .rounded_md()
1792 .border_1()
1793 .border_color(cx.theme().colors().border)
1794 .bg(cx.theme().colors().editor_background)
1795 .child(feedback_editor.clone())
1796 .child(
1797 h_flex()
1798 .gap_1()
1799 .justify_end()
1800 .child(
1801 Button::new("dismiss-feedback-message", "Cancel")
1802 .label_size(LabelSize::Small)
1803 .key_binding(
1804 KeyBinding::for_action_in(
1805 &menu::Cancel,
1806 &focus_handle,
1807 window,
1808 cx,
1809 )
1810 .map(|kb| kb.size(rems_from_px(10.))),
1811 )
1812 .on_click(cx.listener(
1813 move |this, _, _window, cx| {
1814 this.open_feedback_editors
1815 .remove(&message_id);
1816 cx.notify();
1817 },
1818 )),
1819 )
1820 .child(
1821 Button::new(
1822 "submit-feedback-message",
1823 "Share Feedback",
1824 )
1825 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
1826 .label_size(LabelSize::Small)
1827 .key_binding(
1828 KeyBinding::for_action_in(
1829 &menu::Confirm,
1830 &focus_handle,
1831 window,
1832 cx,
1833 )
1834 .map(|kb| kb.size(rems_from_px(10.))),
1835 )
1836 .on_click(
1837 cx.listener(move |this, _, _window, cx| {
1838 this.submit_feedback_message(message_id, cx);
1839 cx.notify()
1840 }),
1841 ),
1842 ),
1843 ),
1844 )
1845 },
1846 )
1847 })
1848 .into_any()
1849 }
1850
1851 fn render_message_content(
1852 &self,
1853 message_id: MessageId,
1854 rendered_message: &RenderedMessage,
1855 has_tool_uses: bool,
1856 workspace: WeakEntity<Workspace>,
1857 window: &Window,
1858 cx: &Context<Self>,
1859 ) -> impl IntoElement {
1860 let is_last_message = self.messages.last() == Some(&message_id);
1861 let is_generating = self.thread.read(cx).is_generating();
1862 let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses {
1863 rendered_message
1864 .segments
1865 .iter()
1866 .enumerate()
1867 .next_back()
1868 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1869 .map(|(index, _)| index)
1870 } else {
1871 None
1872 };
1873
1874 v_flex()
1875 .text_ui(cx)
1876 .gap_2()
1877 .children(
1878 rendered_message.segments.iter().enumerate().map(
1879 |(index, segment)| match segment {
1880 RenderedMessageSegment::Thinking {
1881 content,
1882 scroll_handle,
1883 } => self
1884 .render_message_thinking_segment(
1885 message_id,
1886 index,
1887 content.clone(),
1888 &scroll_handle,
1889 Some(index) == pending_thinking_segment_index,
1890 window,
1891 cx,
1892 )
1893 .into_any_element(),
1894 RenderedMessageSegment::Text(markdown) => div()
1895 .child(
1896 MarkdownElement::new(
1897 markdown.clone(),
1898 default_markdown_style(window, cx),
1899 )
1900 .code_block_renderer(markdown::CodeBlockRenderer::Custom {
1901 render: Arc::new({
1902 let workspace = workspace.clone();
1903 let active_thread = cx.entity();
1904 move |kind, parsed_markdown, range, window, cx| {
1905 render_markdown_code_block(
1906 message_id,
1907 range.start,
1908 kind,
1909 parsed_markdown,
1910 range,
1911 active_thread.clone(),
1912 workspace.clone(),
1913 window,
1914 cx,
1915 )
1916 }
1917 }),
1918 transform: Some(Arc::new({
1919 let active_thread = cx.entity();
1920 move |el, range, _, cx| {
1921 let is_expanded = active_thread
1922 .read(cx)
1923 .expanded_code_blocks
1924 .get(&(message_id, range.start))
1925 .copied()
1926 .unwrap_or(false);
1927
1928 if is_expanded {
1929 return el;
1930 }
1931 el.child(
1932 div()
1933 .absolute()
1934 .bottom_0()
1935 .left_0()
1936 .w_full()
1937 .h_1_4()
1938 .rounded_b_lg()
1939 .bg(gpui::linear_gradient(
1940 0.,
1941 gpui::linear_color_stop(
1942 cx.theme().colors().editor_background,
1943 0.,
1944 ),
1945 gpui::linear_color_stop(
1946 cx.theme()
1947 .colors()
1948 .editor_background
1949 .opacity(0.),
1950 1.,
1951 ),
1952 )),
1953 )
1954 }
1955 })),
1956 })
1957 .on_url_click({
1958 let workspace = self.workspace.clone();
1959 move |text, window, cx| {
1960 open_markdown_link(text, workspace.clone(), window, cx);
1961 }
1962 }),
1963 )
1964 .into_any_element(),
1965 },
1966 ),
1967 )
1968 }
1969
1970 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1971 cx.theme().colors().border.opacity(0.5)
1972 }
1973
1974 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1975 cx.theme()
1976 .colors()
1977 .element_background
1978 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1979 }
1980
1981 fn render_message_thinking_segment(
1982 &self,
1983 message_id: MessageId,
1984 ix: usize,
1985 markdown: Entity<Markdown>,
1986 scroll_handle: &ScrollHandle,
1987 pending: bool,
1988 window: &Window,
1989 cx: &Context<Self>,
1990 ) -> impl IntoElement {
1991 let is_open = self
1992 .expanded_thinking_segments
1993 .get(&(message_id, ix))
1994 .copied()
1995 .unwrap_or_default();
1996
1997 let editor_bg = cx.theme().colors().panel_background;
1998
1999 div().map(|this| {
2000 if pending {
2001 this.v_flex()
2002 .mt_neg_2()
2003 .mb_1p5()
2004 .child(
2005 h_flex()
2006 .group("disclosure-header")
2007 .justify_between()
2008 .child(
2009 h_flex()
2010 .gap_1p5()
2011 .child(
2012 Icon::new(IconName::LightBulb)
2013 .size(IconSize::XSmall)
2014 .color(Color::Muted),
2015 )
2016 .child({
2017 Label::new("Thinking")
2018 .color(Color::Muted)
2019 .size(LabelSize::Small)
2020 .with_animation(
2021 "generating-label",
2022 Animation::new(Duration::from_secs(1)).repeat(),
2023 |mut label, delta| {
2024 let text = match delta {
2025 d if d < 0.25 => "Thinking",
2026 d if d < 0.5 => "Thinking.",
2027 d if d < 0.75 => "Thinking..",
2028 _ => "Thinking...",
2029 };
2030 label.set_text(text);
2031 label
2032 },
2033 )
2034 .with_animation(
2035 "pulsating-label",
2036 Animation::new(Duration::from_secs(2))
2037 .repeat()
2038 .with_easing(pulsating_between(0.6, 1.)),
2039 |label, delta| {
2040 label.map_element(|label| label.alpha(delta))
2041 },
2042 )
2043 }),
2044 )
2045 .child(
2046 h_flex()
2047 .gap_1()
2048 .child(
2049 div().visible_on_hover("disclosure-header").child(
2050 Disclosure::new("thinking-disclosure", is_open)
2051 .opened_icon(IconName::ChevronUp)
2052 .closed_icon(IconName::ChevronDown)
2053 .on_click(cx.listener({
2054 move |this, _event, _window, _cx| {
2055 let is_open = this
2056 .expanded_thinking_segments
2057 .entry((message_id, ix))
2058 .or_insert(false);
2059
2060 *is_open = !*is_open;
2061 }
2062 })),
2063 ),
2064 )
2065 .child({
2066 Icon::new(IconName::ArrowCircle)
2067 .color(Color::Accent)
2068 .size(IconSize::Small)
2069 .with_animation(
2070 "arrow-circle",
2071 Animation::new(Duration::from_secs(2)).repeat(),
2072 |icon, delta| {
2073 icon.transform(Transformation::rotate(
2074 percentage(delta),
2075 ))
2076 },
2077 )
2078 }),
2079 ),
2080 )
2081 .when(!is_open, |this| {
2082 let gradient_overlay = div()
2083 .rounded_b_lg()
2084 .h_full()
2085 .absolute()
2086 .w_full()
2087 .bottom_0()
2088 .left_0()
2089 .bg(linear_gradient(
2090 180.,
2091 linear_color_stop(editor_bg, 1.),
2092 linear_color_stop(editor_bg.opacity(0.2), 0.),
2093 ));
2094
2095 this.child(
2096 div()
2097 .relative()
2098 .bg(editor_bg)
2099 .rounded_b_lg()
2100 .mt_2()
2101 .pl_4()
2102 .child(
2103 div()
2104 .id(("thinking-content", ix))
2105 .max_h_20()
2106 .track_scroll(scroll_handle)
2107 .text_ui_sm(cx)
2108 .overflow_hidden()
2109 .child(
2110 MarkdownElement::new(
2111 markdown.clone(),
2112 default_markdown_style(window, cx),
2113 )
2114 .on_url_click({
2115 let workspace = self.workspace.clone();
2116 move |text, window, cx| {
2117 open_markdown_link(
2118 text,
2119 workspace.clone(),
2120 window,
2121 cx,
2122 );
2123 }
2124 }),
2125 ),
2126 )
2127 .child(gradient_overlay),
2128 )
2129 })
2130 .when(is_open, |this| {
2131 this.child(
2132 div()
2133 .id(("thinking-content", ix))
2134 .h_full()
2135 .bg(editor_bg)
2136 .text_ui_sm(cx)
2137 .child(
2138 MarkdownElement::new(
2139 markdown.clone(),
2140 default_markdown_style(window, cx),
2141 )
2142 .on_url_click({
2143 let workspace = self.workspace.clone();
2144 move |text, window, cx| {
2145 open_markdown_link(text, workspace.clone(), window, cx);
2146 }
2147 }),
2148 ),
2149 )
2150 })
2151 } else {
2152 this.v_flex()
2153 .mt_neg_2()
2154 .child(
2155 h_flex()
2156 .group("disclosure-header")
2157 .pr_1()
2158 .justify_between()
2159 .opacity(0.8)
2160 .hover(|style| style.opacity(1.))
2161 .child(
2162 h_flex()
2163 .gap_1p5()
2164 .child(
2165 Icon::new(IconName::LightBulb)
2166 .size(IconSize::XSmall)
2167 .color(Color::Muted),
2168 )
2169 .child(Label::new("Thought Process").size(LabelSize::Small)),
2170 )
2171 .child(
2172 div().visible_on_hover("disclosure-header").child(
2173 Disclosure::new("thinking-disclosure", is_open)
2174 .opened_icon(IconName::ChevronUp)
2175 .closed_icon(IconName::ChevronDown)
2176 .on_click(cx.listener({
2177 move |this, _event, _window, _cx| {
2178 let is_open = this
2179 .expanded_thinking_segments
2180 .entry((message_id, ix))
2181 .or_insert(false);
2182
2183 *is_open = !*is_open;
2184 }
2185 })),
2186 ),
2187 ),
2188 )
2189 .child(
2190 div()
2191 .id(("thinking-content", ix))
2192 .relative()
2193 .mt_1p5()
2194 .ml_1p5()
2195 .pl_2p5()
2196 .border_l_1()
2197 .border_color(cx.theme().colors().border_variant)
2198 .text_ui_sm(cx)
2199 .when(is_open, |this| {
2200 this.child(
2201 MarkdownElement::new(
2202 markdown.clone(),
2203 default_markdown_style(window, cx),
2204 )
2205 .on_url_click({
2206 let workspace = self.workspace.clone();
2207 move |text, window, cx| {
2208 open_markdown_link(text, workspace.clone(), window, cx);
2209 }
2210 }),
2211 )
2212 }),
2213 )
2214 }
2215 })
2216 }
2217
2218 fn render_tool_use(
2219 &self,
2220 tool_use: ToolUse,
2221 window: &mut Window,
2222 cx: &mut Context<Self>,
2223 ) -> impl IntoElement + use<> {
2224 let is_open = self
2225 .expanded_tool_uses
2226 .get(&tool_use.id)
2227 .copied()
2228 .unwrap_or_default();
2229
2230 let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
2231
2232 let fs = self
2233 .workspace
2234 .upgrade()
2235 .map(|workspace| workspace.read(cx).app_state().fs.clone());
2236 let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
2237 let edit_tools = tool_use.needs_confirmation;
2238
2239 let status_icons = div().child(match &tool_use.status {
2240 ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
2241 let icon = Icon::new(IconName::Warning)
2242 .color(Color::Warning)
2243 .size(IconSize::Small);
2244 icon.into_any_element()
2245 }
2246 ToolUseStatus::Running => {
2247 let icon = Icon::new(IconName::ArrowCircle)
2248 .color(Color::Accent)
2249 .size(IconSize::Small);
2250 icon.with_animation(
2251 "arrow-circle",
2252 Animation::new(Duration::from_secs(2)).repeat(),
2253 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
2254 )
2255 .into_any_element()
2256 }
2257 ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
2258 ToolUseStatus::Error(_) => {
2259 let icon = Icon::new(IconName::Close)
2260 .color(Color::Error)
2261 .size(IconSize::Small);
2262 icon.into_any_element()
2263 }
2264 });
2265
2266 let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
2267 let results_content_container = || v_flex().p_2().gap_0p5();
2268
2269 let results_content = v_flex()
2270 .gap_1()
2271 .child(
2272 results_content_container()
2273 .child(
2274 Label::new("Input")
2275 .size(LabelSize::XSmall)
2276 .color(Color::Muted)
2277 .buffer_font(cx),
2278 )
2279 .child(
2280 div()
2281 .w_full()
2282 .text_ui_sm(cx)
2283 .children(rendered_tool_use.as_ref().map(|rendered| {
2284 MarkdownElement::new(
2285 rendered.input.clone(),
2286 tool_use_markdown_style(window, cx),
2287 )
2288 .on_url_click({
2289 let workspace = self.workspace.clone();
2290 move |text, window, cx| {
2291 open_markdown_link(text, workspace.clone(), window, cx);
2292 }
2293 })
2294 })),
2295 ),
2296 )
2297 .map(|container| match tool_use.status {
2298 ToolUseStatus::Finished(_) => container.child(
2299 results_content_container()
2300 .border_t_1()
2301 .border_color(self.tool_card_border_color(cx))
2302 .child(
2303 Label::new("Result")
2304 .size(LabelSize::XSmall)
2305 .color(Color::Muted)
2306 .buffer_font(cx),
2307 )
2308 .child(div().w_full().text_ui_sm(cx).children(
2309 rendered_tool_use.as_ref().map(|rendered| {
2310 MarkdownElement::new(
2311 rendered.output.clone(),
2312 tool_use_markdown_style(window, cx),
2313 )
2314 .on_url_click({
2315 let workspace = self.workspace.clone();
2316 move |text, window, cx| {
2317 open_markdown_link(text, workspace.clone(), window, cx);
2318 }
2319 })
2320 }),
2321 )),
2322 ),
2323 ToolUseStatus::Running => container.child(
2324 results_content_container().child(
2325 h_flex()
2326 .gap_1()
2327 .pb_1()
2328 .border_t_1()
2329 .border_color(self.tool_card_border_color(cx))
2330 .child(
2331 Icon::new(IconName::ArrowCircle)
2332 .size(IconSize::Small)
2333 .color(Color::Accent)
2334 .with_animation(
2335 "arrow-circle",
2336 Animation::new(Duration::from_secs(2)).repeat(),
2337 |icon, delta| {
2338 icon.transform(Transformation::rotate(percentage(
2339 delta,
2340 )))
2341 },
2342 ),
2343 )
2344 .child(
2345 Label::new("Running…")
2346 .size(LabelSize::XSmall)
2347 .color(Color::Muted)
2348 .buffer_font(cx),
2349 ),
2350 ),
2351 ),
2352 ToolUseStatus::Error(_) => container.child(
2353 results_content_container()
2354 .border_t_1()
2355 .border_color(self.tool_card_border_color(cx))
2356 .child(
2357 Label::new("Error")
2358 .size(LabelSize::XSmall)
2359 .color(Color::Muted)
2360 .buffer_font(cx),
2361 )
2362 .child(
2363 div()
2364 .text_ui_sm(cx)
2365 .children(rendered_tool_use.as_ref().map(|rendered| {
2366 MarkdownElement::new(
2367 rendered.output.clone(),
2368 tool_use_markdown_style(window, cx),
2369 )
2370 .on_url_click({
2371 let workspace = self.workspace.clone();
2372 move |text, window, cx| {
2373 open_markdown_link(text, workspace.clone(), window, cx);
2374 }
2375 })
2376 })),
2377 ),
2378 ),
2379 ToolUseStatus::Pending => container,
2380 ToolUseStatus::NeedsConfirmation => container.child(
2381 results_content_container()
2382 .border_t_1()
2383 .border_color(self.tool_card_border_color(cx))
2384 .child(
2385 Label::new("Asking Permission")
2386 .size(LabelSize::Small)
2387 .color(Color::Muted)
2388 .buffer_font(cx),
2389 ),
2390 ),
2391 });
2392
2393 let gradient_overlay = |color: Hsla| {
2394 div()
2395 .h_full()
2396 .absolute()
2397 .w_12()
2398 .bottom_0()
2399 .map(|element| {
2400 if is_status_finished {
2401 element.right_6()
2402 } else {
2403 element.right(px(44.))
2404 }
2405 })
2406 .bg(linear_gradient(
2407 90.,
2408 linear_color_stop(color, 1.),
2409 linear_color_stop(color.opacity(0.2), 0.),
2410 ))
2411 };
2412
2413 div().map(|element| {
2414 if !edit_tools {
2415 element.child(
2416 v_flex()
2417 .my_2()
2418 .child(
2419 h_flex()
2420 .group("disclosure-header")
2421 .relative()
2422 .gap_1p5()
2423 .justify_between()
2424 .opacity(0.8)
2425 .hover(|style| style.opacity(1.))
2426 .when(!is_status_finished, |this| this.pr_2())
2427 .child(
2428 h_flex()
2429 .id("tool-label-container")
2430 .gap_1p5()
2431 .max_w_full()
2432 .overflow_x_scroll()
2433 .child(
2434 Icon::new(tool_use.icon)
2435 .size(IconSize::XSmall)
2436 .color(Color::Muted),
2437 )
2438 .child(
2439 h_flex().pr_8().text_ui_sm(cx).children(
2440 rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| {
2441 open_markdown_link(text, workspace.clone(), window, cx);
2442 }}))
2443 ),
2444 ),
2445 )
2446 .child(
2447 h_flex()
2448 .gap_1()
2449 .child(
2450 div().visible_on_hover("disclosure-header").child(
2451 Disclosure::new("tool-use-disclosure", is_open)
2452 .opened_icon(IconName::ChevronUp)
2453 .closed_icon(IconName::ChevronDown)
2454 .on_click(cx.listener({
2455 let tool_use_id = tool_use.id.clone();
2456 move |this, _event, _window, _cx| {
2457 let is_open = this
2458 .expanded_tool_uses
2459 .entry(tool_use_id.clone())
2460 .or_insert(false);
2461
2462 *is_open = !*is_open;
2463 }
2464 })),
2465 ),
2466 )
2467 .child(status_icons),
2468 )
2469 .child(gradient_overlay(cx.theme().colors().panel_background)),
2470 )
2471 .map(|parent| {
2472 if !is_open {
2473 return parent;
2474 }
2475
2476 parent.child(
2477 v_flex()
2478 .mt_1()
2479 .border_1()
2480 .border_color(self.tool_card_border_color(cx))
2481 .bg(cx.theme().colors().editor_background)
2482 .rounded_lg()
2483 .child(results_content),
2484 )
2485 }),
2486 )
2487 } else {
2488 v_flex()
2489 .my_3()
2490 .rounded_lg()
2491 .border_1()
2492 .border_color(self.tool_card_border_color(cx))
2493 .overflow_hidden()
2494 .child(
2495 h_flex()
2496 .group("disclosure-header")
2497 .relative()
2498 .justify_between()
2499 .py_1()
2500 .map(|element| {
2501 if is_status_finished {
2502 element.pl_2().pr_0p5()
2503 } else {
2504 element.px_2()
2505 }
2506 })
2507 .bg(self.tool_card_header_bg(cx))
2508 .map(|element| {
2509 if is_open {
2510 element.border_b_1().rounded_t_md()
2511 } else if needs_confirmation {
2512 element.rounded_t_md()
2513 } else {
2514 element.rounded_md()
2515 }
2516 })
2517 .border_color(self.tool_card_border_color(cx))
2518 .child(
2519 h_flex()
2520 .id("tool-label-container")
2521 .gap_1p5()
2522 .max_w_full()
2523 .overflow_x_scroll()
2524 .child(
2525 Icon::new(tool_use.icon)
2526 .size(IconSize::XSmall)
2527 .color(Color::Muted),
2528 )
2529 .child(
2530 h_flex().pr_8().text_ui_sm(cx).children(
2531 rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| {
2532 open_markdown_link(text, workspace.clone(), window, cx);
2533 }}))
2534 ),
2535 ),
2536 )
2537 .child(
2538 h_flex()
2539 .gap_1()
2540 .child(
2541 div().visible_on_hover("disclosure-header").child(
2542 Disclosure::new("tool-use-disclosure", is_open)
2543 .opened_icon(IconName::ChevronUp)
2544 .closed_icon(IconName::ChevronDown)
2545 .on_click(cx.listener({
2546 let tool_use_id = tool_use.id.clone();
2547 move |this, _event, _window, _cx| {
2548 let is_open = this
2549 .expanded_tool_uses
2550 .entry(tool_use_id.clone())
2551 .or_insert(false);
2552
2553 *is_open = !*is_open;
2554 }
2555 })),
2556 ),
2557 )
2558 .child(status_icons),
2559 )
2560 .child(gradient_overlay(self.tool_card_header_bg(cx))),
2561 )
2562 .map(|parent| {
2563 if !is_open {
2564 return parent;
2565 }
2566
2567 parent.child(
2568 v_flex()
2569 .bg(cx.theme().colors().editor_background)
2570 .map(|element| {
2571 if needs_confirmation {
2572 element.rounded_none()
2573 } else {
2574 element.rounded_b_lg()
2575 }
2576 })
2577 .child(results_content),
2578 )
2579 })
2580 .when(needs_confirmation, |this| {
2581 this.child(
2582 h_flex()
2583 .py_1()
2584 .pl_2()
2585 .pr_1()
2586 .gap_1()
2587 .justify_between()
2588 .bg(cx.theme().colors().editor_background)
2589 .border_t_1()
2590 .border_color(self.tool_card_border_color(cx))
2591 .rounded_b_lg()
2592 .child(
2593 Label::new("Waiting for Confirmation…")
2594 .color(Color::Muted)
2595 .size(LabelSize::Small)
2596 .with_animation(
2597 "generating-label",
2598 Animation::new(Duration::from_secs(1)).repeat(),
2599 |mut label, delta| {
2600 let text = match delta {
2601 d if d < 0.25 => "Waiting for Confirmation",
2602 d if d < 0.5 => "Waiting for Confirmation.",
2603 d if d < 0.75 => "Waiting for Confirmation..",
2604 _ => "Waiting for Confirmation...",
2605 };
2606 label.set_text(text);
2607 label
2608 },
2609 )
2610 .with_animation(
2611 "pulsating-label",
2612 Animation::new(Duration::from_secs(2))
2613 .repeat()
2614 .with_easing(pulsating_between(0.6, 1.)),
2615 |label, delta| label.map_element(|label| label.alpha(delta)),
2616 ),
2617 )
2618 .child(
2619 h_flex()
2620 .gap_0p5()
2621 .child({
2622 let tool_id = tool_use.id.clone();
2623 Button::new(
2624 "always-allow-tool-action",
2625 "Always Allow",
2626 )
2627 .label_size(LabelSize::Small)
2628 .icon(IconName::CheckDouble)
2629 .icon_position(IconPosition::Start)
2630 .icon_size(IconSize::Small)
2631 .icon_color(Color::Success)
2632 .tooltip(move |window, cx| {
2633 Tooltip::with_meta(
2634 "Never ask for permission",
2635 None,
2636 "Restore the original behavior in your Agent Panel settings",
2637 window,
2638 cx,
2639 )
2640 })
2641 .on_click(cx.listener(
2642 move |this, event, window, cx| {
2643 if let Some(fs) = fs.clone() {
2644 update_settings_file::<AssistantSettings>(
2645 fs.clone(),
2646 cx,
2647 |settings, _| {
2648 settings.set_always_allow_tool_actions(true);
2649 },
2650 );
2651 }
2652 this.handle_allow_tool(
2653 tool_id.clone(),
2654 event,
2655 window,
2656 cx,
2657 )
2658 },
2659 ))
2660 })
2661 .child(ui::Divider::vertical())
2662 .child({
2663 let tool_id = tool_use.id.clone();
2664 Button::new("allow-tool-action", "Allow")
2665 .label_size(LabelSize::Small)
2666 .icon(IconName::Check)
2667 .icon_position(IconPosition::Start)
2668 .icon_size(IconSize::Small)
2669 .icon_color(Color::Success)
2670 .on_click(cx.listener(
2671 move |this, event, window, cx| {
2672 this.handle_allow_tool(
2673 tool_id.clone(),
2674 event,
2675 window,
2676 cx,
2677 )
2678 },
2679 ))
2680 })
2681 .child({
2682 let tool_id = tool_use.id.clone();
2683 let tool_name: Arc<str> = tool_use.name.into();
2684 Button::new("deny-tool", "Deny")
2685 .label_size(LabelSize::Small)
2686 .icon(IconName::Close)
2687 .icon_position(IconPosition::Start)
2688 .icon_size(IconSize::Small)
2689 .icon_color(Color::Error)
2690 .on_click(cx.listener(
2691 move |this, event, window, cx| {
2692 this.handle_deny_tool(
2693 tool_id.clone(),
2694 tool_name.clone(),
2695 event,
2696 window,
2697 cx,
2698 )
2699 },
2700 ))
2701 }),
2702 ),
2703 )
2704 })
2705 }
2706 })
2707 }
2708
2709 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
2710 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2711 else {
2712 return div().into_any();
2713 };
2714
2715 let rules_files = system_prompt_context
2716 .worktrees
2717 .iter()
2718 .filter_map(|worktree| worktree.rules_file.as_ref())
2719 .collect::<Vec<_>>();
2720
2721 let label_text = match rules_files.as_slice() {
2722 &[] => return div().into_any(),
2723 &[rules_file] => {
2724 format!("Using {:?} file", rules_file.path_in_worktree)
2725 }
2726 rules_files => {
2727 format!("Using {} rules files", rules_files.len())
2728 }
2729 };
2730
2731 div()
2732 .pt_2()
2733 .px_2p5()
2734 .child(
2735 h_flex()
2736 .w_full()
2737 .gap_0p5()
2738 .child(
2739 h_flex()
2740 .gap_1p5()
2741 .child(
2742 Icon::new(IconName::File)
2743 .size(IconSize::XSmall)
2744 .color(Color::Disabled),
2745 )
2746 .child(
2747 Label::new(label_text)
2748 .size(LabelSize::XSmall)
2749 .color(Color::Muted)
2750 .buffer_font(cx),
2751 ),
2752 )
2753 .child(
2754 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
2755 .shape(ui::IconButtonShape::Square)
2756 .icon_size(IconSize::XSmall)
2757 .icon_color(Color::Ignored)
2758 .on_click(cx.listener(Self::handle_open_rules))
2759 .tooltip(Tooltip::text("View Rules")),
2760 ),
2761 )
2762 .into_any()
2763 }
2764
2765 fn handle_allow_tool(
2766 &mut self,
2767 tool_use_id: LanguageModelToolUseId,
2768 _: &ClickEvent,
2769 _window: &mut Window,
2770 cx: &mut Context<Self>,
2771 ) {
2772 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
2773 .thread
2774 .read(cx)
2775 .pending_tool(&tool_use_id)
2776 .map(|tool_use| tool_use.status.clone())
2777 {
2778 self.thread.update(cx, |thread, cx| {
2779 thread.run_tool(
2780 c.tool_use_id.clone(),
2781 c.ui_text.clone(),
2782 c.input.clone(),
2783 &c.messages,
2784 c.tool.clone(),
2785 cx,
2786 );
2787 });
2788 }
2789 }
2790
2791 fn handle_deny_tool(
2792 &mut self,
2793 tool_use_id: LanguageModelToolUseId,
2794 tool_name: Arc<str>,
2795 _: &ClickEvent,
2796 _window: &mut Window,
2797 cx: &mut Context<Self>,
2798 ) {
2799 self.thread.update(cx, |thread, cx| {
2800 thread.deny_tool_use(tool_use_id, tool_name, cx);
2801 });
2802 }
2803
2804 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
2805 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2806 else {
2807 return;
2808 };
2809
2810 let abs_paths = system_prompt_context
2811 .worktrees
2812 .iter()
2813 .flat_map(|worktree| worktree.rules_file.as_ref())
2814 .map(|rules_file| rules_file.abs_path.to_path_buf())
2815 .collect::<Vec<_>>();
2816
2817 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
2818 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
2819 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
2820 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
2821 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
2822 }) {
2823 task.detach();
2824 }
2825 }
2826
2827 fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
2828 for window in self.notifications.drain(..) {
2829 window
2830 .update(cx, |_, window, _| {
2831 window.remove_window();
2832 })
2833 .ok();
2834
2835 self.notification_subscriptions.remove(&window);
2836 }
2837 }
2838
2839 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
2840 if !self.show_scrollbar && !self.scrollbar_state.is_dragging() {
2841 return None;
2842 }
2843
2844 Some(
2845 div()
2846 .occlude()
2847 .id("active-thread-scrollbar")
2848 .on_mouse_move(cx.listener(|_, _, _, cx| {
2849 cx.notify();
2850 cx.stop_propagation()
2851 }))
2852 .on_hover(|_, _, cx| {
2853 cx.stop_propagation();
2854 })
2855 .on_any_mouse_down(|_, _, cx| {
2856 cx.stop_propagation();
2857 })
2858 .on_mouse_up(
2859 MouseButton::Left,
2860 cx.listener(|_, _, _, cx| {
2861 cx.stop_propagation();
2862 }),
2863 )
2864 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2865 cx.notify();
2866 }))
2867 .h_full()
2868 .absolute()
2869 .right_1()
2870 .top_1()
2871 .bottom_0()
2872 .w(px(12.))
2873 .cursor_default()
2874 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
2875 )
2876 }
2877
2878 fn hide_scrollbar_later(&mut self, cx: &mut Context<Self>) {
2879 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
2880 self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| {
2881 cx.background_executor()
2882 .timer(SCROLLBAR_SHOW_INTERVAL)
2883 .await;
2884 thread
2885 .update(cx, |thread, cx| {
2886 if !thread.scrollbar_state.is_dragging() {
2887 thread.show_scrollbar = false;
2888 cx.notify();
2889 }
2890 })
2891 .log_err();
2892 }))
2893 }
2894}
2895
2896impl Render for ActiveThread {
2897 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2898 v_flex()
2899 .size_full()
2900 .relative()
2901 .on_mouse_move(cx.listener(|this, _, _, cx| {
2902 this.show_scrollbar = true;
2903 this.hide_scrollbar_later(cx);
2904 cx.notify();
2905 }))
2906 .on_scroll_wheel(cx.listener(|this, _, _, cx| {
2907 this.show_scrollbar = true;
2908 this.hide_scrollbar_later(cx);
2909 cx.notify();
2910 }))
2911 .on_mouse_up(
2912 MouseButton::Left,
2913 cx.listener(|this, _, _, cx| {
2914 this.hide_scrollbar_later(cx);
2915 }),
2916 )
2917 .child(list(self.list_state.clone()).flex_grow())
2918 .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
2919 this.child(scrollbar)
2920 })
2921 }
2922}
2923
2924pub(crate) fn open_context(
2925 id: ContextId,
2926 context_store: Entity<ContextStore>,
2927 workspace: Entity<Workspace>,
2928 window: &mut Window,
2929 cx: &mut App,
2930) {
2931 let Some(context) = context_store.read(cx).context_for_id(id) else {
2932 return;
2933 };
2934
2935 match context {
2936 AssistantContext::File(file_context) => {
2937 if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
2938 {
2939 workspace.update(cx, |workspace, cx| {
2940 workspace
2941 .open_path(project_path, None, true, window, cx)
2942 .detach_and_log_err(cx);
2943 });
2944 }
2945 }
2946 AssistantContext::Directory(directory_context) => {
2947 let project_path = directory_context.project_path(cx);
2948 workspace.update(cx, |workspace, cx| {
2949 workspace.project().update(cx, |project, cx| {
2950 if let Some(entry) = project.entry_for_path(&project_path, cx) {
2951 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2952 }
2953 })
2954 })
2955 }
2956 AssistantContext::Symbol(symbol_context) => {
2957 if let Some(project_path) = symbol_context
2958 .context_symbol
2959 .buffer
2960 .read(cx)
2961 .project_path(cx)
2962 {
2963 let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
2964 let target_position = symbol_context
2965 .context_symbol
2966 .id
2967 .range
2968 .start
2969 .to_point(&snapshot);
2970
2971 let open_task = workspace.update(cx, |workspace, cx| {
2972 workspace.open_path(project_path, None, true, window, cx)
2973 });
2974 window
2975 .spawn(cx, async move |cx| {
2976 if let Some(active_editor) = open_task
2977 .await
2978 .log_err()
2979 .and_then(|item| item.downcast::<Editor>())
2980 {
2981 active_editor
2982 .downgrade()
2983 .update_in(cx, |editor, window, cx| {
2984 editor.go_to_singleton_buffer_point(
2985 target_position,
2986 window,
2987 cx,
2988 );
2989 })
2990 .log_err();
2991 }
2992 })
2993 .detach();
2994 }
2995 }
2996 AssistantContext::FetchedUrl(fetched_url_context) => {
2997 cx.open_url(&fetched_url_context.url);
2998 }
2999 AssistantContext::Thread(thread_context) => {
3000 let thread_id = thread_context.thread.read(cx).id().clone();
3001 workspace.update(cx, |workspace, cx| {
3002 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
3003 panel.update(cx, |panel, cx| {
3004 panel
3005 .open_thread(&thread_id, window, cx)
3006 .detach_and_log_err(cx)
3007 });
3008 }
3009 })
3010 }
3011 }
3012}