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