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