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::{RulesLoadingError, 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, StopReason};
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 cx.subscribe(&thread_store, Self::handle_rules_loading_error),
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::Stopped(reason) => match reason {
838 Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
839 let thread = self.thread.read(cx);
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 },
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 { tool_uses } => {
898 for tool_use in tool_uses {
899 self.render_tool_use_markdown(
900 tool_use.id.clone(),
901 tool_use.ui_text.clone(),
902 &tool_use.input,
903 "".into(),
904 cx,
905 );
906 }
907 }
908 ThreadEvent::ToolFinished {
909 pending_tool_use, ..
910 } => {
911 if let Some(tool_use) = pending_tool_use {
912 self.render_tool_use_markdown(
913 tool_use.id.clone(),
914 tool_use.ui_text.clone(),
915 &tool_use.input,
916 self.thread
917 .read(cx)
918 .tool_result(&tool_use.id)
919 .map(|result| result.content.clone().into())
920 .unwrap_or("".into()),
921 cx,
922 );
923 }
924 }
925 ThreadEvent::CheckpointChanged => cx.notify(),
926 }
927 }
928
929 fn handle_rules_loading_error(
930 &mut self,
931 _thread_store: Entity<ThreadStore>,
932 error: &RulesLoadingError,
933 cx: &mut Context<Self>,
934 ) {
935 self.last_error = Some(ThreadError::Message {
936 header: "Error loading rules file".into(),
937 message: error.message.clone(),
938 });
939 cx.notify();
940 }
941
942 fn show_notification(
943 &mut self,
944 caption: impl Into<SharedString>,
945 icon: IconName,
946 window: &mut Window,
947 cx: &mut Context<ActiveThread>,
948 ) {
949 if window.is_window_active() || !self.notifications.is_empty() {
950 return;
951 }
952
953 let title = self
954 .thread
955 .read(cx)
956 .summary()
957 .unwrap_or("Agent Panel".into());
958
959 match AssistantSettings::get_global(cx).notify_when_agent_waiting {
960 NotifyWhenAgentWaiting::PrimaryScreen => {
961 if let Some(primary) = cx.primary_display() {
962 self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
963 }
964 }
965 NotifyWhenAgentWaiting::AllScreens => {
966 let caption = caption.into();
967 for screen in cx.displays() {
968 self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
969 }
970 }
971 NotifyWhenAgentWaiting::Never => {
972 // Don't show anything
973 }
974 }
975 }
976
977 fn pop_up(
978 &mut self,
979 icon: IconName,
980 caption: SharedString,
981 title: SharedString,
982 window: &mut Window,
983 screen: Rc<dyn PlatformDisplay>,
984 cx: &mut Context<'_, ActiveThread>,
985 ) {
986 let options = AgentNotification::window_options(screen, cx);
987
988 if let Some(screen_window) = cx
989 .open_window(options, |_, cx| {
990 cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
991 })
992 .log_err()
993 {
994 if let Some(pop_up) = screen_window.entity(cx).log_err() {
995 self.notification_subscriptions
996 .entry(screen_window)
997 .or_insert_with(Vec::new)
998 .push(cx.subscribe_in(&pop_up, window, {
999 |this, _, event, window, cx| match event {
1000 AgentNotificationEvent::Accepted => {
1001 let handle = window.window_handle();
1002 cx.activate(true);
1003
1004 let workspace_handle = this.workspace.clone();
1005
1006 // If there are multiple Zed windows, activate the correct one.
1007 cx.defer(move |cx| {
1008 handle
1009 .update(cx, |_view, window, _cx| {
1010 window.activate_window();
1011
1012 if let Some(workspace) = workspace_handle.upgrade() {
1013 workspace.update(_cx, |workspace, cx| {
1014 workspace
1015 .focus_panel::<AssistantPanel>(window, cx);
1016 });
1017 }
1018 })
1019 .log_err();
1020 });
1021
1022 this.dismiss_notifications(cx);
1023 }
1024 AgentNotificationEvent::Dismissed => {
1025 this.dismiss_notifications(cx);
1026 }
1027 }
1028 }));
1029
1030 self.notifications.push(screen_window);
1031
1032 // If the user manually refocuses the original window, dismiss the popup.
1033 self.notification_subscriptions
1034 .entry(screen_window)
1035 .or_insert_with(Vec::new)
1036 .push({
1037 let pop_up_weak = pop_up.downgrade();
1038
1039 cx.observe_window_activation(window, move |_, window, cx| {
1040 if window.is_window_active() {
1041 if let Some(pop_up) = pop_up_weak.upgrade() {
1042 pop_up.update(cx, |_, cx| {
1043 cx.emit(AgentNotificationEvent::Dismissed);
1044 });
1045 }
1046 }
1047 })
1048 });
1049 }
1050 }
1051 }
1052
1053 /// Spawns a task to save the active thread.
1054 ///
1055 /// Only one task to save the thread will be in flight at a time.
1056 fn save_thread(&mut self, cx: &mut Context<Self>) {
1057 let thread = self.thread.clone();
1058 self.save_thread_task = Some(cx.spawn(async move |this, cx| {
1059 let task = this
1060 .update(cx, |this, cx| {
1061 this.thread_store
1062 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
1063 })
1064 .ok();
1065
1066 if let Some(task) = task {
1067 task.await.log_err();
1068 }
1069 }));
1070 }
1071
1072 fn start_editing_message(
1073 &mut self,
1074 message_id: MessageId,
1075 message_segments: &[MessageSegment],
1076 window: &mut Window,
1077 cx: &mut Context<Self>,
1078 ) {
1079 // User message should always consist of a single text segment,
1080 // therefore we can skip returning early if it's not a text segment.
1081 let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
1082 return;
1083 };
1084
1085 let buffer = cx.new(|cx| {
1086 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
1087 });
1088 let editor = cx.new(|cx| {
1089 let mut editor = Editor::new(
1090 editor::EditorMode::AutoHeight { max_lines: 8 },
1091 buffer,
1092 None,
1093 window,
1094 cx,
1095 );
1096 editor.focus_handle(cx).focus(window);
1097 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
1098 editor
1099 });
1100 self.editing_message = Some((
1101 message_id,
1102 EditMessageState {
1103 editor: editor.clone(),
1104 },
1105 ));
1106 cx.notify();
1107 }
1108
1109 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
1110 self.editing_message.take();
1111 cx.notify();
1112 }
1113
1114 fn confirm_editing_message(
1115 &mut self,
1116 _: &menu::Confirm,
1117 _: &mut Window,
1118 cx: &mut Context<Self>,
1119 ) {
1120 let Some((message_id, state)) = self.editing_message.take() else {
1121 return;
1122 };
1123 let edited_text = state.editor.read(cx).text(cx);
1124 self.thread.update(cx, |thread, cx| {
1125 thread.edit_message(
1126 message_id,
1127 Role::User,
1128 vec![MessageSegment::Text(edited_text)],
1129 cx,
1130 );
1131 for message_id in self.messages_after(message_id) {
1132 thread.delete_message(*message_id, cx);
1133 }
1134 });
1135
1136 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1137 return;
1138 };
1139
1140 if model.provider.must_accept_terms(cx) {
1141 cx.notify();
1142 return;
1143 }
1144
1145 self.thread.update(cx, |thread, cx| {
1146 thread.send_to_model(model.model, RequestKind::Chat, cx)
1147 });
1148 cx.notify();
1149 }
1150
1151 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
1152 self.messages
1153 .iter()
1154 .position(|id| *id == message_id)
1155 .map(|index| &self.messages[index + 1..])
1156 .unwrap_or(&[])
1157 }
1158
1159 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1160 self.cancel_editing_message(&menu::Cancel, window, cx);
1161 }
1162
1163 fn handle_regenerate_click(
1164 &mut self,
1165 _: &ClickEvent,
1166 window: &mut Window,
1167 cx: &mut Context<Self>,
1168 ) {
1169 self.confirm_editing_message(&menu::Confirm, window, cx);
1170 }
1171
1172 fn handle_feedback_click(
1173 &mut self,
1174 message_id: MessageId,
1175 feedback: ThreadFeedback,
1176 window: &mut Window,
1177 cx: &mut Context<Self>,
1178 ) {
1179 let report = self.thread.update(cx, |thread, cx| {
1180 thread.report_message_feedback(message_id, feedback, cx)
1181 });
1182
1183 cx.spawn(async move |this, cx| {
1184 report.await?;
1185 this.update(cx, |_this, cx| cx.notify())
1186 })
1187 .detach_and_log_err(cx);
1188
1189 match feedback {
1190 ThreadFeedback::Positive => {
1191 self.open_feedback_editors.remove(&message_id);
1192 }
1193 ThreadFeedback::Negative => {
1194 self.handle_show_feedback_comments(message_id, window, cx);
1195 }
1196 }
1197 }
1198
1199 fn handle_show_feedback_comments(
1200 &mut self,
1201 message_id: MessageId,
1202 window: &mut Window,
1203 cx: &mut Context<Self>,
1204 ) {
1205 let buffer = cx.new(|cx| {
1206 let empty_string = String::new();
1207 MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
1208 });
1209
1210 let editor = cx.new(|cx| {
1211 let mut editor = Editor::new(
1212 editor::EditorMode::AutoHeight { max_lines: 4 },
1213 buffer,
1214 None,
1215 window,
1216 cx,
1217 );
1218 editor.set_placeholder_text(
1219 "What went wrong? Share your feedback so we can improve.",
1220 cx,
1221 );
1222 editor
1223 });
1224
1225 editor.read(cx).focus_handle(cx).focus(window);
1226 self.open_feedback_editors.insert(message_id, editor);
1227 cx.notify();
1228 }
1229
1230 fn submit_feedback_message(&mut self, message_id: MessageId, cx: &mut Context<Self>) {
1231 let Some(editor) = self.open_feedback_editors.get(&message_id) else {
1232 return;
1233 };
1234
1235 let report_task = self.thread.update(cx, |thread, cx| {
1236 thread.report_message_feedback(message_id, ThreadFeedback::Negative, cx)
1237 });
1238
1239 let comments = editor.read(cx).text(cx);
1240 if !comments.is_empty() {
1241 let thread_id = self.thread.read(cx).id().clone();
1242 let comments_value = String::from(comments.as_str());
1243
1244 let message_content = self
1245 .thread
1246 .read(cx)
1247 .message(message_id)
1248 .map(|msg| msg.to_string())
1249 .unwrap_or_default();
1250
1251 telemetry::event!(
1252 "Assistant Thread Feedback Comments",
1253 thread_id,
1254 message_id = message_id.0,
1255 message_content,
1256 comments = comments_value
1257 );
1258
1259 self.open_feedback_editors.remove(&message_id);
1260
1261 cx.spawn(async move |this, cx| {
1262 report_task.await?;
1263 this.update(cx, |_this, cx| cx.notify())
1264 })
1265 .detach_and_log_err(cx);
1266 }
1267 }
1268
1269 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1270 let message_id = self.messages[ix];
1271 let Some(message) = self.thread.read(cx).message(message_id) else {
1272 return Empty.into_any();
1273 };
1274
1275 let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
1276 return Empty.into_any();
1277 };
1278
1279 let context_store = self.context_store.clone();
1280 let workspace = self.workspace.clone();
1281 let thread = self.thread.read(cx);
1282
1283 // Get all the data we need from thread before we start using it in closures
1284 let checkpoint = thread.checkpoint_for_message(message_id);
1285 let context = thread.context_for_message(message_id).collect::<Vec<_>>();
1286
1287 let tool_uses = thread.tool_uses_for_message(message_id, cx);
1288 let has_tool_uses = !tool_uses.is_empty();
1289 let is_generating = thread.is_generating();
1290
1291 let is_first_message = ix == 0;
1292 let is_last_message = ix == self.messages.len() - 1;
1293
1294 let show_feedback = (!is_generating && is_last_message && message.role != Role::User)
1295 || self.messages.get(ix + 1).map_or(false, |next_id| {
1296 self.thread
1297 .read(cx)
1298 .message(*next_id)
1299 .map_or(false, |next_message| {
1300 next_message.role == Role::User
1301 && thread.tool_uses_for_message(*next_id, cx).is_empty()
1302 && thread.tool_results_for_message(*next_id).is_empty()
1303 })
1304 });
1305
1306 let needs_confirmation = tool_uses.iter().any(|tool_use| tool_use.needs_confirmation);
1307
1308 let generating_label = (is_generating && is_last_message).then(|| {
1309 Label::new("Generating")
1310 .color(Color::Muted)
1311 .size(LabelSize::Small)
1312 .with_animations(
1313 "generating-label",
1314 vec![
1315 Animation::new(Duration::from_secs(1)),
1316 Animation::new(Duration::from_secs(1)).repeat(),
1317 ],
1318 |mut label, animation_ix, delta| {
1319 match animation_ix {
1320 0 => {
1321 let chars_to_show = (delta * 10.).ceil() as usize;
1322 let text = &"Generating"[0..chars_to_show];
1323 label.set_text(text);
1324 }
1325 1 => {
1326 let text = match delta {
1327 d if d < 0.25 => "Generating",
1328 d if d < 0.5 => "Generating.",
1329 d if d < 0.75 => "Generating..",
1330 _ => "Generating...",
1331 };
1332 label.set_text(text);
1333 }
1334 _ => {}
1335 }
1336 label
1337 },
1338 )
1339 .with_animation(
1340 "pulsating-label",
1341 Animation::new(Duration::from_secs(2))
1342 .repeat()
1343 .with_easing(pulsating_between(0.6, 1.)),
1344 |label, delta| label.map_element(|label| label.alpha(delta)),
1345 )
1346 });
1347
1348 // Don't render user messages that are just there for returning tool results.
1349 if message.role == Role::User && thread.message_has_tool_results(message_id) {
1350 if let Some(generating_label) = generating_label {
1351 return h_flex()
1352 .w_full()
1353 .h_10()
1354 .py_1p5()
1355 .pl_4()
1356 .pb_3()
1357 .child(generating_label)
1358 .into_any_element();
1359 }
1360
1361 return Empty.into_any();
1362 }
1363
1364 let allow_editing_message = message.role == Role::User;
1365
1366 let edit_message_editor = self
1367 .editing_message
1368 .as_ref()
1369 .filter(|(id, _)| *id == message_id)
1370 .map(|(_, state)| state.editor.clone());
1371
1372 let colors = cx.theme().colors();
1373 let active_color = colors.element_active;
1374 let editor_bg_color = colors.editor_background;
1375 let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
1376
1377 let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileCode)
1378 .shape(ui::IconButtonShape::Square)
1379 .icon_size(IconSize::XSmall)
1380 .icon_color(Color::Ignored)
1381 .tooltip(Tooltip::text("Open Thread as Markdown"))
1382 .on_click(|_event, window, cx| {
1383 window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
1384 });
1385
1386 let feedback_container = h_flex().py_2().px_4().gap_1().justify_between();
1387 let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
1388 Some(feedback) => feedback_container
1389 .child(
1390 Label::new(match feedback {
1391 ThreadFeedback::Positive => "Thanks for your feedback!",
1392 ThreadFeedback::Negative => {
1393 "We appreciate your feedback and will use it to improve."
1394 }
1395 })
1396 .color(Color::Muted)
1397 .size(LabelSize::XSmall),
1398 )
1399 .child(
1400 h_flex()
1401 .pr_1()
1402 .gap_1()
1403 .child(
1404 IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
1405 .shape(ui::IconButtonShape::Square)
1406 .icon_size(IconSize::XSmall)
1407 .icon_color(match feedback {
1408 ThreadFeedback::Positive => Color::Accent,
1409 ThreadFeedback::Negative => Color::Ignored,
1410 })
1411 .tooltip(Tooltip::text("Helpful Response"))
1412 .on_click(cx.listener(move |this, _, window, cx| {
1413 this.handle_feedback_click(
1414 message_id,
1415 ThreadFeedback::Positive,
1416 window,
1417 cx,
1418 );
1419 })),
1420 )
1421 .child(
1422 IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
1423 .shape(ui::IconButtonShape::Square)
1424 .icon_size(IconSize::XSmall)
1425 .icon_color(match feedback {
1426 ThreadFeedback::Positive => Color::Ignored,
1427 ThreadFeedback::Negative => Color::Accent,
1428 })
1429 .tooltip(Tooltip::text("Not Helpful"))
1430 .on_click(cx.listener(move |this, _, window, cx| {
1431 this.handle_feedback_click(
1432 message_id,
1433 ThreadFeedback::Negative,
1434 window,
1435 cx,
1436 );
1437 })),
1438 )
1439 .child(open_as_markdown),
1440 )
1441 .into_any_element(),
1442 None => feedback_container
1443 .child(
1444 Label::new(
1445 "Rating the thread sends all of your current conversation to the Zed team.",
1446 )
1447 .color(Color::Muted)
1448 .size(LabelSize::XSmall),
1449 )
1450 .child(
1451 h_flex()
1452 .pr_1()
1453 .gap_1()
1454 .child(
1455 IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
1456 .icon_size(IconSize::XSmall)
1457 .icon_color(Color::Ignored)
1458 .shape(ui::IconButtonShape::Square)
1459 .tooltip(Tooltip::text("Helpful Response"))
1460 .on_click(cx.listener(move |this, _, window, cx| {
1461 this.handle_feedback_click(
1462 message_id,
1463 ThreadFeedback::Positive,
1464 window,
1465 cx,
1466 );
1467 })),
1468 )
1469 .child(
1470 IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
1471 .icon_size(IconSize::XSmall)
1472 .icon_color(Color::Ignored)
1473 .shape(ui::IconButtonShape::Square)
1474 .tooltip(Tooltip::text("Not Helpful"))
1475 .on_click(cx.listener(move |this, _, window, cx| {
1476 this.handle_feedback_click(
1477 message_id,
1478 ThreadFeedback::Negative,
1479 window,
1480 cx,
1481 );
1482 })),
1483 )
1484 .child(open_as_markdown),
1485 )
1486 .into_any_element(),
1487 };
1488
1489 let message_is_empty = message.should_display_content();
1490 let has_content = !message_is_empty || !context.is_empty();
1491
1492 let message_content =
1493 has_content.then(|| {
1494 v_flex()
1495 .gap_1p5()
1496 .when(!message_is_empty, |parent| {
1497 parent.child(
1498 if let Some(edit_message_editor) = edit_message_editor.clone() {
1499 div()
1500 .key_context("EditMessageEditor")
1501 .on_action(cx.listener(Self::cancel_editing_message))
1502 .on_action(cx.listener(Self::confirm_editing_message))
1503 .min_h_6()
1504 .child(edit_message_editor)
1505 .into_any()
1506 } else {
1507 div()
1508 .min_h_6()
1509 .text_ui(cx)
1510 .child(self.render_message_content(
1511 message_id,
1512 rendered_message,
1513 has_tool_uses,
1514 workspace.clone(),
1515 window,
1516 cx,
1517 ))
1518 .into_any()
1519 },
1520 )
1521 })
1522 .when(!context.is_empty(), |parent| {
1523 parent.child(h_flex().flex_wrap().gap_1().children(
1524 context.into_iter().map(|context| {
1525 let context_id = context.id();
1526 ContextPill::added(
1527 AddedContext::new(context, cx),
1528 false,
1529 false,
1530 None,
1531 )
1532 .on_click(Rc::new(cx.listener({
1533 let workspace = workspace.clone();
1534 let context_store = context_store.clone();
1535 move |_, _, window, cx| {
1536 if let Some(workspace) = workspace.upgrade() {
1537 open_context(
1538 context_id,
1539 context_store.clone(),
1540 workspace,
1541 window,
1542 cx,
1543 );
1544 cx.notify();
1545 }
1546 }
1547 })))
1548 }),
1549 ))
1550 })
1551 });
1552
1553 let styled_message = match message.role {
1554 Role::User => v_flex()
1555 .id(("message-container", ix))
1556 .map(|this| {
1557 if is_first_message {
1558 this.pt_2()
1559 } else {
1560 this.pt_4()
1561 }
1562 })
1563 .pb_4()
1564 .pl_2()
1565 .pr_2p5()
1566 .child(
1567 v_flex()
1568 .bg(colors.editor_background)
1569 .rounded_lg()
1570 .border_1()
1571 .border_color(colors.border)
1572 .shadow_md()
1573 .child(
1574 h_flex()
1575 .py_1()
1576 .pl_2()
1577 .pr_1()
1578 .bg(bg_user_message_header)
1579 .border_b_1()
1580 .border_color(colors.border)
1581 .justify_between()
1582 .rounded_t_md()
1583 .child(
1584 h_flex()
1585 .gap_1p5()
1586 .child(
1587 Icon::new(IconName::PersonCircle)
1588 .size(IconSize::XSmall)
1589 .color(Color::Muted),
1590 )
1591 .child(
1592 Label::new("You")
1593 .size(LabelSize::Small)
1594 .color(Color::Muted),
1595 ),
1596 )
1597 .child(
1598 h_flex()
1599 .gap_1()
1600 .when_some(
1601 edit_message_editor.clone(),
1602 |this, edit_message_editor| {
1603 let focus_handle =
1604 edit_message_editor.focus_handle(cx);
1605 this.child(
1606 Button::new("cancel-edit-message", "Cancel")
1607 .label_size(LabelSize::Small)
1608 .key_binding(
1609 KeyBinding::for_action_in(
1610 &menu::Cancel,
1611 &focus_handle,
1612 window,
1613 cx,
1614 )
1615 .map(|kb| kb.size(rems_from_px(12.))),
1616 )
1617 .on_click(
1618 cx.listener(Self::handle_cancel_click),
1619 ),
1620 )
1621 .child(
1622 Button::new(
1623 "confirm-edit-message",
1624 "Regenerate",
1625 )
1626 .label_size(LabelSize::Small)
1627 .key_binding(
1628 KeyBinding::for_action_in(
1629 &menu::Confirm,
1630 &focus_handle,
1631 window,
1632 cx,
1633 )
1634 .map(|kb| kb.size(rems_from_px(12.))),
1635 )
1636 .on_click(
1637 cx.listener(Self::handle_regenerate_click),
1638 ),
1639 )
1640 },
1641 )
1642 .when(
1643 edit_message_editor.is_none() && allow_editing_message,
1644 |this| {
1645 this.child(
1646 Button::new("edit-message", "Edit")
1647 .label_size(LabelSize::Small)
1648 .on_click(cx.listener({
1649 let message_segments =
1650 message.segments.clone();
1651 move |this, _, window, cx| {
1652 this.start_editing_message(
1653 message_id,
1654 &message_segments,
1655 window,
1656 cx,
1657 );
1658 }
1659 })),
1660 )
1661 },
1662 ),
1663 ),
1664 )
1665 .child(div().p_2().children(message_content)),
1666 ),
1667 Role::Assistant => v_flex()
1668 .id(("message-container", ix))
1669 .ml_2()
1670 .pl_2()
1671 .pr_4()
1672 .border_l_1()
1673 .border_color(cx.theme().colors().border_variant)
1674 .children(message_content)
1675 .when(has_tool_uses, |parent| {
1676 parent.children(
1677 tool_uses
1678 .into_iter()
1679 .map(|tool_use| self.render_tool_use(tool_use, window, cx)),
1680 )
1681 }),
1682 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1683 v_flex()
1684 .bg(colors.editor_background)
1685 .rounded_sm()
1686 .child(div().p_4().children(message_content)),
1687 ),
1688 };
1689
1690 v_flex()
1691 .w_full()
1692 .when_some(checkpoint, |parent, checkpoint| {
1693 let mut is_pending = false;
1694 let mut error = None;
1695 if let Some(last_restore_checkpoint) =
1696 self.thread.read(cx).last_restore_checkpoint()
1697 {
1698 if last_restore_checkpoint.message_id() == message_id {
1699 match last_restore_checkpoint {
1700 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1701 LastRestoreCheckpoint::Error { error: err, .. } => {
1702 error = Some(err.clone());
1703 }
1704 }
1705 }
1706 }
1707
1708 let restore_checkpoint_button =
1709 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1710 .icon(if error.is_some() {
1711 IconName::XCircle
1712 } else {
1713 IconName::Undo
1714 })
1715 .icon_size(IconSize::XSmall)
1716 .icon_position(IconPosition::Start)
1717 .icon_color(if error.is_some() {
1718 Some(Color::Error)
1719 } else {
1720 None
1721 })
1722 .label_size(LabelSize::XSmall)
1723 .disabled(is_pending)
1724 .on_click(cx.listener(move |this, _, _window, cx| {
1725 this.thread.update(cx, |thread, cx| {
1726 thread
1727 .restore_checkpoint(checkpoint.clone(), cx)
1728 .detach_and_log_err(cx);
1729 });
1730 }));
1731
1732 let restore_checkpoint_button = if is_pending {
1733 restore_checkpoint_button
1734 .with_animation(
1735 ("pulsating-restore-checkpoint-button", ix),
1736 Animation::new(Duration::from_secs(2))
1737 .repeat()
1738 .with_easing(pulsating_between(0.6, 1.)),
1739 |label, delta| label.alpha(delta),
1740 )
1741 .into_any_element()
1742 } else if let Some(error) = error {
1743 restore_checkpoint_button
1744 .tooltip(Tooltip::text(error.to_string()))
1745 .into_any_element()
1746 } else {
1747 restore_checkpoint_button.into_any_element()
1748 };
1749
1750 parent.child(
1751 h_flex()
1752 .pt_2p5()
1753 .px_2p5()
1754 .w_full()
1755 .gap_1()
1756 .child(ui::Divider::horizontal())
1757 .child(restore_checkpoint_button)
1758 .child(ui::Divider::horizontal()),
1759 )
1760 })
1761 .when(is_first_message, |parent| {
1762 parent.child(self.render_rules_item(cx))
1763 })
1764 .child(styled_message)
1765 .when(!needs_confirmation && generating_label.is_some(), |this| {
1766 this.child(
1767 h_flex()
1768 .h_8()
1769 .mt_2()
1770 .mb_4()
1771 .ml_4()
1772 .py_1p5()
1773 .child(generating_label.unwrap()),
1774 )
1775 })
1776 .when(show_feedback, move |parent| {
1777 parent.child(feedback_items).when_some(
1778 self.open_feedback_editors.get(&message_id),
1779 move |parent, feedback_editor| {
1780 let focus_handle = feedback_editor.focus_handle(cx);
1781 parent.child(
1782 v_flex()
1783 .key_context("AgentFeedbackMessageEditor")
1784 .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
1785 this.open_feedback_editors.remove(&message_id);
1786 cx.notify();
1787 }))
1788 .on_action(cx.listener(move |this, _: &menu::Confirm, _, cx| {
1789 this.submit_feedback_message(message_id, cx);
1790 cx.notify();
1791 }))
1792 .on_action(cx.listener(Self::confirm_editing_message))
1793 .mb_2()
1794 .mx_4()
1795 .p_2()
1796 .rounded_md()
1797 .border_1()
1798 .border_color(cx.theme().colors().border)
1799 .bg(cx.theme().colors().editor_background)
1800 .child(feedback_editor.clone())
1801 .child(
1802 h_flex()
1803 .gap_1()
1804 .justify_end()
1805 .child(
1806 Button::new("dismiss-feedback-message", "Cancel")
1807 .label_size(LabelSize::Small)
1808 .key_binding(
1809 KeyBinding::for_action_in(
1810 &menu::Cancel,
1811 &focus_handle,
1812 window,
1813 cx,
1814 )
1815 .map(|kb| kb.size(rems_from_px(10.))),
1816 )
1817 .on_click(cx.listener(
1818 move |this, _, _window, cx| {
1819 this.open_feedback_editors
1820 .remove(&message_id);
1821 cx.notify();
1822 },
1823 )),
1824 )
1825 .child(
1826 Button::new(
1827 "submit-feedback-message",
1828 "Share Feedback",
1829 )
1830 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
1831 .label_size(LabelSize::Small)
1832 .key_binding(
1833 KeyBinding::for_action_in(
1834 &menu::Confirm,
1835 &focus_handle,
1836 window,
1837 cx,
1838 )
1839 .map(|kb| kb.size(rems_from_px(10.))),
1840 )
1841 .on_click(
1842 cx.listener(move |this, _, _window, cx| {
1843 this.submit_feedback_message(message_id, cx);
1844 cx.notify()
1845 }),
1846 ),
1847 ),
1848 ),
1849 )
1850 },
1851 )
1852 })
1853 .into_any()
1854 }
1855
1856 fn render_message_content(
1857 &self,
1858 message_id: MessageId,
1859 rendered_message: &RenderedMessage,
1860 has_tool_uses: bool,
1861 workspace: WeakEntity<Workspace>,
1862 window: &Window,
1863 cx: &Context<Self>,
1864 ) -> impl IntoElement {
1865 let is_last_message = self.messages.last() == Some(&message_id);
1866 let is_generating = self.thread.read(cx).is_generating();
1867 let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses {
1868 rendered_message
1869 .segments
1870 .iter()
1871 .enumerate()
1872 .next_back()
1873 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1874 .map(|(index, _)| index)
1875 } else {
1876 None
1877 };
1878
1879 v_flex()
1880 .text_ui(cx)
1881 .gap_2()
1882 .children(
1883 rendered_message.segments.iter().enumerate().map(
1884 |(index, segment)| match segment {
1885 RenderedMessageSegment::Thinking {
1886 content,
1887 scroll_handle,
1888 } => self
1889 .render_message_thinking_segment(
1890 message_id,
1891 index,
1892 content.clone(),
1893 &scroll_handle,
1894 Some(index) == pending_thinking_segment_index,
1895 window,
1896 cx,
1897 )
1898 .into_any_element(),
1899 RenderedMessageSegment::Text(markdown) => div()
1900 .child(
1901 MarkdownElement::new(
1902 markdown.clone(),
1903 default_markdown_style(window, cx),
1904 )
1905 .code_block_renderer(markdown::CodeBlockRenderer::Custom {
1906 render: Arc::new({
1907 let workspace = workspace.clone();
1908 let active_thread = cx.entity();
1909 move |kind, parsed_markdown, range, metadata, window, cx| {
1910 render_markdown_code_block(
1911 message_id,
1912 range.start,
1913 kind,
1914 parsed_markdown,
1915 metadata,
1916 active_thread.clone(),
1917 workspace.clone(),
1918 window,
1919 cx,
1920 )
1921 }
1922 }),
1923 transform: Some(Arc::new({
1924 let active_thread = cx.entity();
1925 move |el, range, metadata, _, cx| {
1926 let is_expanded = active_thread
1927 .read(cx)
1928 .expanded_code_blocks
1929 .get(&(message_id, range.start))
1930 .copied()
1931 .unwrap_or(false);
1932
1933 if is_expanded
1934 || metadata.line_count
1935 <= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
1936 {
1937 return el;
1938 }
1939 el.child(
1940 div()
1941 .absolute()
1942 .bottom_0()
1943 .left_0()
1944 .w_full()
1945 .h_1_4()
1946 .rounded_b_lg()
1947 .bg(gpui::linear_gradient(
1948 0.,
1949 gpui::linear_color_stop(
1950 cx.theme().colors().editor_background,
1951 0.,
1952 ),
1953 gpui::linear_color_stop(
1954 cx.theme()
1955 .colors()
1956 .editor_background
1957 .opacity(0.),
1958 1.,
1959 ),
1960 )),
1961 )
1962 }
1963 })),
1964 })
1965 .on_url_click({
1966 let workspace = self.workspace.clone();
1967 move |text, window, cx| {
1968 open_markdown_link(text, workspace.clone(), window, cx);
1969 }
1970 }),
1971 )
1972 .into_any_element(),
1973 },
1974 ),
1975 )
1976 }
1977
1978 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1979 cx.theme().colors().border.opacity(0.5)
1980 }
1981
1982 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1983 cx.theme()
1984 .colors()
1985 .element_background
1986 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1987 }
1988
1989 fn render_message_thinking_segment(
1990 &self,
1991 message_id: MessageId,
1992 ix: usize,
1993 markdown: Entity<Markdown>,
1994 scroll_handle: &ScrollHandle,
1995 pending: bool,
1996 window: &Window,
1997 cx: &Context<Self>,
1998 ) -> impl IntoElement {
1999 let is_open = self
2000 .expanded_thinking_segments
2001 .get(&(message_id, ix))
2002 .copied()
2003 .unwrap_or_default();
2004
2005 let editor_bg = cx.theme().colors().panel_background;
2006
2007 div().map(|this| {
2008 if pending {
2009 this.v_flex()
2010 .mt_neg_2()
2011 .mb_1p5()
2012 .child(
2013 h_flex()
2014 .group("disclosure-header")
2015 .justify_between()
2016 .child(
2017 h_flex()
2018 .gap_1p5()
2019 .child(
2020 Icon::new(IconName::LightBulb)
2021 .size(IconSize::XSmall)
2022 .color(Color::Muted),
2023 )
2024 .child({
2025 Label::new("Thinking")
2026 .color(Color::Muted)
2027 .size(LabelSize::Small)
2028 .with_animation(
2029 "generating-label",
2030 Animation::new(Duration::from_secs(1)).repeat(),
2031 |mut label, delta| {
2032 let text = match delta {
2033 d if d < 0.25 => "Thinking",
2034 d if d < 0.5 => "Thinking.",
2035 d if d < 0.75 => "Thinking..",
2036 _ => "Thinking...",
2037 };
2038 label.set_text(text);
2039 label
2040 },
2041 )
2042 .with_animation(
2043 "pulsating-label",
2044 Animation::new(Duration::from_secs(2))
2045 .repeat()
2046 .with_easing(pulsating_between(0.6, 1.)),
2047 |label, delta| {
2048 label.map_element(|label| label.alpha(delta))
2049 },
2050 )
2051 }),
2052 )
2053 .child(
2054 h_flex()
2055 .gap_1()
2056 .child(
2057 div().visible_on_hover("disclosure-header").child(
2058 Disclosure::new("thinking-disclosure", is_open)
2059 .opened_icon(IconName::ChevronUp)
2060 .closed_icon(IconName::ChevronDown)
2061 .on_click(cx.listener({
2062 move |this, _event, _window, _cx| {
2063 let is_open = this
2064 .expanded_thinking_segments
2065 .entry((message_id, ix))
2066 .or_insert(false);
2067
2068 *is_open = !*is_open;
2069 }
2070 })),
2071 ),
2072 )
2073 .child({
2074 Icon::new(IconName::ArrowCircle)
2075 .color(Color::Accent)
2076 .size(IconSize::Small)
2077 .with_animation(
2078 "arrow-circle",
2079 Animation::new(Duration::from_secs(2)).repeat(),
2080 |icon, delta| {
2081 icon.transform(Transformation::rotate(
2082 percentage(delta),
2083 ))
2084 },
2085 )
2086 }),
2087 ),
2088 )
2089 .when(!is_open, |this| {
2090 let gradient_overlay = div()
2091 .rounded_b_lg()
2092 .h_full()
2093 .absolute()
2094 .w_full()
2095 .bottom_0()
2096 .left_0()
2097 .bg(linear_gradient(
2098 180.,
2099 linear_color_stop(editor_bg, 1.),
2100 linear_color_stop(editor_bg.opacity(0.2), 0.),
2101 ));
2102
2103 this.child(
2104 div()
2105 .relative()
2106 .bg(editor_bg)
2107 .rounded_b_lg()
2108 .mt_2()
2109 .pl_4()
2110 .child(
2111 div()
2112 .id(("thinking-content", ix))
2113 .max_h_20()
2114 .track_scroll(scroll_handle)
2115 .text_ui_sm(cx)
2116 .overflow_hidden()
2117 .child(
2118 MarkdownElement::new(
2119 markdown.clone(),
2120 default_markdown_style(window, cx),
2121 )
2122 .on_url_click({
2123 let workspace = self.workspace.clone();
2124 move |text, window, cx| {
2125 open_markdown_link(
2126 text,
2127 workspace.clone(),
2128 window,
2129 cx,
2130 );
2131 }
2132 }),
2133 ),
2134 )
2135 .child(gradient_overlay),
2136 )
2137 })
2138 .when(is_open, |this| {
2139 this.child(
2140 div()
2141 .id(("thinking-content", ix))
2142 .h_full()
2143 .bg(editor_bg)
2144 .text_ui_sm(cx)
2145 .child(
2146 MarkdownElement::new(
2147 markdown.clone(),
2148 default_markdown_style(window, cx),
2149 )
2150 .on_url_click({
2151 let workspace = self.workspace.clone();
2152 move |text, window, cx| {
2153 open_markdown_link(text, workspace.clone(), window, cx);
2154 }
2155 }),
2156 ),
2157 )
2158 })
2159 } else {
2160 this.v_flex()
2161 .mt_neg_2()
2162 .child(
2163 h_flex()
2164 .group("disclosure-header")
2165 .pr_1()
2166 .justify_between()
2167 .opacity(0.8)
2168 .hover(|style| style.opacity(1.))
2169 .child(
2170 h_flex()
2171 .gap_1p5()
2172 .child(
2173 Icon::new(IconName::LightBulb)
2174 .size(IconSize::XSmall)
2175 .color(Color::Muted),
2176 )
2177 .child(Label::new("Thought Process").size(LabelSize::Small)),
2178 )
2179 .child(
2180 div().visible_on_hover("disclosure-header").child(
2181 Disclosure::new("thinking-disclosure", is_open)
2182 .opened_icon(IconName::ChevronUp)
2183 .closed_icon(IconName::ChevronDown)
2184 .on_click(cx.listener({
2185 move |this, _event, _window, _cx| {
2186 let is_open = this
2187 .expanded_thinking_segments
2188 .entry((message_id, ix))
2189 .or_insert(false);
2190
2191 *is_open = !*is_open;
2192 }
2193 })),
2194 ),
2195 ),
2196 )
2197 .child(
2198 div()
2199 .id(("thinking-content", ix))
2200 .relative()
2201 .mt_1p5()
2202 .ml_1p5()
2203 .pl_2p5()
2204 .border_l_1()
2205 .border_color(cx.theme().colors().border_variant)
2206 .text_ui_sm(cx)
2207 .when(is_open, |this| {
2208 this.child(
2209 MarkdownElement::new(
2210 markdown.clone(),
2211 default_markdown_style(window, cx),
2212 )
2213 .on_url_click({
2214 let workspace = self.workspace.clone();
2215 move |text, window, cx| {
2216 open_markdown_link(text, workspace.clone(), window, cx);
2217 }
2218 }),
2219 )
2220 }),
2221 )
2222 }
2223 })
2224 }
2225
2226 fn render_tool_use(
2227 &self,
2228 tool_use: ToolUse,
2229 window: &mut Window,
2230 cx: &mut Context<Self>,
2231 ) -> impl IntoElement + use<> {
2232 let is_open = self
2233 .expanded_tool_uses
2234 .get(&tool_use.id)
2235 .copied()
2236 .unwrap_or_default();
2237
2238 let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
2239
2240 let fs = self
2241 .workspace
2242 .upgrade()
2243 .map(|workspace| workspace.read(cx).app_state().fs.clone());
2244 let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
2245 let edit_tools = tool_use.needs_confirmation;
2246
2247 let status_icons = div().child(match &tool_use.status {
2248 ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
2249 let icon = Icon::new(IconName::Warning)
2250 .color(Color::Warning)
2251 .size(IconSize::Small);
2252 icon.into_any_element()
2253 }
2254 ToolUseStatus::Running => {
2255 let icon = Icon::new(IconName::ArrowCircle)
2256 .color(Color::Accent)
2257 .size(IconSize::Small);
2258 icon.with_animation(
2259 "arrow-circle",
2260 Animation::new(Duration::from_secs(2)).repeat(),
2261 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
2262 )
2263 .into_any_element()
2264 }
2265 ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
2266 ToolUseStatus::Error(_) => {
2267 let icon = Icon::new(IconName::Close)
2268 .color(Color::Error)
2269 .size(IconSize::Small);
2270 icon.into_any_element()
2271 }
2272 });
2273
2274 let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
2275 let results_content_container = || v_flex().p_2().gap_0p5();
2276
2277 let results_content = v_flex()
2278 .gap_1()
2279 .child(
2280 results_content_container()
2281 .child(
2282 Label::new("Input")
2283 .size(LabelSize::XSmall)
2284 .color(Color::Muted)
2285 .buffer_font(cx),
2286 )
2287 .child(
2288 div()
2289 .w_full()
2290 .text_ui_sm(cx)
2291 .children(rendered_tool_use.as_ref().map(|rendered| {
2292 MarkdownElement::new(
2293 rendered.input.clone(),
2294 tool_use_markdown_style(window, cx),
2295 )
2296 .on_url_click({
2297 let workspace = self.workspace.clone();
2298 move |text, window, cx| {
2299 open_markdown_link(text, workspace.clone(), window, cx);
2300 }
2301 })
2302 })),
2303 ),
2304 )
2305 .map(|container| match tool_use.status {
2306 ToolUseStatus::Finished(_) => container.child(
2307 results_content_container()
2308 .border_t_1()
2309 .border_color(self.tool_card_border_color(cx))
2310 .child(
2311 Label::new("Result")
2312 .size(LabelSize::XSmall)
2313 .color(Color::Muted)
2314 .buffer_font(cx),
2315 )
2316 .child(div().w_full().text_ui_sm(cx).children(
2317 rendered_tool_use.as_ref().map(|rendered| {
2318 MarkdownElement::new(
2319 rendered.output.clone(),
2320 tool_use_markdown_style(window, cx),
2321 )
2322 .on_url_click({
2323 let workspace = self.workspace.clone();
2324 move |text, window, cx| {
2325 open_markdown_link(text, workspace.clone(), window, cx);
2326 }
2327 })
2328 }),
2329 )),
2330 ),
2331 ToolUseStatus::Running => container.child(
2332 results_content_container().child(
2333 h_flex()
2334 .gap_1()
2335 .pb_1()
2336 .border_t_1()
2337 .border_color(self.tool_card_border_color(cx))
2338 .child(
2339 Icon::new(IconName::ArrowCircle)
2340 .size(IconSize::Small)
2341 .color(Color::Accent)
2342 .with_animation(
2343 "arrow-circle",
2344 Animation::new(Duration::from_secs(2)).repeat(),
2345 |icon, delta| {
2346 icon.transform(Transformation::rotate(percentage(
2347 delta,
2348 )))
2349 },
2350 ),
2351 )
2352 .child(
2353 Label::new("Running…")
2354 .size(LabelSize::XSmall)
2355 .color(Color::Muted)
2356 .buffer_font(cx),
2357 ),
2358 ),
2359 ),
2360 ToolUseStatus::Error(_) => container.child(
2361 results_content_container()
2362 .border_t_1()
2363 .border_color(self.tool_card_border_color(cx))
2364 .child(
2365 Label::new("Error")
2366 .size(LabelSize::XSmall)
2367 .color(Color::Muted)
2368 .buffer_font(cx),
2369 )
2370 .child(
2371 div()
2372 .text_ui_sm(cx)
2373 .children(rendered_tool_use.as_ref().map(|rendered| {
2374 MarkdownElement::new(
2375 rendered.output.clone(),
2376 tool_use_markdown_style(window, cx),
2377 )
2378 .on_url_click({
2379 let workspace = self.workspace.clone();
2380 move |text, window, cx| {
2381 open_markdown_link(text, workspace.clone(), window, cx);
2382 }
2383 })
2384 })),
2385 ),
2386 ),
2387 ToolUseStatus::Pending => container,
2388 ToolUseStatus::NeedsConfirmation => container.child(
2389 results_content_container()
2390 .border_t_1()
2391 .border_color(self.tool_card_border_color(cx))
2392 .child(
2393 Label::new("Asking Permission")
2394 .size(LabelSize::Small)
2395 .color(Color::Muted)
2396 .buffer_font(cx),
2397 ),
2398 ),
2399 });
2400
2401 let gradient_overlay = |color: Hsla| {
2402 div()
2403 .h_full()
2404 .absolute()
2405 .w_12()
2406 .bottom_0()
2407 .map(|element| {
2408 if is_status_finished {
2409 element.right_6()
2410 } else {
2411 element.right(px(44.))
2412 }
2413 })
2414 .bg(linear_gradient(
2415 90.,
2416 linear_color_stop(color, 1.),
2417 linear_color_stop(color.opacity(0.2), 0.),
2418 ))
2419 };
2420
2421 div().map(|element| {
2422 if !edit_tools {
2423 element.child(
2424 v_flex()
2425 .my_2()
2426 .child(
2427 h_flex()
2428 .group("disclosure-header")
2429 .relative()
2430 .gap_1p5()
2431 .justify_between()
2432 .opacity(0.8)
2433 .hover(|style| style.opacity(1.))
2434 .when(!is_status_finished, |this| this.pr_2())
2435 .child(
2436 h_flex()
2437 .id("tool-label-container")
2438 .gap_1p5()
2439 .max_w_full()
2440 .overflow_x_scroll()
2441 .child(
2442 Icon::new(tool_use.icon)
2443 .size(IconSize::XSmall)
2444 .color(Color::Muted),
2445 )
2446 .child(
2447 h_flex().pr_8().text_ui_sm(cx).children(
2448 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| {
2449 open_markdown_link(text, workspace.clone(), window, cx);
2450 }}))
2451 ),
2452 ),
2453 )
2454 .child(
2455 h_flex()
2456 .gap_1()
2457 .child(
2458 div().visible_on_hover("disclosure-header").child(
2459 Disclosure::new("tool-use-disclosure", is_open)
2460 .opened_icon(IconName::ChevronUp)
2461 .closed_icon(IconName::ChevronDown)
2462 .on_click(cx.listener({
2463 let tool_use_id = tool_use.id.clone();
2464 move |this, _event, _window, _cx| {
2465 let is_open = this
2466 .expanded_tool_uses
2467 .entry(tool_use_id.clone())
2468 .or_insert(false);
2469
2470 *is_open = !*is_open;
2471 }
2472 })),
2473 ),
2474 )
2475 .child(status_icons),
2476 )
2477 .child(gradient_overlay(cx.theme().colors().panel_background)),
2478 )
2479 .map(|parent| {
2480 if !is_open {
2481 return parent;
2482 }
2483
2484 parent.child(
2485 v_flex()
2486 .mt_1()
2487 .border_1()
2488 .border_color(self.tool_card_border_color(cx))
2489 .bg(cx.theme().colors().editor_background)
2490 .rounded_lg()
2491 .child(results_content),
2492 )
2493 }),
2494 )
2495 } else {
2496 v_flex()
2497 .my_3()
2498 .rounded_lg()
2499 .border_1()
2500 .border_color(self.tool_card_border_color(cx))
2501 .overflow_hidden()
2502 .child(
2503 h_flex()
2504 .group("disclosure-header")
2505 .relative()
2506 .justify_between()
2507 .py_1()
2508 .map(|element| {
2509 if is_status_finished {
2510 element.pl_2().pr_0p5()
2511 } else {
2512 element.px_2()
2513 }
2514 })
2515 .bg(self.tool_card_header_bg(cx))
2516 .map(|element| {
2517 if is_open {
2518 element.border_b_1().rounded_t_md()
2519 } else if needs_confirmation {
2520 element.rounded_t_md()
2521 } else {
2522 element.rounded_md()
2523 }
2524 })
2525 .border_color(self.tool_card_border_color(cx))
2526 .child(
2527 h_flex()
2528 .id("tool-label-container")
2529 .gap_1p5()
2530 .max_w_full()
2531 .overflow_x_scroll()
2532 .child(
2533 Icon::new(tool_use.icon)
2534 .size(IconSize::XSmall)
2535 .color(Color::Muted),
2536 )
2537 .child(
2538 h_flex().pr_8().text_ui_sm(cx).children(
2539 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| {
2540 open_markdown_link(text, workspace.clone(), window, cx);
2541 }}))
2542 ),
2543 ),
2544 )
2545 .child(
2546 h_flex()
2547 .gap_1()
2548 .child(
2549 div().visible_on_hover("disclosure-header").child(
2550 Disclosure::new("tool-use-disclosure", is_open)
2551 .opened_icon(IconName::ChevronUp)
2552 .closed_icon(IconName::ChevronDown)
2553 .on_click(cx.listener({
2554 let tool_use_id = tool_use.id.clone();
2555 move |this, _event, _window, _cx| {
2556 let is_open = this
2557 .expanded_tool_uses
2558 .entry(tool_use_id.clone())
2559 .or_insert(false);
2560
2561 *is_open = !*is_open;
2562 }
2563 })),
2564 ),
2565 )
2566 .child(status_icons),
2567 )
2568 .child(gradient_overlay(self.tool_card_header_bg(cx))),
2569 )
2570 .map(|parent| {
2571 if !is_open {
2572 return parent;
2573 }
2574
2575 parent.child(
2576 v_flex()
2577 .bg(cx.theme().colors().editor_background)
2578 .map(|element| {
2579 if needs_confirmation {
2580 element.rounded_none()
2581 } else {
2582 element.rounded_b_lg()
2583 }
2584 })
2585 .child(results_content),
2586 )
2587 })
2588 .when(needs_confirmation, |this| {
2589 this.child(
2590 h_flex()
2591 .py_1()
2592 .pl_2()
2593 .pr_1()
2594 .gap_1()
2595 .justify_between()
2596 .bg(cx.theme().colors().editor_background)
2597 .border_t_1()
2598 .border_color(self.tool_card_border_color(cx))
2599 .rounded_b_lg()
2600 .child(
2601 Label::new("Waiting for Confirmation…")
2602 .color(Color::Muted)
2603 .size(LabelSize::Small)
2604 .with_animation(
2605 "generating-label",
2606 Animation::new(Duration::from_secs(1)).repeat(),
2607 |mut label, delta| {
2608 let text = match delta {
2609 d if d < 0.25 => "Waiting for Confirmation",
2610 d if d < 0.5 => "Waiting for Confirmation.",
2611 d if d < 0.75 => "Waiting for Confirmation..",
2612 _ => "Waiting for Confirmation...",
2613 };
2614 label.set_text(text);
2615 label
2616 },
2617 )
2618 .with_animation(
2619 "pulsating-label",
2620 Animation::new(Duration::from_secs(2))
2621 .repeat()
2622 .with_easing(pulsating_between(0.6, 1.)),
2623 |label, delta| label.map_element(|label| label.alpha(delta)),
2624 ),
2625 )
2626 .child(
2627 h_flex()
2628 .gap_0p5()
2629 .child({
2630 let tool_id = tool_use.id.clone();
2631 Button::new(
2632 "always-allow-tool-action",
2633 "Always Allow",
2634 )
2635 .label_size(LabelSize::Small)
2636 .icon(IconName::CheckDouble)
2637 .icon_position(IconPosition::Start)
2638 .icon_size(IconSize::Small)
2639 .icon_color(Color::Success)
2640 .tooltip(move |window, cx| {
2641 Tooltip::with_meta(
2642 "Never ask for permission",
2643 None,
2644 "Restore the original behavior in your Agent Panel settings",
2645 window,
2646 cx,
2647 )
2648 })
2649 .on_click(cx.listener(
2650 move |this, event, window, cx| {
2651 if let Some(fs) = fs.clone() {
2652 update_settings_file::<AssistantSettings>(
2653 fs.clone(),
2654 cx,
2655 |settings, _| {
2656 settings.set_always_allow_tool_actions(true);
2657 },
2658 );
2659 }
2660 this.handle_allow_tool(
2661 tool_id.clone(),
2662 event,
2663 window,
2664 cx,
2665 )
2666 },
2667 ))
2668 })
2669 .child(ui::Divider::vertical())
2670 .child({
2671 let tool_id = tool_use.id.clone();
2672 Button::new("allow-tool-action", "Allow")
2673 .label_size(LabelSize::Small)
2674 .icon(IconName::Check)
2675 .icon_position(IconPosition::Start)
2676 .icon_size(IconSize::Small)
2677 .icon_color(Color::Success)
2678 .on_click(cx.listener(
2679 move |this, event, window, cx| {
2680 this.handle_allow_tool(
2681 tool_id.clone(),
2682 event,
2683 window,
2684 cx,
2685 )
2686 },
2687 ))
2688 })
2689 .child({
2690 let tool_id = tool_use.id.clone();
2691 let tool_name: Arc<str> = tool_use.name.into();
2692 Button::new("deny-tool", "Deny")
2693 .label_size(LabelSize::Small)
2694 .icon(IconName::Close)
2695 .icon_position(IconPosition::Start)
2696 .icon_size(IconSize::Small)
2697 .icon_color(Color::Error)
2698 .on_click(cx.listener(
2699 move |this, event, window, cx| {
2700 this.handle_deny_tool(
2701 tool_id.clone(),
2702 tool_name.clone(),
2703 event,
2704 window,
2705 cx,
2706 )
2707 },
2708 ))
2709 }),
2710 ),
2711 )
2712 })
2713 }
2714 })
2715 }
2716
2717 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
2718 let project_context = self.thread.read(cx).project_context();
2719 let project_context = project_context.borrow();
2720 let Some(project_context) = project_context.as_ref() else {
2721 return div().into_any();
2722 };
2723
2724 let rules_files = project_context
2725 .worktrees
2726 .iter()
2727 .filter_map(|worktree| worktree.rules_file.as_ref())
2728 .collect::<Vec<_>>();
2729
2730 let label_text = match rules_files.as_slice() {
2731 &[] => return div().into_any(),
2732 &[rules_file] => {
2733 format!("Using {:?} file", rules_file.path_in_worktree)
2734 }
2735 rules_files => {
2736 format!("Using {} rules files", rules_files.len())
2737 }
2738 };
2739
2740 div()
2741 .pt_2()
2742 .px_2p5()
2743 .child(
2744 h_flex()
2745 .w_full()
2746 .gap_0p5()
2747 .child(
2748 h_flex()
2749 .gap_1p5()
2750 .child(
2751 Icon::new(IconName::File)
2752 .size(IconSize::XSmall)
2753 .color(Color::Disabled),
2754 )
2755 .child(
2756 Label::new(label_text)
2757 .size(LabelSize::XSmall)
2758 .color(Color::Muted)
2759 .buffer_font(cx),
2760 ),
2761 )
2762 .child(
2763 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
2764 .shape(ui::IconButtonShape::Square)
2765 .icon_size(IconSize::XSmall)
2766 .icon_color(Color::Ignored)
2767 .on_click(cx.listener(Self::handle_open_rules))
2768 .tooltip(Tooltip::text("View Rules")),
2769 ),
2770 )
2771 .into_any()
2772 }
2773
2774 fn handle_allow_tool(
2775 &mut self,
2776 tool_use_id: LanguageModelToolUseId,
2777 _: &ClickEvent,
2778 _window: &mut Window,
2779 cx: &mut Context<Self>,
2780 ) {
2781 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
2782 .thread
2783 .read(cx)
2784 .pending_tool(&tool_use_id)
2785 .map(|tool_use| tool_use.status.clone())
2786 {
2787 self.thread.update(cx, |thread, cx| {
2788 thread.run_tool(
2789 c.tool_use_id.clone(),
2790 c.ui_text.clone(),
2791 c.input.clone(),
2792 &c.messages,
2793 c.tool.clone(),
2794 cx,
2795 );
2796 });
2797 }
2798 }
2799
2800 fn handle_deny_tool(
2801 &mut self,
2802 tool_use_id: LanguageModelToolUseId,
2803 tool_name: Arc<str>,
2804 _: &ClickEvent,
2805 _window: &mut Window,
2806 cx: &mut Context<Self>,
2807 ) {
2808 self.thread.update(cx, |thread, cx| {
2809 thread.deny_tool_use(tool_use_id, tool_name, cx);
2810 });
2811 }
2812
2813 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
2814 let project_context = self.thread.read(cx).project_context();
2815 let project_context = project_context.borrow();
2816 let Some(project_context) = project_context.as_ref() else {
2817 return;
2818 };
2819
2820 let abs_paths = project_context
2821 .worktrees
2822 .iter()
2823 .flat_map(|worktree| worktree.rules_file.as_ref())
2824 .map(|rules_file| rules_file.abs_path.to_path_buf())
2825 .collect::<Vec<_>>();
2826
2827 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
2828 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
2829 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
2830 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
2831 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
2832 }) {
2833 task.detach();
2834 }
2835 }
2836
2837 fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
2838 for window in self.notifications.drain(..) {
2839 window
2840 .update(cx, |_, window, _| {
2841 window.remove_window();
2842 })
2843 .ok();
2844
2845 self.notification_subscriptions.remove(&window);
2846 }
2847 }
2848
2849 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
2850 if !self.show_scrollbar && !self.scrollbar_state.is_dragging() {
2851 return None;
2852 }
2853
2854 Some(
2855 div()
2856 .occlude()
2857 .id("active-thread-scrollbar")
2858 .on_mouse_move(cx.listener(|_, _, _, cx| {
2859 cx.notify();
2860 cx.stop_propagation()
2861 }))
2862 .on_hover(|_, _, cx| {
2863 cx.stop_propagation();
2864 })
2865 .on_any_mouse_down(|_, _, cx| {
2866 cx.stop_propagation();
2867 })
2868 .on_mouse_up(
2869 MouseButton::Left,
2870 cx.listener(|_, _, _, cx| {
2871 cx.stop_propagation();
2872 }),
2873 )
2874 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2875 cx.notify();
2876 }))
2877 .h_full()
2878 .absolute()
2879 .right_1()
2880 .top_1()
2881 .bottom_0()
2882 .w(px(12.))
2883 .cursor_default()
2884 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
2885 )
2886 }
2887
2888 fn hide_scrollbar_later(&mut self, cx: &mut Context<Self>) {
2889 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
2890 self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| {
2891 cx.background_executor()
2892 .timer(SCROLLBAR_SHOW_INTERVAL)
2893 .await;
2894 thread
2895 .update(cx, |thread, cx| {
2896 if !thread.scrollbar_state.is_dragging() {
2897 thread.show_scrollbar = false;
2898 cx.notify();
2899 }
2900 })
2901 .log_err();
2902 }))
2903 }
2904}
2905
2906impl Render for ActiveThread {
2907 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2908 v_flex()
2909 .size_full()
2910 .relative()
2911 .on_mouse_move(cx.listener(|this, _, _, cx| {
2912 this.show_scrollbar = true;
2913 this.hide_scrollbar_later(cx);
2914 cx.notify();
2915 }))
2916 .on_scroll_wheel(cx.listener(|this, _, _, cx| {
2917 this.show_scrollbar = true;
2918 this.hide_scrollbar_later(cx);
2919 cx.notify();
2920 }))
2921 .on_mouse_up(
2922 MouseButton::Left,
2923 cx.listener(|this, _, _, cx| {
2924 this.hide_scrollbar_later(cx);
2925 }),
2926 )
2927 .child(list(self.list_state.clone()).flex_grow())
2928 .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
2929 this.child(scrollbar)
2930 })
2931 }
2932}
2933
2934pub(crate) fn open_context(
2935 id: ContextId,
2936 context_store: Entity<ContextStore>,
2937 workspace: Entity<Workspace>,
2938 window: &mut Window,
2939 cx: &mut App,
2940) {
2941 let Some(context) = context_store.read(cx).context_for_id(id) else {
2942 return;
2943 };
2944
2945 match context {
2946 AssistantContext::File(file_context) => {
2947 if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
2948 {
2949 workspace.update(cx, |workspace, cx| {
2950 workspace
2951 .open_path(project_path, None, true, window, cx)
2952 .detach_and_log_err(cx);
2953 });
2954 }
2955 }
2956 AssistantContext::Directory(directory_context) => {
2957 let project_path = directory_context.project_path(cx);
2958 workspace.update(cx, |workspace, cx| {
2959 workspace.project().update(cx, |project, cx| {
2960 if let Some(entry) = project.entry_for_path(&project_path, cx) {
2961 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2962 }
2963 })
2964 })
2965 }
2966 AssistantContext::Symbol(symbol_context) => {
2967 if let Some(project_path) = symbol_context
2968 .context_symbol
2969 .buffer
2970 .read(cx)
2971 .project_path(cx)
2972 {
2973 let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
2974 let target_position = symbol_context
2975 .context_symbol
2976 .id
2977 .range
2978 .start
2979 .to_point(&snapshot);
2980
2981 let open_task = workspace.update(cx, |workspace, cx| {
2982 workspace.open_path(project_path, None, true, window, cx)
2983 });
2984 window
2985 .spawn(cx, async move |cx| {
2986 if let Some(active_editor) = open_task
2987 .await
2988 .log_err()
2989 .and_then(|item| item.downcast::<Editor>())
2990 {
2991 active_editor
2992 .downgrade()
2993 .update_in(cx, |editor, window, cx| {
2994 editor.go_to_singleton_buffer_point(
2995 target_position,
2996 window,
2997 cx,
2998 );
2999 })
3000 .log_err();
3001 }
3002 })
3003 .detach();
3004 }
3005 }
3006 AssistantContext::FetchedUrl(fetched_url_context) => {
3007 cx.open_url(&fetched_url_context.url);
3008 }
3009 AssistantContext::Thread(thread_context) => {
3010 let thread_id = thread_context.thread.read(cx).id().clone();
3011 workspace.update(cx, |workspace, cx| {
3012 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
3013 panel.update(cx, |panel, cx| {
3014 panel
3015 .open_thread(&thread_id, window, cx)
3016 .detach_and_log_err(cx)
3017 });
3018 }
3019 })
3020 }
3021 }
3022}