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