1use crate::context::{AssistantContext, ContextId};
2use crate::context_picker::MentionLink;
3use crate::thread::{
4 LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
5 ThreadEvent, ThreadFeedback,
6};
7use crate::thread_store::ThreadStore;
8use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
9use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
10use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
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 open_as_markdown = IconButton::new("open-as-markdown", IconName::FileCode)
1384 .shape(ui::IconButtonShape::Square)
1385 .icon_size(IconSize::XSmall)
1386 .icon_color(Color::Ignored)
1387 .tooltip(Tooltip::text("Open Thread as Markdown"))
1388 .on_click(|_event, window, cx| {
1389 window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
1390 });
1391
1392 let feedback_container = h_flex().py_2().px_4().gap_1().justify_between();
1393 let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
1394 Some(feedback) => feedback_container
1395 .child(
1396 Label::new(match feedback {
1397 ThreadFeedback::Positive => "Thanks for your feedback!",
1398 ThreadFeedback::Negative => {
1399 "We appreciate your feedback and will use it to improve."
1400 }
1401 })
1402 .color(Color::Muted)
1403 .size(LabelSize::XSmall),
1404 )
1405 .child(
1406 h_flex()
1407 .pr_1()
1408 .gap_1()
1409 .child(
1410 IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
1411 .shape(ui::IconButtonShape::Square)
1412 .icon_size(IconSize::XSmall)
1413 .icon_color(match feedback {
1414 ThreadFeedback::Positive => Color::Accent,
1415 ThreadFeedback::Negative => Color::Ignored,
1416 })
1417 .tooltip(Tooltip::text("Helpful Response"))
1418 .on_click(cx.listener(move |this, _, window, cx| {
1419 this.handle_feedback_click(
1420 message_id,
1421 ThreadFeedback::Positive,
1422 window,
1423 cx,
1424 );
1425 })),
1426 )
1427 .child(
1428 IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
1429 .shape(ui::IconButtonShape::Square)
1430 .icon_size(IconSize::XSmall)
1431 .icon_color(match feedback {
1432 ThreadFeedback::Positive => Color::Ignored,
1433 ThreadFeedback::Negative => Color::Accent,
1434 })
1435 .tooltip(Tooltip::text("Not Helpful"))
1436 .on_click(cx.listener(move |this, _, window, cx| {
1437 this.handle_feedback_click(
1438 message_id,
1439 ThreadFeedback::Negative,
1440 window,
1441 cx,
1442 );
1443 })),
1444 )
1445 .child(open_as_markdown),
1446 )
1447 .into_any_element(),
1448 None => feedback_container
1449 .child(
1450 Label::new(
1451 "Rating the thread sends all of your current conversation to the Zed team.",
1452 )
1453 .color(Color::Muted)
1454 .size(LabelSize::XSmall),
1455 )
1456 .child(
1457 h_flex()
1458 .pr_1()
1459 .gap_1()
1460 .child(
1461 IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
1462 .icon_size(IconSize::XSmall)
1463 .icon_color(Color::Ignored)
1464 .shape(ui::IconButtonShape::Square)
1465 .tooltip(Tooltip::text("Helpful Response"))
1466 .on_click(cx.listener(move |this, _, window, cx| {
1467 this.handle_feedback_click(
1468 message_id,
1469 ThreadFeedback::Positive,
1470 window,
1471 cx,
1472 );
1473 })),
1474 )
1475 .child(
1476 IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
1477 .icon_size(IconSize::XSmall)
1478 .icon_color(Color::Ignored)
1479 .shape(ui::IconButtonShape::Square)
1480 .tooltip(Tooltip::text("Not Helpful"))
1481 .on_click(cx.listener(move |this, _, window, cx| {
1482 this.handle_feedback_click(
1483 message_id,
1484 ThreadFeedback::Negative,
1485 window,
1486 cx,
1487 );
1488 })),
1489 )
1490 .child(open_as_markdown),
1491 )
1492 .into_any_element(),
1493 };
1494
1495 let message_is_empty = message.should_display_content();
1496 let has_content = !message_is_empty || !context.is_empty();
1497
1498 let message_content =
1499 has_content.then(|| {
1500 v_flex()
1501 .gap_1p5()
1502 .when(!message_is_empty, |parent| {
1503 parent.child(
1504 if let Some(edit_message_editor) = edit_message_editor.clone() {
1505 div()
1506 .key_context("EditMessageEditor")
1507 .on_action(cx.listener(Self::cancel_editing_message))
1508 .on_action(cx.listener(Self::confirm_editing_message))
1509 .min_h_6()
1510 .child(edit_message_editor)
1511 .into_any()
1512 } else {
1513 div()
1514 .min_h_6()
1515 .text_ui(cx)
1516 .child(self.render_message_content(
1517 message_id,
1518 rendered_message,
1519 has_tool_uses,
1520 workspace.clone(),
1521 window,
1522 cx,
1523 ))
1524 .into_any()
1525 },
1526 )
1527 })
1528 .when(!context.is_empty(), |parent| {
1529 parent.child(h_flex().flex_wrap().gap_1().children(
1530 context.into_iter().map(|context| {
1531 let context_id = context.id();
1532 ContextPill::added(
1533 AddedContext::new(context, cx),
1534 false,
1535 false,
1536 None,
1537 )
1538 .on_click(Rc::new(cx.listener({
1539 let workspace = workspace.clone();
1540 let context_store = context_store.clone();
1541 move |_, _, window, cx| {
1542 if let Some(workspace) = workspace.upgrade() {
1543 open_context(
1544 context_id,
1545 context_store.clone(),
1546 workspace,
1547 window,
1548 cx,
1549 );
1550 cx.notify();
1551 }
1552 }
1553 })))
1554 }),
1555 ))
1556 })
1557 });
1558
1559 let styled_message = match message.role {
1560 Role::User => v_flex()
1561 .id(("message-container", ix))
1562 .map(|this| {
1563 if is_first_message {
1564 this.pt_2()
1565 } else {
1566 this.pt_4()
1567 }
1568 })
1569 .pb_4()
1570 .pl_2()
1571 .pr_2p5()
1572 .child(
1573 v_flex()
1574 .bg(colors.editor_background)
1575 .rounded_lg()
1576 .border_1()
1577 .border_color(colors.border)
1578 .shadow_md()
1579 .child(
1580 h_flex()
1581 .py_1()
1582 .pl_2()
1583 .pr_1()
1584 .bg(bg_user_message_header)
1585 .border_b_1()
1586 .border_color(colors.border)
1587 .justify_between()
1588 .rounded_t_md()
1589 .child(
1590 h_flex()
1591 .gap_1p5()
1592 .child(
1593 Icon::new(IconName::PersonCircle)
1594 .size(IconSize::XSmall)
1595 .color(Color::Muted),
1596 )
1597 .child(
1598 Label::new("You")
1599 .size(LabelSize::Small)
1600 .color(Color::Muted),
1601 ),
1602 )
1603 .child(
1604 h_flex()
1605 .gap_1()
1606 .when_some(
1607 edit_message_editor.clone(),
1608 |this, edit_message_editor| {
1609 let focus_handle =
1610 edit_message_editor.focus_handle(cx);
1611 this.child(
1612 Button::new("cancel-edit-message", "Cancel")
1613 .label_size(LabelSize::Small)
1614 .key_binding(
1615 KeyBinding::for_action_in(
1616 &menu::Cancel,
1617 &focus_handle,
1618 window,
1619 cx,
1620 )
1621 .map(|kb| kb.size(rems_from_px(12.))),
1622 )
1623 .on_click(
1624 cx.listener(Self::handle_cancel_click),
1625 ),
1626 )
1627 .child(
1628 Button::new(
1629 "confirm-edit-message",
1630 "Regenerate",
1631 )
1632 .label_size(LabelSize::Small)
1633 .key_binding(
1634 KeyBinding::for_action_in(
1635 &menu::Confirm,
1636 &focus_handle,
1637 window,
1638 cx,
1639 )
1640 .map(|kb| kb.size(rems_from_px(12.))),
1641 )
1642 .on_click(
1643 cx.listener(Self::handle_regenerate_click),
1644 ),
1645 )
1646 },
1647 )
1648 .when(
1649 edit_message_editor.is_none() && allow_editing_message,
1650 |this| {
1651 this.child(
1652 Button::new("edit-message", "Edit")
1653 .label_size(LabelSize::Small)
1654 .on_click(cx.listener({
1655 let message_segments =
1656 message.segments.clone();
1657 move |this, _, window, cx| {
1658 this.start_editing_message(
1659 message_id,
1660 &message_segments,
1661 window,
1662 cx,
1663 );
1664 }
1665 })),
1666 )
1667 },
1668 ),
1669 ),
1670 )
1671 .child(div().p_2().children(message_content)),
1672 ),
1673 Role::Assistant => v_flex()
1674 .id(("message-container", ix))
1675 .ml_2()
1676 .pl_2()
1677 .pr_4()
1678 .border_l_1()
1679 .border_color(cx.theme().colors().border_variant)
1680 .children(message_content)
1681 .when(has_tool_uses, |parent| {
1682 parent.children(
1683 tool_uses
1684 .into_iter()
1685 .map(|tool_use| self.render_tool_use(tool_use, window, cx)),
1686 )
1687 }),
1688 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1689 v_flex()
1690 .bg(colors.editor_background)
1691 .rounded_sm()
1692 .child(div().p_4().children(message_content)),
1693 ),
1694 };
1695
1696 v_flex()
1697 .w_full()
1698 .when_some(checkpoint, |parent, checkpoint| {
1699 let mut is_pending = false;
1700 let mut error = None;
1701 if let Some(last_restore_checkpoint) =
1702 self.thread.read(cx).last_restore_checkpoint()
1703 {
1704 if last_restore_checkpoint.message_id() == message_id {
1705 match last_restore_checkpoint {
1706 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1707 LastRestoreCheckpoint::Error { error: err, .. } => {
1708 error = Some(err.clone());
1709 }
1710 }
1711 }
1712 }
1713
1714 let restore_checkpoint_button =
1715 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1716 .icon(if error.is_some() {
1717 IconName::XCircle
1718 } else {
1719 IconName::Undo
1720 })
1721 .icon_size(IconSize::XSmall)
1722 .icon_position(IconPosition::Start)
1723 .icon_color(if error.is_some() {
1724 Some(Color::Error)
1725 } else {
1726 None
1727 })
1728 .label_size(LabelSize::XSmall)
1729 .disabled(is_pending)
1730 .on_click(cx.listener(move |this, _, _window, cx| {
1731 this.thread.update(cx, |thread, cx| {
1732 thread
1733 .restore_checkpoint(checkpoint.clone(), cx)
1734 .detach_and_log_err(cx);
1735 });
1736 }));
1737
1738 let restore_checkpoint_button = if is_pending {
1739 restore_checkpoint_button
1740 .with_animation(
1741 ("pulsating-restore-checkpoint-button", ix),
1742 Animation::new(Duration::from_secs(2))
1743 .repeat()
1744 .with_easing(pulsating_between(0.6, 1.)),
1745 |label, delta| label.alpha(delta),
1746 )
1747 .into_any_element()
1748 } else if let Some(error) = error {
1749 restore_checkpoint_button
1750 .tooltip(Tooltip::text(error.to_string()))
1751 .into_any_element()
1752 } else {
1753 restore_checkpoint_button.into_any_element()
1754 };
1755
1756 parent.child(
1757 h_flex()
1758 .pt_2p5()
1759 .px_2p5()
1760 .w_full()
1761 .gap_1()
1762 .child(ui::Divider::horizontal())
1763 .child(restore_checkpoint_button)
1764 .child(ui::Divider::horizontal()),
1765 )
1766 })
1767 .when(is_first_message, |parent| {
1768 parent.child(self.render_rules_item(cx))
1769 })
1770 .child(styled_message)
1771 .when(!needs_confirmation && generating_label.is_some(), |this| {
1772 this.child(
1773 h_flex()
1774 .h_8()
1775 .mt_2()
1776 .mb_4()
1777 .ml_4()
1778 .py_1p5()
1779 .child(generating_label.unwrap()),
1780 )
1781 })
1782 .when(show_feedback, move |parent| {
1783 parent.child(feedback_items).when_some(
1784 self.open_feedback_editors.get(&message_id),
1785 move |parent, feedback_editor| {
1786 let focus_handle = feedback_editor.focus_handle(cx);
1787 parent.child(
1788 v_flex()
1789 .key_context("AgentFeedbackMessageEditor")
1790 .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
1791 this.open_feedback_editors.remove(&message_id);
1792 cx.notify();
1793 }))
1794 .on_action(cx.listener(move |this, _: &menu::Confirm, _, cx| {
1795 this.submit_feedback_message(message_id, cx);
1796 cx.notify();
1797 }))
1798 .on_action(cx.listener(Self::confirm_editing_message))
1799 .mb_2()
1800 .mx_4()
1801 .p_2()
1802 .rounded_md()
1803 .border_1()
1804 .border_color(cx.theme().colors().border)
1805 .bg(cx.theme().colors().editor_background)
1806 .child(feedback_editor.clone())
1807 .child(
1808 h_flex()
1809 .gap_1()
1810 .justify_end()
1811 .child(
1812 Button::new("dismiss-feedback-message", "Cancel")
1813 .label_size(LabelSize::Small)
1814 .key_binding(
1815 KeyBinding::for_action_in(
1816 &menu::Cancel,
1817 &focus_handle,
1818 window,
1819 cx,
1820 )
1821 .map(|kb| kb.size(rems_from_px(10.))),
1822 )
1823 .on_click(cx.listener(
1824 move |this, _, _window, cx| {
1825 this.open_feedback_editors
1826 .remove(&message_id);
1827 cx.notify();
1828 },
1829 )),
1830 )
1831 .child(
1832 Button::new(
1833 "submit-feedback-message",
1834 "Share Feedback",
1835 )
1836 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
1837 .label_size(LabelSize::Small)
1838 .key_binding(
1839 KeyBinding::for_action_in(
1840 &menu::Confirm,
1841 &focus_handle,
1842 window,
1843 cx,
1844 )
1845 .map(|kb| kb.size(rems_from_px(10.))),
1846 )
1847 .on_click(
1848 cx.listener(move |this, _, _window, cx| {
1849 this.submit_feedback_message(message_id, cx);
1850 cx.notify()
1851 }),
1852 ),
1853 ),
1854 ),
1855 )
1856 },
1857 )
1858 })
1859 .into_any()
1860 }
1861
1862 fn render_message_content(
1863 &self,
1864 message_id: MessageId,
1865 rendered_message: &RenderedMessage,
1866 has_tool_uses: bool,
1867 workspace: WeakEntity<Workspace>,
1868 window: &Window,
1869 cx: &Context<Self>,
1870 ) -> impl IntoElement {
1871 let is_last_message = self.messages.last() == Some(&message_id);
1872 let is_generating = self.thread.read(cx).is_generating();
1873 let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses {
1874 rendered_message
1875 .segments
1876 .iter()
1877 .enumerate()
1878 .next_back()
1879 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1880 .map(|(index, _)| index)
1881 } else {
1882 None
1883 };
1884
1885 v_flex()
1886 .text_ui(cx)
1887 .gap_2()
1888 .children(
1889 rendered_message.segments.iter().enumerate().map(
1890 |(index, segment)| match segment {
1891 RenderedMessageSegment::Thinking {
1892 content,
1893 scroll_handle,
1894 } => self
1895 .render_message_thinking_segment(
1896 message_id,
1897 index,
1898 content.clone(),
1899 &scroll_handle,
1900 Some(index) == pending_thinking_segment_index,
1901 window,
1902 cx,
1903 )
1904 .into_any_element(),
1905 RenderedMessageSegment::Text(markdown) => div()
1906 .child(
1907 MarkdownElement::new(
1908 markdown.clone(),
1909 default_markdown_style(window, cx),
1910 )
1911 .code_block_renderer(markdown::CodeBlockRenderer::Custom {
1912 render: Arc::new({
1913 let workspace = workspace.clone();
1914 let active_thread = cx.entity();
1915 move |kind, parsed_markdown, range, window, cx| {
1916 render_markdown_code_block(
1917 message_id,
1918 range.start,
1919 kind,
1920 parsed_markdown,
1921 range,
1922 active_thread.clone(),
1923 workspace.clone(),
1924 window,
1925 cx,
1926 )
1927 }
1928 }),
1929 transform: Some(Arc::new({
1930 let active_thread = cx.entity();
1931 move |el, range, _, cx| {
1932 let is_expanded = active_thread
1933 .read(cx)
1934 .expanded_code_blocks
1935 .get(&(message_id, range.start))
1936 .copied()
1937 .unwrap_or(false);
1938
1939 if is_expanded {
1940 return el;
1941 }
1942 el.child(
1943 div()
1944 .absolute()
1945 .bottom_0()
1946 .left_0()
1947 .w_full()
1948 .h_1_4()
1949 .rounded_b_lg()
1950 .bg(gpui::linear_gradient(
1951 0.,
1952 gpui::linear_color_stop(
1953 cx.theme().colors().editor_background,
1954 0.,
1955 ),
1956 gpui::linear_color_stop(
1957 cx.theme()
1958 .colors()
1959 .editor_background
1960 .opacity(0.),
1961 1.,
1962 ),
1963 )),
1964 )
1965 }
1966 })),
1967 })
1968 .on_url_click({
1969 let workspace = self.workspace.clone();
1970 move |text, window, cx| {
1971 open_markdown_link(text, workspace.clone(), window, cx);
1972 }
1973 }),
1974 )
1975 .into_any_element(),
1976 },
1977 ),
1978 )
1979 }
1980
1981 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1982 cx.theme().colors().border.opacity(0.5)
1983 }
1984
1985 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1986 cx.theme()
1987 .colors()
1988 .element_background
1989 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1990 }
1991
1992 fn render_message_thinking_segment(
1993 &self,
1994 message_id: MessageId,
1995 ix: usize,
1996 markdown: Entity<Markdown>,
1997 scroll_handle: &ScrollHandle,
1998 pending: bool,
1999 window: &Window,
2000 cx: &Context<Self>,
2001 ) -> impl IntoElement {
2002 let is_open = self
2003 .expanded_thinking_segments
2004 .get(&(message_id, ix))
2005 .copied()
2006 .unwrap_or_default();
2007
2008 let editor_bg = cx.theme().colors().panel_background;
2009
2010 div().map(|this| {
2011 if pending {
2012 this.v_flex()
2013 .mt_neg_2()
2014 .mb_1p5()
2015 .child(
2016 h_flex()
2017 .group("disclosure-header")
2018 .justify_between()
2019 .child(
2020 h_flex()
2021 .gap_1p5()
2022 .child(
2023 Icon::new(IconName::LightBulb)
2024 .size(IconSize::XSmall)
2025 .color(Color::Muted),
2026 )
2027 .child({
2028 Label::new("Thinking")
2029 .color(Color::Muted)
2030 .size(LabelSize::Small)
2031 .with_animation(
2032 "generating-label",
2033 Animation::new(Duration::from_secs(1)).repeat(),
2034 |mut label, delta| {
2035 let text = match delta {
2036 d if d < 0.25 => "Thinking",
2037 d if d < 0.5 => "Thinking.",
2038 d if d < 0.75 => "Thinking..",
2039 _ => "Thinking...",
2040 };
2041 label.set_text(text);
2042 label
2043 },
2044 )
2045 .with_animation(
2046 "pulsating-label",
2047 Animation::new(Duration::from_secs(2))
2048 .repeat()
2049 .with_easing(pulsating_between(0.6, 1.)),
2050 |label, delta| {
2051 label.map_element(|label| label.alpha(delta))
2052 },
2053 )
2054 }),
2055 )
2056 .child(
2057 h_flex()
2058 .gap_1()
2059 .child(
2060 div().visible_on_hover("disclosure-header").child(
2061 Disclosure::new("thinking-disclosure", is_open)
2062 .opened_icon(IconName::ChevronUp)
2063 .closed_icon(IconName::ChevronDown)
2064 .on_click(cx.listener({
2065 move |this, _event, _window, _cx| {
2066 let is_open = this
2067 .expanded_thinking_segments
2068 .entry((message_id, ix))
2069 .or_insert(false);
2070
2071 *is_open = !*is_open;
2072 }
2073 })),
2074 ),
2075 )
2076 .child({
2077 Icon::new(IconName::ArrowCircle)
2078 .color(Color::Accent)
2079 .size(IconSize::Small)
2080 .with_animation(
2081 "arrow-circle",
2082 Animation::new(Duration::from_secs(2)).repeat(),
2083 |icon, delta| {
2084 icon.transform(Transformation::rotate(
2085 percentage(delta),
2086 ))
2087 },
2088 )
2089 }),
2090 ),
2091 )
2092 .when(!is_open, |this| {
2093 let gradient_overlay = div()
2094 .rounded_b_lg()
2095 .h_full()
2096 .absolute()
2097 .w_full()
2098 .bottom_0()
2099 .left_0()
2100 .bg(linear_gradient(
2101 180.,
2102 linear_color_stop(editor_bg, 1.),
2103 linear_color_stop(editor_bg.opacity(0.2), 0.),
2104 ));
2105
2106 this.child(
2107 div()
2108 .relative()
2109 .bg(editor_bg)
2110 .rounded_b_lg()
2111 .mt_2()
2112 .pl_4()
2113 .child(
2114 div()
2115 .id(("thinking-content", ix))
2116 .max_h_20()
2117 .track_scroll(scroll_handle)
2118 .text_ui_sm(cx)
2119 .overflow_hidden()
2120 .child(
2121 MarkdownElement::new(
2122 markdown.clone(),
2123 default_markdown_style(window, cx),
2124 )
2125 .on_url_click({
2126 let workspace = self.workspace.clone();
2127 move |text, window, cx| {
2128 open_markdown_link(
2129 text,
2130 workspace.clone(),
2131 window,
2132 cx,
2133 );
2134 }
2135 }),
2136 ),
2137 )
2138 .child(gradient_overlay),
2139 )
2140 })
2141 .when(is_open, |this| {
2142 this.child(
2143 div()
2144 .id(("thinking-content", ix))
2145 .h_full()
2146 .bg(editor_bg)
2147 .text_ui_sm(cx)
2148 .child(
2149 MarkdownElement::new(
2150 markdown.clone(),
2151 default_markdown_style(window, cx),
2152 )
2153 .on_url_click({
2154 let workspace = self.workspace.clone();
2155 move |text, window, cx| {
2156 open_markdown_link(text, workspace.clone(), window, cx);
2157 }
2158 }),
2159 ),
2160 )
2161 })
2162 } else {
2163 this.v_flex()
2164 .mt_neg_2()
2165 .child(
2166 h_flex()
2167 .group("disclosure-header")
2168 .pr_1()
2169 .justify_between()
2170 .opacity(0.8)
2171 .hover(|style| style.opacity(1.))
2172 .child(
2173 h_flex()
2174 .gap_1p5()
2175 .child(
2176 Icon::new(IconName::LightBulb)
2177 .size(IconSize::XSmall)
2178 .color(Color::Muted),
2179 )
2180 .child(Label::new("Thought Process").size(LabelSize::Small)),
2181 )
2182 .child(
2183 div().visible_on_hover("disclosure-header").child(
2184 Disclosure::new("thinking-disclosure", is_open)
2185 .opened_icon(IconName::ChevronUp)
2186 .closed_icon(IconName::ChevronDown)
2187 .on_click(cx.listener({
2188 move |this, _event, _window, _cx| {
2189 let is_open = this
2190 .expanded_thinking_segments
2191 .entry((message_id, ix))
2192 .or_insert(false);
2193
2194 *is_open = !*is_open;
2195 }
2196 })),
2197 ),
2198 ),
2199 )
2200 .child(
2201 div()
2202 .id(("thinking-content", ix))
2203 .relative()
2204 .mt_1p5()
2205 .ml_1p5()
2206 .pl_2p5()
2207 .border_l_1()
2208 .border_color(cx.theme().colors().border_variant)
2209 .text_ui_sm(cx)
2210 .when(is_open, |this| {
2211 this.child(
2212 MarkdownElement::new(
2213 markdown.clone(),
2214 default_markdown_style(window, cx),
2215 )
2216 .on_url_click({
2217 let workspace = self.workspace.clone();
2218 move |text, window, cx| {
2219 open_markdown_link(text, workspace.clone(), window, cx);
2220 }
2221 }),
2222 )
2223 }),
2224 )
2225 }
2226 })
2227 }
2228
2229 fn render_tool_use(
2230 &self,
2231 tool_use: ToolUse,
2232 window: &mut Window,
2233 cx: &mut Context<Self>,
2234 ) -> impl IntoElement + use<> {
2235 let is_open = self
2236 .expanded_tool_uses
2237 .get(&tool_use.id)
2238 .copied()
2239 .unwrap_or_default();
2240
2241 let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
2242
2243 let fs = self
2244 .workspace
2245 .upgrade()
2246 .map(|workspace| workspace.read(cx).app_state().fs.clone());
2247 let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
2248 let edit_tools = tool_use.needs_confirmation;
2249
2250 let status_icons = div().child(match &tool_use.status {
2251 ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
2252 let icon = Icon::new(IconName::Warning)
2253 .color(Color::Warning)
2254 .size(IconSize::Small);
2255 icon.into_any_element()
2256 }
2257 ToolUseStatus::Running => {
2258 let icon = Icon::new(IconName::ArrowCircle)
2259 .color(Color::Accent)
2260 .size(IconSize::Small);
2261 icon.with_animation(
2262 "arrow-circle",
2263 Animation::new(Duration::from_secs(2)).repeat(),
2264 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
2265 )
2266 .into_any_element()
2267 }
2268 ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
2269 ToolUseStatus::Error(_) => {
2270 let icon = Icon::new(IconName::Close)
2271 .color(Color::Error)
2272 .size(IconSize::Small);
2273 icon.into_any_element()
2274 }
2275 });
2276
2277 let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
2278 let results_content_container = || v_flex().p_2().gap_0p5();
2279
2280 let results_content = v_flex()
2281 .gap_1()
2282 .child(
2283 results_content_container()
2284 .child(
2285 Label::new("Input")
2286 .size(LabelSize::XSmall)
2287 .color(Color::Muted)
2288 .buffer_font(cx),
2289 )
2290 .child(
2291 div()
2292 .w_full()
2293 .text_ui_sm(cx)
2294 .children(rendered_tool_use.as_ref().map(|rendered| {
2295 MarkdownElement::new(
2296 rendered.input.clone(),
2297 tool_use_markdown_style(window, cx),
2298 )
2299 .on_url_click({
2300 let workspace = self.workspace.clone();
2301 move |text, window, cx| {
2302 open_markdown_link(text, workspace.clone(), window, cx);
2303 }
2304 })
2305 })),
2306 ),
2307 )
2308 .map(|container| match tool_use.status {
2309 ToolUseStatus::Finished(_) => container.child(
2310 results_content_container()
2311 .border_t_1()
2312 .border_color(self.tool_card_border_color(cx))
2313 .child(
2314 Label::new("Result")
2315 .size(LabelSize::XSmall)
2316 .color(Color::Muted)
2317 .buffer_font(cx),
2318 )
2319 .child(div().w_full().text_ui_sm(cx).children(
2320 rendered_tool_use.as_ref().map(|rendered| {
2321 MarkdownElement::new(
2322 rendered.output.clone(),
2323 tool_use_markdown_style(window, cx),
2324 )
2325 .on_url_click({
2326 let workspace = self.workspace.clone();
2327 move |text, window, cx| {
2328 open_markdown_link(text, workspace.clone(), window, cx);
2329 }
2330 })
2331 }),
2332 )),
2333 ),
2334 ToolUseStatus::Running => container.child(
2335 results_content_container().child(
2336 h_flex()
2337 .gap_1()
2338 .pb_1()
2339 .border_t_1()
2340 .border_color(self.tool_card_border_color(cx))
2341 .child(
2342 Icon::new(IconName::ArrowCircle)
2343 .size(IconSize::Small)
2344 .color(Color::Accent)
2345 .with_animation(
2346 "arrow-circle",
2347 Animation::new(Duration::from_secs(2)).repeat(),
2348 |icon, delta| {
2349 icon.transform(Transformation::rotate(percentage(
2350 delta,
2351 )))
2352 },
2353 ),
2354 )
2355 .child(
2356 Label::new("Running…")
2357 .size(LabelSize::XSmall)
2358 .color(Color::Muted)
2359 .buffer_font(cx),
2360 ),
2361 ),
2362 ),
2363 ToolUseStatus::Error(_) => container.child(
2364 results_content_container()
2365 .border_t_1()
2366 .border_color(self.tool_card_border_color(cx))
2367 .child(
2368 Label::new("Error")
2369 .size(LabelSize::XSmall)
2370 .color(Color::Muted)
2371 .buffer_font(cx),
2372 )
2373 .child(
2374 div()
2375 .text_ui_sm(cx)
2376 .children(rendered_tool_use.as_ref().map(|rendered| {
2377 MarkdownElement::new(
2378 rendered.output.clone(),
2379 tool_use_markdown_style(window, cx),
2380 )
2381 .on_url_click({
2382 let workspace = self.workspace.clone();
2383 move |text, window, cx| {
2384 open_markdown_link(text, workspace.clone(), window, cx);
2385 }
2386 })
2387 })),
2388 ),
2389 ),
2390 ToolUseStatus::Pending => container,
2391 ToolUseStatus::NeedsConfirmation => container.child(
2392 results_content_container()
2393 .border_t_1()
2394 .border_color(self.tool_card_border_color(cx))
2395 .child(
2396 Label::new("Asking Permission")
2397 .size(LabelSize::Small)
2398 .color(Color::Muted)
2399 .buffer_font(cx),
2400 ),
2401 ),
2402 });
2403
2404 let gradient_overlay = |color: Hsla| {
2405 div()
2406 .h_full()
2407 .absolute()
2408 .w_12()
2409 .bottom_0()
2410 .map(|element| {
2411 if is_status_finished {
2412 element.right_6()
2413 } else {
2414 element.right(px(44.))
2415 }
2416 })
2417 .bg(linear_gradient(
2418 90.,
2419 linear_color_stop(color, 1.),
2420 linear_color_stop(color.opacity(0.2), 0.),
2421 ))
2422 };
2423
2424 div().map(|element| {
2425 if !edit_tools {
2426 element.child(
2427 v_flex()
2428 .my_2()
2429 .child(
2430 h_flex()
2431 .group("disclosure-header")
2432 .relative()
2433 .gap_1p5()
2434 .justify_between()
2435 .opacity(0.8)
2436 .hover(|style| style.opacity(1.))
2437 .when(!is_status_finished, |this| this.pr_2())
2438 .child(
2439 h_flex()
2440 .id("tool-label-container")
2441 .gap_1p5()
2442 .max_w_full()
2443 .overflow_x_scroll()
2444 .child(
2445 Icon::new(tool_use.icon)
2446 .size(IconSize::XSmall)
2447 .color(Color::Muted),
2448 )
2449 .child(
2450 h_flex().pr_8().text_ui_sm(cx).children(
2451 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| {
2452 open_markdown_link(text, workspace.clone(), window, cx);
2453 }}))
2454 ),
2455 ),
2456 )
2457 .child(
2458 h_flex()
2459 .gap_1()
2460 .child(
2461 div().visible_on_hover("disclosure-header").child(
2462 Disclosure::new("tool-use-disclosure", is_open)
2463 .opened_icon(IconName::ChevronUp)
2464 .closed_icon(IconName::ChevronDown)
2465 .on_click(cx.listener({
2466 let tool_use_id = tool_use.id.clone();
2467 move |this, _event, _window, _cx| {
2468 let is_open = this
2469 .expanded_tool_uses
2470 .entry(tool_use_id.clone())
2471 .or_insert(false);
2472
2473 *is_open = !*is_open;
2474 }
2475 })),
2476 ),
2477 )
2478 .child(status_icons),
2479 )
2480 .child(gradient_overlay(cx.theme().colors().panel_background)),
2481 )
2482 .map(|parent| {
2483 if !is_open {
2484 return parent;
2485 }
2486
2487 parent.child(
2488 v_flex()
2489 .mt_1()
2490 .border_1()
2491 .border_color(self.tool_card_border_color(cx))
2492 .bg(cx.theme().colors().editor_background)
2493 .rounded_lg()
2494 .child(results_content),
2495 )
2496 }),
2497 )
2498 } else {
2499 v_flex()
2500 .my_3()
2501 .rounded_lg()
2502 .border_1()
2503 .border_color(self.tool_card_border_color(cx))
2504 .overflow_hidden()
2505 .child(
2506 h_flex()
2507 .group("disclosure-header")
2508 .relative()
2509 .justify_between()
2510 .py_1()
2511 .map(|element| {
2512 if is_status_finished {
2513 element.pl_2().pr_0p5()
2514 } else {
2515 element.px_2()
2516 }
2517 })
2518 .bg(self.tool_card_header_bg(cx))
2519 .map(|element| {
2520 if is_open {
2521 element.border_b_1().rounded_t_md()
2522 } else if needs_confirmation {
2523 element.rounded_t_md()
2524 } else {
2525 element.rounded_md()
2526 }
2527 })
2528 .border_color(self.tool_card_border_color(cx))
2529 .child(
2530 h_flex()
2531 .id("tool-label-container")
2532 .gap_1p5()
2533 .max_w_full()
2534 .overflow_x_scroll()
2535 .child(
2536 Icon::new(tool_use.icon)
2537 .size(IconSize::XSmall)
2538 .color(Color::Muted),
2539 )
2540 .child(
2541 h_flex().pr_8().text_ui_sm(cx).children(
2542 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| {
2543 open_markdown_link(text, workspace.clone(), window, cx);
2544 }}))
2545 ),
2546 ),
2547 )
2548 .child(
2549 h_flex()
2550 .gap_1()
2551 .child(
2552 div().visible_on_hover("disclosure-header").child(
2553 Disclosure::new("tool-use-disclosure", is_open)
2554 .opened_icon(IconName::ChevronUp)
2555 .closed_icon(IconName::ChevronDown)
2556 .on_click(cx.listener({
2557 let tool_use_id = tool_use.id.clone();
2558 move |this, _event, _window, _cx| {
2559 let is_open = this
2560 .expanded_tool_uses
2561 .entry(tool_use_id.clone())
2562 .or_insert(false);
2563
2564 *is_open = !*is_open;
2565 }
2566 })),
2567 ),
2568 )
2569 .child(status_icons),
2570 )
2571 .child(gradient_overlay(self.tool_card_header_bg(cx))),
2572 )
2573 .map(|parent| {
2574 if !is_open {
2575 return parent;
2576 }
2577
2578 parent.child(
2579 v_flex()
2580 .bg(cx.theme().colors().editor_background)
2581 .map(|element| {
2582 if needs_confirmation {
2583 element.rounded_none()
2584 } else {
2585 element.rounded_b_lg()
2586 }
2587 })
2588 .child(results_content),
2589 )
2590 })
2591 .when(needs_confirmation, |this| {
2592 this.child(
2593 h_flex()
2594 .py_1()
2595 .pl_2()
2596 .pr_1()
2597 .gap_1()
2598 .justify_between()
2599 .bg(cx.theme().colors().editor_background)
2600 .border_t_1()
2601 .border_color(self.tool_card_border_color(cx))
2602 .rounded_b_lg()
2603 .child(
2604 Label::new("Waiting for Confirmation…")
2605 .color(Color::Muted)
2606 .size(LabelSize::Small)
2607 .with_animation(
2608 "generating-label",
2609 Animation::new(Duration::from_secs(1)).repeat(),
2610 |mut label, delta| {
2611 let text = match delta {
2612 d if d < 0.25 => "Waiting for Confirmation",
2613 d if d < 0.5 => "Waiting for Confirmation.",
2614 d if d < 0.75 => "Waiting for Confirmation..",
2615 _ => "Waiting for Confirmation...",
2616 };
2617 label.set_text(text);
2618 label
2619 },
2620 )
2621 .with_animation(
2622 "pulsating-label",
2623 Animation::new(Duration::from_secs(2))
2624 .repeat()
2625 .with_easing(pulsating_between(0.6, 1.)),
2626 |label, delta| label.map_element(|label| label.alpha(delta)),
2627 ),
2628 )
2629 .child(
2630 h_flex()
2631 .gap_0p5()
2632 .child({
2633 let tool_id = tool_use.id.clone();
2634 Button::new(
2635 "always-allow-tool-action",
2636 "Always Allow",
2637 )
2638 .label_size(LabelSize::Small)
2639 .icon(IconName::CheckDouble)
2640 .icon_position(IconPosition::Start)
2641 .icon_size(IconSize::Small)
2642 .icon_color(Color::Success)
2643 .tooltip(move |window, cx| {
2644 Tooltip::with_meta(
2645 "Never ask for permission",
2646 None,
2647 "Restore the original behavior in your Agent Panel settings",
2648 window,
2649 cx,
2650 )
2651 })
2652 .on_click(cx.listener(
2653 move |this, event, window, cx| {
2654 if let Some(fs) = fs.clone() {
2655 update_settings_file::<AssistantSettings>(
2656 fs.clone(),
2657 cx,
2658 |settings, _| {
2659 settings.set_always_allow_tool_actions(true);
2660 },
2661 );
2662 }
2663 this.handle_allow_tool(
2664 tool_id.clone(),
2665 event,
2666 window,
2667 cx,
2668 )
2669 },
2670 ))
2671 })
2672 .child(ui::Divider::vertical())
2673 .child({
2674 let tool_id = tool_use.id.clone();
2675 Button::new("allow-tool-action", "Allow")
2676 .label_size(LabelSize::Small)
2677 .icon(IconName::Check)
2678 .icon_position(IconPosition::Start)
2679 .icon_size(IconSize::Small)
2680 .icon_color(Color::Success)
2681 .on_click(cx.listener(
2682 move |this, event, window, cx| {
2683 this.handle_allow_tool(
2684 tool_id.clone(),
2685 event,
2686 window,
2687 cx,
2688 )
2689 },
2690 ))
2691 })
2692 .child({
2693 let tool_id = tool_use.id.clone();
2694 let tool_name: Arc<str> = tool_use.name.into();
2695 Button::new("deny-tool", "Deny")
2696 .label_size(LabelSize::Small)
2697 .icon(IconName::Close)
2698 .icon_position(IconPosition::Start)
2699 .icon_size(IconSize::Small)
2700 .icon_color(Color::Error)
2701 .on_click(cx.listener(
2702 move |this, event, window, cx| {
2703 this.handle_deny_tool(
2704 tool_id.clone(),
2705 tool_name.clone(),
2706 event,
2707 window,
2708 cx,
2709 )
2710 },
2711 ))
2712 }),
2713 ),
2714 )
2715 })
2716 }
2717 })
2718 }
2719
2720 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
2721 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2722 else {
2723 return div().into_any();
2724 };
2725
2726 let rules_files = system_prompt_context
2727 .worktrees
2728 .iter()
2729 .filter_map(|worktree| worktree.rules_file.as_ref())
2730 .collect::<Vec<_>>();
2731
2732 let label_text = match rules_files.as_slice() {
2733 &[] => return div().into_any(),
2734 &[rules_file] => {
2735 format!("Using {:?} file", rules_file.path_in_worktree)
2736 }
2737 rules_files => {
2738 format!("Using {} rules files", rules_files.len())
2739 }
2740 };
2741
2742 div()
2743 .pt_2()
2744 .px_2p5()
2745 .child(
2746 h_flex()
2747 .w_full()
2748 .gap_0p5()
2749 .child(
2750 h_flex()
2751 .gap_1p5()
2752 .child(
2753 Icon::new(IconName::File)
2754 .size(IconSize::XSmall)
2755 .color(Color::Disabled),
2756 )
2757 .child(
2758 Label::new(label_text)
2759 .size(LabelSize::XSmall)
2760 .color(Color::Muted)
2761 .buffer_font(cx),
2762 ),
2763 )
2764 .child(
2765 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
2766 .shape(ui::IconButtonShape::Square)
2767 .icon_size(IconSize::XSmall)
2768 .icon_color(Color::Ignored)
2769 .on_click(cx.listener(Self::handle_open_rules))
2770 .tooltip(Tooltip::text("View Rules")),
2771 ),
2772 )
2773 .into_any()
2774 }
2775
2776 fn handle_allow_tool(
2777 &mut self,
2778 tool_use_id: LanguageModelToolUseId,
2779 _: &ClickEvent,
2780 _window: &mut Window,
2781 cx: &mut Context<Self>,
2782 ) {
2783 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
2784 .thread
2785 .read(cx)
2786 .pending_tool(&tool_use_id)
2787 .map(|tool_use| tool_use.status.clone())
2788 {
2789 self.thread.update(cx, |thread, cx| {
2790 thread.run_tool(
2791 c.tool_use_id.clone(),
2792 c.ui_text.clone(),
2793 c.input.clone(),
2794 &c.messages,
2795 c.tool.clone(),
2796 cx,
2797 );
2798 });
2799 }
2800 }
2801
2802 fn handle_deny_tool(
2803 &mut self,
2804 tool_use_id: LanguageModelToolUseId,
2805 tool_name: Arc<str>,
2806 _: &ClickEvent,
2807 _window: &mut Window,
2808 cx: &mut Context<Self>,
2809 ) {
2810 self.thread.update(cx, |thread, cx| {
2811 thread.deny_tool_use(tool_use_id, tool_name, cx);
2812 });
2813 }
2814
2815 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
2816 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2817 else {
2818 return;
2819 };
2820
2821 let abs_paths = system_prompt_context
2822 .worktrees
2823 .iter()
2824 .flat_map(|worktree| worktree.rules_file.as_ref())
2825 .map(|rules_file| rules_file.abs_path.to_path_buf())
2826 .collect::<Vec<_>>();
2827
2828 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
2829 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
2830 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
2831 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
2832 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
2833 }) {
2834 task.detach();
2835 }
2836 }
2837
2838 fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
2839 for window in self.notifications.drain(..) {
2840 window
2841 .update(cx, |_, window, _| {
2842 window.remove_window();
2843 })
2844 .ok();
2845
2846 self.notification_subscriptions.remove(&window);
2847 }
2848 }
2849
2850 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
2851 if !self.show_scrollbar && !self.scrollbar_state.is_dragging() {
2852 return None;
2853 }
2854
2855 Some(
2856 div()
2857 .occlude()
2858 .id("active-thread-scrollbar")
2859 .on_mouse_move(cx.listener(|_, _, _, cx| {
2860 cx.notify();
2861 cx.stop_propagation()
2862 }))
2863 .on_hover(|_, _, cx| {
2864 cx.stop_propagation();
2865 })
2866 .on_any_mouse_down(|_, _, cx| {
2867 cx.stop_propagation();
2868 })
2869 .on_mouse_up(
2870 MouseButton::Left,
2871 cx.listener(|_, _, _, cx| {
2872 cx.stop_propagation();
2873 }),
2874 )
2875 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2876 cx.notify();
2877 }))
2878 .h_full()
2879 .absolute()
2880 .right_1()
2881 .top_1()
2882 .bottom_0()
2883 .w(px(12.))
2884 .cursor_default()
2885 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
2886 )
2887 }
2888
2889 fn hide_scrollbar_later(&mut self, cx: &mut Context<Self>) {
2890 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
2891 self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| {
2892 cx.background_executor()
2893 .timer(SCROLLBAR_SHOW_INTERVAL)
2894 .await;
2895 thread
2896 .update(cx, |thread, cx| {
2897 if !thread.scrollbar_state.is_dragging() {
2898 thread.show_scrollbar = false;
2899 cx.notify();
2900 }
2901 })
2902 .log_err();
2903 }))
2904 }
2905}
2906
2907impl Render for ActiveThread {
2908 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2909 v_flex()
2910 .size_full()
2911 .relative()
2912 .on_mouse_move(cx.listener(|this, _, _, cx| {
2913 this.show_scrollbar = true;
2914 this.hide_scrollbar_later(cx);
2915 cx.notify();
2916 }))
2917 .on_scroll_wheel(cx.listener(|this, _, _, cx| {
2918 this.show_scrollbar = true;
2919 this.hide_scrollbar_later(cx);
2920 cx.notify();
2921 }))
2922 .on_mouse_up(
2923 MouseButton::Left,
2924 cx.listener(|this, _, _, cx| {
2925 this.hide_scrollbar_later(cx);
2926 }),
2927 )
2928 .child(list(self.list_state.clone()).flex_grow())
2929 .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
2930 this.child(scrollbar)
2931 })
2932 }
2933}
2934
2935pub(crate) fn open_context(
2936 id: ContextId,
2937 context_store: Entity<ContextStore>,
2938 workspace: Entity<Workspace>,
2939 window: &mut Window,
2940 cx: &mut App,
2941) {
2942 let Some(context) = context_store.read(cx).context_for_id(id) else {
2943 return;
2944 };
2945
2946 match context {
2947 AssistantContext::File(file_context) => {
2948 if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
2949 {
2950 workspace.update(cx, |workspace, cx| {
2951 workspace
2952 .open_path(project_path, None, true, window, cx)
2953 .detach_and_log_err(cx);
2954 });
2955 }
2956 }
2957 AssistantContext::Directory(directory_context) => {
2958 let project_path = directory_context.project_path(cx);
2959 workspace.update(cx, |workspace, cx| {
2960 workspace.project().update(cx, |project, cx| {
2961 if let Some(entry) = project.entry_for_path(&project_path, cx) {
2962 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2963 }
2964 })
2965 })
2966 }
2967 AssistantContext::Symbol(symbol_context) => {
2968 if let Some(project_path) = symbol_context
2969 .context_symbol
2970 .buffer
2971 .read(cx)
2972 .project_path(cx)
2973 {
2974 let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
2975 let target_position = symbol_context
2976 .context_symbol
2977 .id
2978 .range
2979 .start
2980 .to_point(&snapshot);
2981
2982 let open_task = workspace.update(cx, |workspace, cx| {
2983 workspace.open_path(project_path, None, true, window, cx)
2984 });
2985 window
2986 .spawn(cx, async move |cx| {
2987 if let Some(active_editor) = open_task
2988 .await
2989 .log_err()
2990 .and_then(|item| item.downcast::<Editor>())
2991 {
2992 active_editor
2993 .downgrade()
2994 .update_in(cx, |editor, window, cx| {
2995 editor.go_to_singleton_buffer_point(
2996 target_position,
2997 window,
2998 cx,
2999 );
3000 })
3001 .log_err();
3002 }
3003 })
3004 .detach();
3005 }
3006 }
3007 AssistantContext::FetchedUrl(fetched_url_context) => {
3008 cx.open_url(&fetched_url_context.url);
3009 }
3010 AssistantContext::Thread(thread_context) => {
3011 let thread_id = thread_context.thread.read(cx).id().clone();
3012 workspace.update(cx, |workspace, cx| {
3013 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
3014 panel.update(cx, |panel, cx| {
3015 panel
3016 .open_thread(&thread_id, window, cx)
3017 .detach_and_log_err(cx)
3018 });
3019 }
3020 })
3021 }
3022 }
3023}