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