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