1use crate::AssistantPanel;
2use crate::context::{AssistantContext, ContextId};
3use crate::context_picker::MentionLink;
4use crate::thread::{
5 LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
6 ThreadEvent, ThreadFeedback,
7};
8use crate::thread_store::ThreadStore;
9use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
10use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
11use anyhow::Context as _;
12use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
13use collections::HashMap;
14use editor::scroll::Autoscroll;
15use editor::{Editor, MultiBuffer};
16use gpui::{
17 AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength,
18 EdgesRefinement, Empty, Entity, Focusable, Hsla, Length, ListAlignment, ListState, MouseButton,
19 PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
20 TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
21 linear_color_stop, linear_gradient, list, percentage, pulsating_between,
22};
23use language::{Buffer, LanguageRegistry};
24use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
25use markdown::{Markdown, MarkdownStyle};
26use project::ProjectItem as _;
27use settings::{Settings as _, update_settings_file};
28use std::rc::Rc;
29use std::sync::Arc;
30use std::time::Duration;
31use text::ToPoint;
32use theme::ThemeSettings;
33use ui::{Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*};
34use util::ResultExt as _;
35use workspace::{OpenOptions, Workspace};
36
37use crate::context_store::{ContextStore, refresh_context_store_text};
38
39pub struct ActiveThread {
40 language_registry: Arc<LanguageRegistry>,
41 thread_store: Entity<ThreadStore>,
42 thread: Entity<Thread>,
43 context_store: Entity<ContextStore>,
44 workspace: WeakEntity<Workspace>,
45 save_thread_task: Option<Task<()>>,
46 messages: Vec<MessageId>,
47 list_state: ListState,
48 scrollbar_state: ScrollbarState,
49 rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
50 rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
51 editing_message: Option<(MessageId, EditMessageState)>,
52 expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
53 expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
54 last_error: Option<ThreadError>,
55 notifications: Vec<WindowHandle<AgentNotification>>,
56 _subscriptions: Vec<Subscription>,
57 notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
58 showing_feedback_comments: bool,
59 feedback_comments_editor: Option<Entity<Editor>>,
60}
61
62struct RenderedMessage {
63 language_registry: Arc<LanguageRegistry>,
64 segments: Vec<RenderedMessageSegment>,
65}
66
67impl RenderedMessage {
68 fn from_segments(
69 segments: &[MessageSegment],
70 language_registry: Arc<LanguageRegistry>,
71 workspace: WeakEntity<Workspace>,
72 window: &Window,
73 cx: &mut App,
74 ) -> Self {
75 let mut this = Self {
76 language_registry,
77 segments: Vec::with_capacity(segments.len()),
78 };
79 for segment in segments {
80 this.push_segment(segment, workspace.clone(), window, cx);
81 }
82 this
83 }
84
85 fn append_thinking(
86 &mut self,
87 text: &String,
88 workspace: WeakEntity<Workspace>,
89 window: &Window,
90 cx: &mut App,
91 ) {
92 if let Some(RenderedMessageSegment::Thinking {
93 content,
94 scroll_handle,
95 }) = self.segments.last_mut()
96 {
97 content.update(cx, |markdown, cx| {
98 markdown.append(text, cx);
99 });
100 scroll_handle.scroll_to_bottom();
101 } else {
102 self.segments.push(RenderedMessageSegment::Thinking {
103 content: render_markdown(
104 text.into(),
105 self.language_registry.clone(),
106 workspace,
107 window,
108 cx,
109 ),
110 scroll_handle: ScrollHandle::default(),
111 });
112 }
113 }
114
115 fn append_text(
116 &mut self,
117 text: &String,
118 workspace: WeakEntity<Workspace>,
119 window: &Window,
120 cx: &mut App,
121 ) {
122 if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {
123 markdown.update(cx, |markdown, cx| markdown.append(text, cx));
124 } else {
125 self.segments
126 .push(RenderedMessageSegment::Text(render_markdown(
127 SharedString::from(text),
128 self.language_registry.clone(),
129 workspace,
130 window,
131 cx,
132 )));
133 }
134 }
135
136 fn push_segment(
137 &mut self,
138 segment: &MessageSegment,
139 workspace: WeakEntity<Workspace>,
140 window: &Window,
141 cx: &mut App,
142 ) {
143 let rendered_segment = match segment {
144 MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
145 content: render_markdown(
146 text.into(),
147 self.language_registry.clone(),
148 workspace,
149 window,
150 cx,
151 ),
152 scroll_handle: ScrollHandle::default(),
153 },
154 MessageSegment::Text(text) => RenderedMessageSegment::Text(render_markdown(
155 text.into(),
156 self.language_registry.clone(),
157 workspace,
158 window,
159 cx,
160 )),
161 };
162 self.segments.push(rendered_segment);
163 }
164}
165
166enum RenderedMessageSegment {
167 Thinking {
168 content: Entity<Markdown>,
169 scroll_handle: ScrollHandle,
170 },
171 Text(Entity<Markdown>),
172}
173
174fn render_markdown(
175 text: SharedString,
176 language_registry: Arc<LanguageRegistry>,
177 workspace: WeakEntity<Workspace>,
178 window: &Window,
179 cx: &mut App,
180) -> Entity<Markdown> {
181 let theme_settings = ThemeSettings::get_global(cx);
182 let colors = cx.theme().colors();
183 let ui_font_size = TextSize::Default.rems(cx);
184 let buffer_font_size = TextSize::Small.rems(cx);
185 let mut text_style = window.text_style();
186
187 text_style.refine(&TextStyleRefinement {
188 font_family: Some(theme_settings.ui_font.family.clone()),
189 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
190 font_features: Some(theme_settings.ui_font.features.clone()),
191 font_size: Some(ui_font_size.into()),
192 color: Some(cx.theme().colors().text),
193 ..Default::default()
194 });
195
196 let markdown_style = MarkdownStyle {
197 base_text_style: text_style,
198 syntax: cx.theme().syntax().clone(),
199 selection_background_color: cx.theme().players().local().selection,
200 code_block_overflow_x_scroll: true,
201 table_overflow_x_scroll: true,
202 code_block: StyleRefinement {
203 margin: EdgesRefinement {
204 top: Some(Length::Definite(rems(0.).into())),
205 left: Some(Length::Definite(rems(0.).into())),
206 right: Some(Length::Definite(rems(0.).into())),
207 bottom: Some(Length::Definite(rems(0.5).into())),
208 },
209 padding: EdgesRefinement {
210 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
211 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
212 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
213 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
214 },
215 background: Some(colors.editor_background.into()),
216 border_color: Some(colors.border_variant),
217 border_widths: EdgesRefinement {
218 top: Some(AbsoluteLength::Pixels(Pixels(1.))),
219 left: Some(AbsoluteLength::Pixels(Pixels(1.))),
220 right: Some(AbsoluteLength::Pixels(Pixels(1.))),
221 bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
222 },
223 text: Some(TextStyleRefinement {
224 font_family: Some(theme_settings.buffer_font.family.clone()),
225 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
226 font_features: Some(theme_settings.buffer_font.features.clone()),
227 font_size: Some(buffer_font_size.into()),
228 ..Default::default()
229 }),
230 ..Default::default()
231 },
232 inline_code: TextStyleRefinement {
233 font_family: Some(theme_settings.buffer_font.family.clone()),
234 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
235 font_features: Some(theme_settings.buffer_font.features.clone()),
236 font_size: Some(buffer_font_size.into()),
237 background_color: Some(colors.editor_foreground.opacity(0.1)),
238 ..Default::default()
239 },
240 link: TextStyleRefinement {
241 background_color: Some(colors.editor_foreground.opacity(0.025)),
242 underline: Some(UnderlineStyle {
243 color: Some(colors.text_accent.opacity(0.5)),
244 thickness: px(1.),
245 ..Default::default()
246 }),
247 ..Default::default()
248 },
249 ..Default::default()
250 };
251
252 cx.new(|cx| {
253 Markdown::new(text, markdown_style, Some(language_registry), None, cx).open_url(
254 move |text, window, cx| {
255 open_markdown_link(text, workspace.clone(), window, cx);
256 },
257 )
258 })
259}
260
261fn open_markdown_link(
262 text: SharedString,
263 workspace: WeakEntity<Workspace>,
264 window: &mut Window,
265 cx: &mut App,
266) {
267 let Some(workspace) = workspace.upgrade() else {
268 cx.open_url(&text);
269 return;
270 };
271
272 match MentionLink::try_parse(&text, &workspace, cx) {
273 Some(MentionLink::File(path, entry)) => workspace.update(cx, |workspace, cx| {
274 if entry.is_dir() {
275 workspace.project().update(cx, |_, cx| {
276 cx.emit(project::Event::RevealInProjectPanel(entry.id));
277 })
278 } else {
279 workspace
280 .open_path(path, None, true, window, cx)
281 .detach_and_log_err(cx);
282 }
283 }),
284 Some(MentionLink::Symbol(path, symbol_name)) => {
285 let open_task = workspace.update(cx, |workspace, cx| {
286 workspace.open_path(path, None, true, window, cx)
287 });
288 window
289 .spawn(cx, async move |cx| {
290 let active_editor = open_task
291 .await?
292 .downcast::<Editor>()
293 .context("Item is not an editor")?;
294 active_editor.update_in(cx, |editor, window, cx| {
295 let symbol_range = editor
296 .buffer()
297 .read(cx)
298 .snapshot(cx)
299 .outline(None)
300 .and_then(|outline| {
301 outline
302 .find_most_similar(&symbol_name)
303 .map(|(_, item)| item.range.clone())
304 })
305 .context("Could not find matching symbol")?;
306
307 editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
308 s.select_anchor_ranges([symbol_range.start..symbol_range.start])
309 });
310 anyhow::Ok(())
311 })
312 })
313 .detach_and_log_err(cx);
314 }
315 Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
316 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
317 panel.update(cx, |panel, cx| {
318 panel
319 .open_thread(&thread_id, window, cx)
320 .detach_and_log_err(cx)
321 });
322 }
323 }),
324 None => cx.open_url(&text),
325 }
326}
327
328struct EditMessageState {
329 editor: Entity<Editor>,
330}
331
332impl ActiveThread {
333 pub fn new(
334 thread: Entity<Thread>,
335 thread_store: Entity<ThreadStore>,
336 language_registry: Arc<LanguageRegistry>,
337 context_store: Entity<ContextStore>,
338 workspace: WeakEntity<Workspace>,
339 window: &mut Window,
340 cx: &mut Context<Self>,
341 ) -> Self {
342 let subscriptions = vec![
343 cx.observe(&thread, |_, _, cx| cx.notify()),
344 cx.subscribe_in(&thread, window, Self::handle_thread_event),
345 ];
346
347 let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
348 let this = cx.entity().downgrade();
349 move |ix, window: &mut Window, cx: &mut App| {
350 this.update(cx, |this, cx| this.render_message(ix, window, cx))
351 .unwrap()
352 }
353 });
354
355 let mut this = Self {
356 language_registry,
357 thread_store,
358 thread: thread.clone(),
359 context_store,
360 workspace,
361 save_thread_task: None,
362 messages: Vec::new(),
363 rendered_messages_by_id: HashMap::default(),
364 rendered_tool_use_labels: HashMap::default(),
365 expanded_tool_uses: HashMap::default(),
366 expanded_thinking_segments: HashMap::default(),
367 list_state: list_state.clone(),
368 scrollbar_state: ScrollbarState::new(list_state),
369 editing_message: None,
370 last_error: None,
371 notifications: Vec::new(),
372 _subscriptions: subscriptions,
373 notification_subscriptions: HashMap::default(),
374 showing_feedback_comments: false,
375 feedback_comments_editor: None,
376 };
377
378 for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
379 this.push_message(&message.id, &message.segments, window, cx);
380
381 for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
382 this.render_tool_use_label_markdown(
383 tool_use.id.clone(),
384 tool_use.ui_text.clone(),
385 window,
386 cx,
387 );
388 }
389 }
390
391 this
392 }
393
394 pub fn thread(&self) -> &Entity<Thread> {
395 &self.thread
396 }
397
398 pub fn is_empty(&self) -> bool {
399 self.messages.is_empty()
400 }
401
402 pub fn summary(&self, cx: &App) -> Option<SharedString> {
403 self.thread.read(cx).summary()
404 }
405
406 pub fn summary_or_default(&self, cx: &App) -> SharedString {
407 self.thread.read(cx).summary_or_default()
408 }
409
410 pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
411 self.last_error.take();
412 self.thread
413 .update(cx, |thread, cx| thread.cancel_last_completion(cx))
414 }
415
416 pub fn last_error(&self) -> Option<ThreadError> {
417 self.last_error.clone()
418 }
419
420 pub fn clear_last_error(&mut self) {
421 self.last_error.take();
422 }
423
424 fn push_message(
425 &mut self,
426 id: &MessageId,
427 segments: &[MessageSegment],
428 window: &mut Window,
429 cx: &mut Context<Self>,
430 ) {
431 let old_len = self.messages.len();
432 self.messages.push(*id);
433 self.list_state.splice(old_len..old_len, 1);
434
435 let rendered_message = RenderedMessage::from_segments(
436 segments,
437 self.language_registry.clone(),
438 self.workspace.clone(),
439 window,
440 cx,
441 );
442 self.rendered_messages_by_id.insert(*id, rendered_message);
443 }
444
445 fn edited_message(
446 &mut self,
447 id: &MessageId,
448 segments: &[MessageSegment],
449 window: &mut Window,
450 cx: &mut Context<Self>,
451 ) {
452 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
453 return;
454 };
455 self.list_state.splice(index..index + 1, 1);
456 let rendered_message = RenderedMessage::from_segments(
457 segments,
458 self.language_registry.clone(),
459 self.workspace.clone(),
460 window,
461 cx,
462 );
463 self.rendered_messages_by_id.insert(*id, rendered_message);
464 }
465
466 fn deleted_message(&mut self, id: &MessageId) {
467 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
468 return;
469 };
470 self.messages.remove(index);
471 self.list_state.splice(index..index + 1, 0);
472 self.rendered_messages_by_id.remove(id);
473 }
474
475 fn render_tool_use_label_markdown(
476 &mut self,
477 tool_use_id: LanguageModelToolUseId,
478 tool_label: impl Into<SharedString>,
479 window: &mut Window,
480 cx: &mut Context<Self>,
481 ) {
482 self.rendered_tool_use_labels.insert(
483 tool_use_id,
484 render_markdown(
485 tool_label.into(),
486 self.language_registry.clone(),
487 self.workspace.clone(),
488 window,
489 cx,
490 ),
491 );
492 }
493
494 fn handle_thread_event(
495 &mut self,
496 _thread: &Entity<Thread>,
497 event: &ThreadEvent,
498 window: &mut Window,
499 cx: &mut Context<Self>,
500 ) {
501 match event {
502 ThreadEvent::ShowError(error) => {
503 self.last_error = Some(error.clone());
504 }
505 ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
506 self.save_thread(cx);
507 }
508 ThreadEvent::DoneStreaming => {
509 let thread = self.thread.read(cx);
510
511 if !thread.is_generating() {
512 self.show_notification(
513 if thread.used_tools_since_last_user_message() {
514 "Finished running tools"
515 } else {
516 "New message"
517 },
518 IconName::ZedAssistant,
519 window,
520 cx,
521 );
522 }
523 }
524 ThreadEvent::ToolConfirmationNeeded => {
525 self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx);
526 }
527 ThreadEvent::StreamedAssistantText(message_id, text) => {
528 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
529 rendered_message.append_text(text, self.workspace.clone(), window, cx);
530 }
531 }
532 ThreadEvent::StreamedAssistantThinking(message_id, text) => {
533 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
534 rendered_message.append_thinking(text, self.workspace.clone(), window, cx);
535 }
536 }
537 ThreadEvent::MessageAdded(message_id) => {
538 if let Some(message_segments) = self
539 .thread
540 .read(cx)
541 .message(*message_id)
542 .map(|message| message.segments.clone())
543 {
544 self.push_message(message_id, &message_segments, window, cx);
545 }
546
547 self.save_thread(cx);
548 cx.notify();
549 }
550 ThreadEvent::MessageEdited(message_id) => {
551 if let Some(message_segments) = self
552 .thread
553 .read(cx)
554 .message(*message_id)
555 .map(|message| message.segments.clone())
556 {
557 self.edited_message(message_id, &message_segments, window, cx);
558 }
559
560 self.save_thread(cx);
561 cx.notify();
562 }
563 ThreadEvent::MessageDeleted(message_id) => {
564 self.deleted_message(message_id);
565 self.save_thread(cx);
566 cx.notify();
567 }
568 ThreadEvent::UsePendingTools => {
569 let tool_uses = self
570 .thread
571 .update(cx, |thread, cx| thread.use_pending_tools(cx));
572
573 for tool_use in tool_uses {
574 self.render_tool_use_label_markdown(
575 tool_use.id.clone(),
576 tool_use.ui_text.clone(),
577 window,
578 cx,
579 );
580 }
581 }
582 ThreadEvent::ToolFinished {
583 pending_tool_use,
584 canceled,
585 ..
586 } => {
587 let canceled = *canceled;
588 if let Some(tool_use) = pending_tool_use {
589 self.render_tool_use_label_markdown(
590 tool_use.id.clone(),
591 SharedString::from(tool_use.ui_text.clone()),
592 window,
593 cx,
594 );
595 }
596
597 if self.thread.read(cx).all_tools_finished() {
598 let pending_refresh_buffers = self.thread.update(cx, |thread, cx| {
599 thread.action_log().update(cx, |action_log, _cx| {
600 action_log.take_stale_buffers_in_context()
601 })
602 });
603
604 let context_update_task = if !pending_refresh_buffers.is_empty() {
605 let refresh_task = refresh_context_store_text(
606 self.context_store.clone(),
607 &pending_refresh_buffers,
608 cx,
609 );
610
611 cx.spawn(async move |this, cx| {
612 let updated_context_ids = refresh_task.await;
613
614 this.update(cx, |this, cx| {
615 this.context_store.read_with(cx, |context_store, _cx| {
616 context_store
617 .context()
618 .iter()
619 .filter(|context| {
620 updated_context_ids.contains(&context.id())
621 })
622 .cloned()
623 .collect()
624 })
625 })
626 })
627 } else {
628 Task::ready(anyhow::Ok(Vec::new()))
629 };
630
631 let model_registry = LanguageModelRegistry::read_global(cx);
632 if let Some(model) = model_registry.active_model() {
633 cx.spawn(async move |this, cx| {
634 let updated_context = context_update_task.await?;
635
636 this.update(cx, |this, cx| {
637 this.thread.update(cx, |thread, cx| {
638 thread.attach_tool_results(updated_context, cx);
639 if !canceled {
640 thread.send_to_model(model, RequestKind::Chat, cx);
641 }
642 });
643 })
644 })
645 .detach();
646 }
647 }
648 }
649 ThreadEvent::CheckpointChanged => cx.notify(),
650 }
651 }
652
653 fn show_notification(
654 &mut self,
655 caption: impl Into<SharedString>,
656 icon: IconName,
657 window: &mut Window,
658 cx: &mut Context<ActiveThread>,
659 ) {
660 if window.is_window_active() || !self.notifications.is_empty() {
661 return;
662 }
663
664 let title = self
665 .thread
666 .read(cx)
667 .summary()
668 .unwrap_or("Agent Panel".into());
669
670 match AssistantSettings::get_global(cx).notify_when_agent_waiting {
671 NotifyWhenAgentWaiting::PrimaryScreen => {
672 if let Some(primary) = cx.primary_display() {
673 self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
674 }
675 }
676 NotifyWhenAgentWaiting::AllScreens => {
677 let caption = caption.into();
678 for screen in cx.displays() {
679 self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
680 }
681 }
682 NotifyWhenAgentWaiting::Never => {
683 // Don't show anything
684 }
685 }
686 }
687
688 fn pop_up(
689 &mut self,
690 icon: IconName,
691 caption: SharedString,
692 title: SharedString,
693 window: &mut Window,
694 screen: Rc<dyn PlatformDisplay>,
695 cx: &mut Context<'_, ActiveThread>,
696 ) {
697 let options = AgentNotification::window_options(screen, cx);
698
699 if let Some(screen_window) = cx
700 .open_window(options, |_, cx| {
701 cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
702 })
703 .log_err()
704 {
705 if let Some(pop_up) = screen_window.entity(cx).log_err() {
706 self.notification_subscriptions
707 .entry(screen_window)
708 .or_insert_with(Vec::new)
709 .push(cx.subscribe_in(&pop_up, window, {
710 |this, _, event, window, cx| match event {
711 AgentNotificationEvent::Accepted => {
712 let handle = window.window_handle();
713 cx.activate(true); // Switch back to the Zed application
714
715 let workspace_handle = this.workspace.clone();
716
717 // If there are multiple Zed windows, activate the correct one.
718 cx.defer(move |cx| {
719 handle
720 .update(cx, |_view, window, _cx| {
721 window.activate_window();
722
723 if let Some(workspace) = workspace_handle.upgrade() {
724 workspace.update(_cx, |workspace, cx| {
725 workspace
726 .focus_panel::<AssistantPanel>(window, cx);
727 });
728 }
729 })
730 .log_err();
731 });
732
733 this.dismiss_notifications(cx);
734 }
735 AgentNotificationEvent::Dismissed => {
736 this.dismiss_notifications(cx);
737 }
738 }
739 }));
740
741 self.notifications.push(screen_window);
742
743 // If the user manually refocuses the original window, dismiss the popup.
744 self.notification_subscriptions
745 .entry(screen_window)
746 .or_insert_with(Vec::new)
747 .push({
748 let pop_up_weak = pop_up.downgrade();
749
750 cx.observe_window_activation(window, move |_, window, cx| {
751 if window.is_window_active() {
752 if let Some(pop_up) = pop_up_weak.upgrade() {
753 pop_up.update(cx, |_, cx| {
754 cx.emit(AgentNotificationEvent::Dismissed);
755 });
756 }
757 }
758 })
759 });
760 }
761 }
762 }
763
764 /// Spawns a task to save the active thread.
765 ///
766 /// Only one task to save the thread will be in flight at a time.
767 fn save_thread(&mut self, cx: &mut Context<Self>) {
768 let thread = self.thread.clone();
769 self.save_thread_task = Some(cx.spawn(async move |this, cx| {
770 let task = this
771 .update(cx, |this, cx| {
772 this.thread_store
773 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
774 })
775 .ok();
776
777 if let Some(task) = task {
778 task.await.log_err();
779 }
780 }));
781 }
782
783 fn start_editing_message(
784 &mut self,
785 message_id: MessageId,
786 message_segments: &[MessageSegment],
787 window: &mut Window,
788 cx: &mut Context<Self>,
789 ) {
790 // User message should always consist of a single text segment,
791 // therefore we can skip returning early if it's not a text segment.
792 let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
793 return;
794 };
795
796 let buffer = cx.new(|cx| {
797 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
798 });
799 let editor = cx.new(|cx| {
800 let mut editor = Editor::new(
801 editor::EditorMode::AutoHeight { max_lines: 8 },
802 buffer,
803 None,
804 window,
805 cx,
806 );
807 editor.focus_handle(cx).focus(window);
808 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
809 editor
810 });
811 self.editing_message = Some((
812 message_id,
813 EditMessageState {
814 editor: editor.clone(),
815 },
816 ));
817 cx.notify();
818 }
819
820 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
821 self.editing_message.take();
822 cx.notify();
823 }
824
825 fn confirm_editing_message(
826 &mut self,
827 _: &menu::Confirm,
828 _: &mut Window,
829 cx: &mut Context<Self>,
830 ) {
831 let Some((message_id, state)) = self.editing_message.take() else {
832 return;
833 };
834 let edited_text = state.editor.read(cx).text(cx);
835 self.thread.update(cx, |thread, cx| {
836 thread.edit_message(
837 message_id,
838 Role::User,
839 vec![MessageSegment::Text(edited_text)],
840 cx,
841 );
842 for message_id in self.messages_after(message_id) {
843 thread.delete_message(*message_id, cx);
844 }
845 });
846
847 let provider = LanguageModelRegistry::read_global(cx).active_provider();
848 if provider
849 .as_ref()
850 .map_or(false, |provider| provider.must_accept_terms(cx))
851 {
852 cx.notify();
853 return;
854 }
855 let model_registry = LanguageModelRegistry::read_global(cx);
856 let Some(model) = model_registry.active_model() else {
857 return;
858 };
859
860 self.thread.update(cx, |thread, cx| {
861 thread.send_to_model(model, RequestKind::Chat, cx)
862 });
863 cx.notify();
864 }
865
866 fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
867 self.messages
868 .iter()
869 .rev()
870 .find(|message_id| {
871 self.thread
872 .read(cx)
873 .message(**message_id)
874 .map_or(false, |message| message.role == Role::User)
875 })
876 .cloned()
877 }
878
879 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
880 self.messages
881 .iter()
882 .position(|id| *id == message_id)
883 .map(|index| &self.messages[index + 1..])
884 .unwrap_or(&[])
885 }
886
887 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
888 self.cancel_editing_message(&menu::Cancel, window, cx);
889 }
890
891 fn handle_regenerate_click(
892 &mut self,
893 _: &ClickEvent,
894 window: &mut Window,
895 cx: &mut Context<Self>,
896 ) {
897 self.confirm_editing_message(&menu::Confirm, window, cx);
898 }
899
900 fn handle_feedback_click(
901 &mut self,
902 feedback: ThreadFeedback,
903 window: &mut Window,
904 cx: &mut Context<Self>,
905 ) {
906 match feedback {
907 ThreadFeedback::Positive => {
908 let report = self
909 .thread
910 .update(cx, |thread, cx| thread.report_feedback(feedback, cx));
911
912 let this = cx.entity().downgrade();
913 cx.spawn(async move |_, cx| {
914 report.await?;
915 this.update(cx, |_this, cx| cx.notify())
916 })
917 .detach_and_log_err(cx);
918 }
919 ThreadFeedback::Negative => {
920 self.handle_show_feedback_comments(window, cx);
921 }
922 }
923 }
924
925 fn handle_show_feedback_comments(&mut self, window: &mut Window, cx: &mut Context<Self>) {
926 self.showing_feedback_comments = true;
927
928 if self.feedback_comments_editor.is_none() {
929 let buffer = cx.new(|cx| {
930 let empty_string = String::new();
931 MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
932 });
933
934 let editor = cx.new(|cx| {
935 Editor::new(
936 editor::EditorMode::AutoHeight { max_lines: 4 },
937 buffer,
938 None,
939 window,
940 cx,
941 )
942 });
943
944 self.feedback_comments_editor = Some(editor);
945 }
946
947 cx.notify();
948 }
949
950 fn handle_submit_comments(
951 &mut self,
952 _: &ClickEvent,
953 _window: &mut Window,
954 cx: &mut Context<Self>,
955 ) {
956 if let Some(editor) = self.feedback_comments_editor.clone() {
957 let comments = editor.read(cx).text(cx);
958
959 // Submit negative feedback
960 let report = self.thread.update(cx, |thread, cx| {
961 thread.report_feedback(ThreadFeedback::Negative, cx)
962 });
963
964 if !comments.is_empty() {
965 let thread_id = self.thread.read(cx).id().clone();
966 let comments_value = String::from(comments.as_str());
967
968 // Log comments as a separate telemetry event
969 telemetry::event!(
970 "Assistant Thread Feedback Comments",
971 thread_id,
972 comments = comments_value
973 );
974 }
975
976 self.showing_feedback_comments = false;
977 self.feedback_comments_editor = None;
978
979 let this = cx.entity().downgrade();
980 cx.spawn(async move |_, cx| {
981 report.await?;
982 this.update(cx, |_this, cx| cx.notify())
983 })
984 .detach_and_log_err(cx);
985 }
986 }
987
988 fn handle_cancel_comments(
989 &mut self,
990 _: &ClickEvent,
991 _window: &mut Window,
992 cx: &mut Context<Self>,
993 ) {
994 self.showing_feedback_comments = false;
995 self.feedback_comments_editor = None;
996 cx.notify();
997 }
998
999 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1000 let message_id = self.messages[ix];
1001 let Some(message) = self.thread.read(cx).message(message_id) else {
1002 return Empty.into_any();
1003 };
1004
1005 let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
1006 return Empty.into_any();
1007 };
1008
1009 let context_store = self.context_store.clone();
1010 let workspace = self.workspace.clone();
1011
1012 let thread = self.thread.read(cx);
1013 // Get all the data we need from thread before we start using it in closures
1014 let checkpoint = thread.checkpoint_for_message(message_id);
1015 let context = thread.context_for_message(message_id).collect::<Vec<_>>();
1016 let tool_uses = thread.tool_uses_for_message(message_id, cx);
1017 let has_tool_uses = !tool_uses.is_empty();
1018
1019 // Don't render user messages that are just there for returning tool results.
1020 if message.role == Role::User && thread.message_has_tool_results(message_id) {
1021 return Empty.into_any();
1022 }
1023
1024 let allow_editing_message =
1025 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
1026
1027 let edit_message_editor = self
1028 .editing_message
1029 .as_ref()
1030 .filter(|(id, _)| *id == message_id)
1031 .map(|(_, state)| state.editor.clone());
1032
1033 let first_message = ix == 0;
1034 let show_feedback = ix == self.messages.len() - 1 && message.role != Role::User;
1035
1036 let colors = cx.theme().colors();
1037 let active_color = colors.element_active;
1038 let editor_bg_color = colors.editor_background;
1039 let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
1040
1041 let feedback_container = h_flex().pt_2().pb_4().px_4().gap_1().justify_between();
1042 let feedback_items = match self.thread.read(cx).feedback() {
1043 Some(feedback) => feedback_container
1044 .child(
1045 Label::new(match feedback {
1046 ThreadFeedback::Positive => "Thanks for your feedback!",
1047 ThreadFeedback::Negative => {
1048 "We appreciate your feedback and will use it to improve."
1049 }
1050 })
1051 .color(Color::Muted)
1052 .size(LabelSize::XSmall),
1053 )
1054 .child(
1055 h_flex()
1056 .gap_1()
1057 .child(
1058 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
1059 .icon_size(IconSize::XSmall)
1060 .icon_color(match feedback {
1061 ThreadFeedback::Positive => Color::Accent,
1062 ThreadFeedback::Negative => Color::Ignored,
1063 })
1064 .shape(ui::IconButtonShape::Square)
1065 .tooltip(Tooltip::text("Helpful Response"))
1066 .on_click(cx.listener(move |this, _, window, cx| {
1067 this.handle_feedback_click(
1068 ThreadFeedback::Positive,
1069 window,
1070 cx,
1071 );
1072 })),
1073 )
1074 .child(
1075 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
1076 .icon_size(IconSize::XSmall)
1077 .icon_color(match feedback {
1078 ThreadFeedback::Positive => Color::Ignored,
1079 ThreadFeedback::Negative => Color::Accent,
1080 })
1081 .shape(ui::IconButtonShape::Square)
1082 .tooltip(Tooltip::text("Not Helpful"))
1083 .on_click(cx.listener(move |this, _, window, cx| {
1084 this.handle_feedback_click(
1085 ThreadFeedback::Negative,
1086 window,
1087 cx,
1088 );
1089 })),
1090 ),
1091 )
1092 .into_any_element(),
1093 None => feedback_container
1094 .child(
1095 Label::new(
1096 "Rating the thread sends all of your current conversation to the Zed team.",
1097 )
1098 .color(Color::Muted)
1099 .size(LabelSize::XSmall),
1100 )
1101 .child(
1102 h_flex()
1103 .gap_1()
1104 .child(
1105 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
1106 .icon_size(IconSize::XSmall)
1107 .icon_color(Color::Ignored)
1108 .shape(ui::IconButtonShape::Square)
1109 .tooltip(Tooltip::text("Helpful Response"))
1110 .on_click(cx.listener(move |this, _, window, cx| {
1111 this.handle_feedback_click(
1112 ThreadFeedback::Positive,
1113 window,
1114 cx,
1115 );
1116 })),
1117 )
1118 .child(
1119 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
1120 .icon_size(IconSize::XSmall)
1121 .icon_color(Color::Ignored)
1122 .shape(ui::IconButtonShape::Square)
1123 .tooltip(Tooltip::text("Not Helpful"))
1124 .on_click(cx.listener(move |this, _, window, cx| {
1125 this.handle_feedback_click(
1126 ThreadFeedback::Negative,
1127 window,
1128 cx,
1129 );
1130 })),
1131 ),
1132 )
1133 .into_any_element(),
1134 };
1135
1136 let message_content = v_flex()
1137 .gap_1p5()
1138 .child(
1139 if let Some(edit_message_editor) = edit_message_editor.clone() {
1140 div()
1141 .key_context("EditMessageEditor")
1142 .on_action(cx.listener(Self::cancel_editing_message))
1143 .on_action(cx.listener(Self::confirm_editing_message))
1144 .min_h_6()
1145 .child(edit_message_editor)
1146 } else {
1147 div()
1148 .min_h_6()
1149 .text_ui(cx)
1150 .child(self.render_message_content(
1151 message_id,
1152 rendered_message,
1153 has_tool_uses,
1154 cx,
1155 ))
1156 },
1157 )
1158 .when(!context.is_empty(), |parent| {
1159 parent.child(
1160 h_flex()
1161 .flex_wrap()
1162 .gap_1()
1163 .children(context.into_iter().map(|context| {
1164 let context_id = context.id();
1165 ContextPill::added(AddedContext::new(context, cx), false, false, None)
1166 .on_click(Rc::new(cx.listener({
1167 let workspace = workspace.clone();
1168 let context_store = context_store.clone();
1169 move |_, _, window, cx| {
1170 if let Some(workspace) = workspace.upgrade() {
1171 open_context(
1172 context_id,
1173 context_store.clone(),
1174 workspace,
1175 window,
1176 cx,
1177 );
1178 cx.notify();
1179 }
1180 }
1181 })))
1182 })),
1183 )
1184 });
1185
1186 let styled_message = match message.role {
1187 Role::User => v_flex()
1188 .id(("message-container", ix))
1189 .map(|this| {
1190 if first_message {
1191 this.pt_2()
1192 } else {
1193 this.pt_4()
1194 }
1195 })
1196 .pb_4()
1197 .pl_2()
1198 .pr_2p5()
1199 .child(
1200 v_flex()
1201 .bg(colors.editor_background)
1202 .rounded_lg()
1203 .border_1()
1204 .border_color(colors.border)
1205 .shadow_md()
1206 .child(
1207 h_flex()
1208 .py_1()
1209 .pl_2()
1210 .pr_1()
1211 .bg(bg_user_message_header)
1212 .border_b_1()
1213 .border_color(colors.border)
1214 .justify_between()
1215 .rounded_t_md()
1216 .child(
1217 h_flex()
1218 .gap_1p5()
1219 .child(
1220 Icon::new(IconName::PersonCircle)
1221 .size(IconSize::XSmall)
1222 .color(Color::Muted),
1223 )
1224 .child(
1225 Label::new("You")
1226 .size(LabelSize::Small)
1227 .color(Color::Muted),
1228 ),
1229 )
1230 .child(
1231 h_flex()
1232 // DL: To double-check whether we want to fully remove
1233 // the editing feature from meassages. Checkpoint sort of
1234 // solve the same problem.
1235 .invisible()
1236 .gap_1()
1237 .when_some(
1238 edit_message_editor.clone(),
1239 |this, edit_message_editor| {
1240 let focus_handle =
1241 edit_message_editor.focus_handle(cx);
1242 this.child(
1243 Button::new("cancel-edit-message", "Cancel")
1244 .label_size(LabelSize::Small)
1245 .key_binding(
1246 KeyBinding::for_action_in(
1247 &menu::Cancel,
1248 &focus_handle,
1249 window,
1250 cx,
1251 )
1252 .map(|kb| kb.size(rems_from_px(12.))),
1253 )
1254 .on_click(
1255 cx.listener(Self::handle_cancel_click),
1256 ),
1257 )
1258 .child(
1259 Button::new(
1260 "confirm-edit-message",
1261 "Regenerate",
1262 )
1263 .label_size(LabelSize::Small)
1264 .key_binding(
1265 KeyBinding::for_action_in(
1266 &menu::Confirm,
1267 &focus_handle,
1268 window,
1269 cx,
1270 )
1271 .map(|kb| kb.size(rems_from_px(12.))),
1272 )
1273 .on_click(
1274 cx.listener(Self::handle_regenerate_click),
1275 ),
1276 )
1277 },
1278 )
1279 .when(
1280 edit_message_editor.is_none() && allow_editing_message,
1281 |this| {
1282 this.child(
1283 Button::new("edit-message", "Edit")
1284 .label_size(LabelSize::Small)
1285 .on_click(cx.listener({
1286 let message_segments =
1287 message.segments.clone();
1288 move |this, _, window, cx| {
1289 this.start_editing_message(
1290 message_id,
1291 &message_segments,
1292 window,
1293 cx,
1294 );
1295 }
1296 })),
1297 )
1298 },
1299 ),
1300 ),
1301 )
1302 .child(div().p_2().child(message_content)),
1303 ),
1304 Role::Assistant => v_flex()
1305 .id(("message-container", ix))
1306 .ml_2()
1307 .pl_2()
1308 .pr_4()
1309 .border_l_1()
1310 .border_color(cx.theme().colors().border_variant)
1311 .child(message_content)
1312 .when(!tool_uses.is_empty(), |parent| {
1313 parent.child(
1314 v_flex().children(
1315 tool_uses
1316 .into_iter()
1317 .map(|tool_use| self.render_tool_use(tool_use, cx)),
1318 ),
1319 )
1320 }),
1321 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1322 v_flex()
1323 .bg(colors.editor_background)
1324 .rounded_sm()
1325 .child(div().p_4().child(message_content)),
1326 ),
1327 };
1328
1329 v_flex()
1330 .w_full()
1331 .when(first_message, |parent| {
1332 parent.child(self.render_rules_item(cx))
1333 })
1334 .when_some(checkpoint, |parent, checkpoint| {
1335 let mut is_pending = false;
1336 let mut error = None;
1337 if let Some(last_restore_checkpoint) =
1338 self.thread.read(cx).last_restore_checkpoint()
1339 {
1340 if last_restore_checkpoint.message_id() == message_id {
1341 match last_restore_checkpoint {
1342 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1343 LastRestoreCheckpoint::Error { error: err, .. } => {
1344 error = Some(err.clone());
1345 }
1346 }
1347 }
1348 }
1349
1350 let restore_checkpoint_button =
1351 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1352 .icon(if error.is_some() {
1353 IconName::XCircle
1354 } else {
1355 IconName::Undo
1356 })
1357 .icon_size(IconSize::XSmall)
1358 .icon_position(IconPosition::Start)
1359 .icon_color(if error.is_some() {
1360 Some(Color::Error)
1361 } else {
1362 None
1363 })
1364 .label_size(LabelSize::XSmall)
1365 .disabled(is_pending)
1366 .on_click(cx.listener(move |this, _, _window, cx| {
1367 this.thread.update(cx, |thread, cx| {
1368 thread
1369 .restore_checkpoint(checkpoint.clone(), cx)
1370 .detach_and_log_err(cx);
1371 });
1372 }));
1373
1374 let restore_checkpoint_button = if is_pending {
1375 restore_checkpoint_button
1376 .with_animation(
1377 ("pulsating-restore-checkpoint-button", ix),
1378 Animation::new(Duration::from_secs(2))
1379 .repeat()
1380 .with_easing(pulsating_between(0.6, 1.)),
1381 |label, delta| label.alpha(delta),
1382 )
1383 .into_any_element()
1384 } else if let Some(error) = error {
1385 restore_checkpoint_button
1386 .tooltip(Tooltip::text(error.to_string()))
1387 .into_any_element()
1388 } else {
1389 restore_checkpoint_button.into_any_element()
1390 };
1391
1392 parent.child(
1393 h_flex()
1394 .pt_2p5()
1395 .px_2p5()
1396 .w_full()
1397 .gap_1()
1398 .child(ui::Divider::horizontal())
1399 .child(restore_checkpoint_button)
1400 .child(ui::Divider::horizontal()),
1401 )
1402 })
1403 .child(styled_message)
1404 .when(
1405 show_feedback && !self.thread.read(cx).is_generating(),
1406 |parent| {
1407 parent
1408 .child(feedback_items)
1409 .when(self.showing_feedback_comments, |parent| {
1410 parent.child(
1411 v_flex()
1412 .gap_1()
1413 .px_4()
1414 .child(
1415 Label::new(
1416 "Please share your feedback to help us improve:",
1417 )
1418 .size(LabelSize::Small),
1419 )
1420 .child(
1421 div()
1422 .p_2()
1423 .rounded_md()
1424 .border_1()
1425 .border_color(cx.theme().colors().border)
1426 .bg(cx.theme().colors().editor_background)
1427 .child(
1428 self.feedback_comments_editor
1429 .as_ref()
1430 .unwrap()
1431 .clone(),
1432 ),
1433 )
1434 .child(
1435 h_flex()
1436 .gap_1()
1437 .justify_end()
1438 .pb_2()
1439 .child(
1440 Button::new("cancel-comments", "Cancel").on_click(
1441 cx.listener(Self::handle_cancel_comments),
1442 ),
1443 )
1444 .child(
1445 Button::new("submit-comments", "Submit").on_click(
1446 cx.listener(Self::handle_submit_comments),
1447 ),
1448 ),
1449 ),
1450 )
1451 })
1452 },
1453 )
1454 .into_any()
1455 }
1456
1457 fn render_message_content(
1458 &self,
1459 message_id: MessageId,
1460 rendered_message: &RenderedMessage,
1461 has_tool_uses: bool,
1462 cx: &Context<Self>,
1463 ) -> impl IntoElement {
1464 let is_last_message = self.messages.last() == Some(&message_id);
1465 let pending_thinking_segment_index = if is_last_message && !has_tool_uses {
1466 rendered_message
1467 .segments
1468 .iter()
1469 .enumerate()
1470 .last()
1471 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1472 .map(|(index, _)| index)
1473 } else {
1474 None
1475 };
1476
1477 div()
1478 .text_ui(cx)
1479 .gap_2()
1480 .children(
1481 rendered_message.segments.iter().enumerate().map(
1482 |(index, segment)| match segment {
1483 RenderedMessageSegment::Thinking {
1484 content,
1485 scroll_handle,
1486 } => self
1487 .render_message_thinking_segment(
1488 message_id,
1489 index,
1490 content.clone(),
1491 &scroll_handle,
1492 Some(index) == pending_thinking_segment_index,
1493 cx,
1494 )
1495 .into_any_element(),
1496 RenderedMessageSegment::Text(markdown) => {
1497 div().child(markdown.clone()).into_any_element()
1498 }
1499 },
1500 ),
1501 )
1502 }
1503
1504 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1505 cx.theme().colors().border.opacity(0.5)
1506 }
1507
1508 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1509 cx.theme()
1510 .colors()
1511 .element_background
1512 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1513 }
1514
1515 fn render_message_thinking_segment(
1516 &self,
1517 message_id: MessageId,
1518 ix: usize,
1519 markdown: Entity<Markdown>,
1520 scroll_handle: &ScrollHandle,
1521 pending: bool,
1522 cx: &Context<Self>,
1523 ) -> impl IntoElement {
1524 let is_open = self
1525 .expanded_thinking_segments
1526 .get(&(message_id, ix))
1527 .copied()
1528 .unwrap_or_default();
1529
1530 let editor_bg = cx.theme().colors().editor_background;
1531
1532 div().pt_0p5().pb_2().child(
1533 v_flex()
1534 .rounded_lg()
1535 .border_1()
1536 .border_color(self.tool_card_border_color(cx))
1537 .child(
1538 h_flex()
1539 .group("disclosure-header")
1540 .justify_between()
1541 .py_1()
1542 .px_2()
1543 .bg(self.tool_card_header_bg(cx))
1544 .map(|this| {
1545 if pending || is_open {
1546 this.rounded_t_md()
1547 .border_b_1()
1548 .border_color(self.tool_card_border_color(cx))
1549 } else {
1550 this.rounded_md()
1551 }
1552 })
1553 .child(
1554 h_flex()
1555 .gap_1p5()
1556 .child(
1557 Icon::new(IconName::Brain)
1558 .size(IconSize::XSmall)
1559 .color(Color::Muted),
1560 )
1561 .child({
1562 if pending {
1563 Label::new("Thinking…")
1564 .size(LabelSize::Small)
1565 .buffer_font(cx)
1566 .with_animation(
1567 "pulsating-label",
1568 Animation::new(Duration::from_secs(2))
1569 .repeat()
1570 .with_easing(pulsating_between(0.4, 0.8)),
1571 |label, delta| label.alpha(delta),
1572 )
1573 .into_any_element()
1574 } else {
1575 Label::new("Thought Process")
1576 .size(LabelSize::Small)
1577 .buffer_font(cx)
1578 .into_any_element()
1579 }
1580 }),
1581 )
1582 .child(
1583 h_flex()
1584 .gap_1()
1585 .child(
1586 div().visible_on_hover("disclosure-header").child(
1587 Disclosure::new("thinking-disclosure", is_open)
1588 .opened_icon(IconName::ChevronUp)
1589 .closed_icon(IconName::ChevronDown)
1590 .on_click(cx.listener({
1591 move |this, _event, _window, _cx| {
1592 let is_open = this
1593 .expanded_thinking_segments
1594 .entry((message_id, ix))
1595 .or_insert(false);
1596
1597 *is_open = !*is_open;
1598 }
1599 })),
1600 ),
1601 )
1602 .child({
1603 let (icon_name, color, animated) = if pending {
1604 (IconName::ArrowCircle, Color::Accent, true)
1605 } else {
1606 (IconName::Check, Color::Success, false)
1607 };
1608
1609 let icon =
1610 Icon::new(icon_name).color(color).size(IconSize::Small);
1611
1612 if animated {
1613 icon.with_animation(
1614 "arrow-circle",
1615 Animation::new(Duration::from_secs(2)).repeat(),
1616 |icon, delta| {
1617 icon.transform(Transformation::rotate(percentage(
1618 delta,
1619 )))
1620 },
1621 )
1622 .into_any_element()
1623 } else {
1624 icon.into_any_element()
1625 }
1626 }),
1627 ),
1628 )
1629 .when(pending && !is_open, |this| {
1630 let gradient_overlay = div()
1631 .rounded_b_lg()
1632 .h_20()
1633 .absolute()
1634 .w_full()
1635 .bottom_0()
1636 .left_0()
1637 .bg(linear_gradient(
1638 180.,
1639 linear_color_stop(editor_bg, 1.),
1640 linear_color_stop(editor_bg.opacity(0.2), 0.),
1641 ));
1642
1643 this.child(
1644 div()
1645 .relative()
1646 .bg(editor_bg)
1647 .rounded_b_lg()
1648 .child(
1649 div()
1650 .id(("thinking-content", ix))
1651 .p_2()
1652 .h_20()
1653 .track_scroll(scroll_handle)
1654 .text_ui_sm(cx)
1655 .child(markdown.clone())
1656 .overflow_hidden(),
1657 )
1658 .child(gradient_overlay),
1659 )
1660 })
1661 .when(is_open, |this| {
1662 this.child(
1663 div()
1664 .id(("thinking-content", ix))
1665 .h_full()
1666 .p_2()
1667 .rounded_b_lg()
1668 .bg(editor_bg)
1669 .text_ui_sm(cx)
1670 .child(markdown.clone()),
1671 )
1672 }),
1673 )
1674 }
1675
1676 fn render_tool_use(
1677 &self,
1678 tool_use: ToolUse,
1679 cx: &mut Context<Self>,
1680 ) -> impl IntoElement + use<> {
1681 let is_open = self
1682 .expanded_tool_uses
1683 .get(&tool_use.id)
1684 .copied()
1685 .unwrap_or_default();
1686
1687 let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
1688
1689 let fs = self
1690 .workspace
1691 .upgrade()
1692 .map(|workspace| workspace.read(cx).app_state().fs.clone());
1693 let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
1694
1695 let status_icons = div().child(match &tool_use.status {
1696 ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
1697 let icon = Icon::new(IconName::Warning)
1698 .color(Color::Warning)
1699 .size(IconSize::Small);
1700 icon.into_any_element()
1701 }
1702 ToolUseStatus::Running => {
1703 let icon = Icon::new(IconName::ArrowCircle)
1704 .color(Color::Accent)
1705 .size(IconSize::Small);
1706 icon.with_animation(
1707 "arrow-circle",
1708 Animation::new(Duration::from_secs(2)).repeat(),
1709 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1710 )
1711 .into_any_element()
1712 }
1713 ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
1714 ToolUseStatus::Error(_) => {
1715 let icon = Icon::new(IconName::Close)
1716 .color(Color::Error)
1717 .size(IconSize::Small);
1718 icon.into_any_element()
1719 }
1720 });
1721
1722 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1723 let results_content = v_flex()
1724 .gap_1()
1725 .child(
1726 content_container()
1727 .child(
1728 Label::new("Input")
1729 .size(LabelSize::XSmall)
1730 .color(Color::Muted)
1731 .buffer_font(cx),
1732 )
1733 .child(
1734 Label::new(
1735 serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1736 )
1737 .size(LabelSize::Small)
1738 .buffer_font(cx),
1739 ),
1740 )
1741 .map(|container| match tool_use.status {
1742 ToolUseStatus::Finished(output) => container.child(
1743 content_container()
1744 .border_t_1()
1745 .border_color(self.tool_card_border_color(cx))
1746 .child(
1747 Label::new("Result")
1748 .size(LabelSize::XSmall)
1749 .color(Color::Muted)
1750 .buffer_font(cx),
1751 )
1752 .child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
1753 ),
1754 ToolUseStatus::Running => container.child(
1755 content_container().child(
1756 h_flex()
1757 .gap_1()
1758 .pb_1()
1759 .border_t_1()
1760 .border_color(self.tool_card_border_color(cx))
1761 .child(
1762 Icon::new(IconName::ArrowCircle)
1763 .size(IconSize::Small)
1764 .color(Color::Accent)
1765 .with_animation(
1766 "arrow-circle",
1767 Animation::new(Duration::from_secs(2)).repeat(),
1768 |icon, delta| {
1769 icon.transform(Transformation::rotate(percentage(
1770 delta,
1771 )))
1772 },
1773 ),
1774 )
1775 .child(
1776 Label::new("Running…")
1777 .size(LabelSize::XSmall)
1778 .color(Color::Muted)
1779 .buffer_font(cx),
1780 ),
1781 ),
1782 ),
1783 ToolUseStatus::Error(err) => container.child(
1784 content_container()
1785 .border_t_1()
1786 .border_color(self.tool_card_border_color(cx))
1787 .child(
1788 Label::new("Error")
1789 .size(LabelSize::XSmall)
1790 .color(Color::Muted)
1791 .buffer_font(cx),
1792 )
1793 .child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
1794 ),
1795 ToolUseStatus::Pending => container,
1796 ToolUseStatus::NeedsConfirmation => container.child(
1797 content_container()
1798 .border_t_1()
1799 .border_color(self.tool_card_border_color(cx))
1800 .child(
1801 Label::new("Asking Permission")
1802 .size(LabelSize::Small)
1803 .color(Color::Muted)
1804 .buffer_font(cx),
1805 ),
1806 ),
1807 });
1808
1809 let gradient_overlay = |color: Hsla| {
1810 div()
1811 .h_full()
1812 .absolute()
1813 .w_8()
1814 .bottom_0()
1815 .map(|element| {
1816 if is_status_finished {
1817 element.right_7()
1818 } else {
1819 element.right(px(46.))
1820 }
1821 })
1822 .bg(linear_gradient(
1823 90.,
1824 linear_color_stop(color, 1.),
1825 linear_color_stop(color.opacity(0.2), 0.),
1826 ))
1827 };
1828
1829 div().map(|element| {
1830 if !tool_use.needs_confirmation {
1831 element.py_2p5().child(
1832 v_flex()
1833 .child(
1834 h_flex()
1835 .group("disclosure-header")
1836 .relative()
1837 .gap_1p5()
1838 .justify_between()
1839 .opacity(0.8)
1840 .hover(|style| style.opacity(1.))
1841 .when(!is_status_finished, |this| this.pr_2())
1842 .child(
1843 h_flex()
1844 .id("tool-label-container")
1845 .gap_1p5()
1846 .max_w_full()
1847 .overflow_x_scroll()
1848 .child(
1849 Icon::new(tool_use.icon)
1850 .size(IconSize::XSmall)
1851 .color(Color::Muted),
1852 )
1853 .child(
1854 h_flex().pr_8().text_ui_sm(cx).children(
1855 self.rendered_tool_use_labels
1856 .get(&tool_use.id)
1857 .cloned(),
1858 ),
1859 ),
1860 )
1861 .child(
1862 h_flex()
1863 .gap_1()
1864 .child(
1865 div().visible_on_hover("disclosure-header").child(
1866 Disclosure::new("tool-use-disclosure", is_open)
1867 .opened_icon(IconName::ChevronUp)
1868 .closed_icon(IconName::ChevronDown)
1869 .on_click(cx.listener({
1870 let tool_use_id = tool_use.id.clone();
1871 move |this, _event, _window, _cx| {
1872 let is_open = this
1873 .expanded_tool_uses
1874 .entry(tool_use_id.clone())
1875 .or_insert(false);
1876
1877 *is_open = !*is_open;
1878 }
1879 })),
1880 ),
1881 )
1882 .child(status_icons),
1883 )
1884 .child(gradient_overlay(cx.theme().colors().panel_background)),
1885 )
1886 .map(|parent| {
1887 if !is_open {
1888 return parent;
1889 }
1890
1891 parent.child(
1892 v_flex()
1893 .mt_1()
1894 .border_1()
1895 .border_color(self.tool_card_border_color(cx))
1896 .bg(cx.theme().colors().editor_background)
1897 .rounded_lg()
1898 .child(results_content),
1899 )
1900 }),
1901 )
1902 } else {
1903 element.py_2().child(
1904 v_flex()
1905 .rounded_lg()
1906 .border_1()
1907 .border_color(self.tool_card_border_color(cx))
1908 .overflow_hidden()
1909 .child(
1910 h_flex()
1911 .group("disclosure-header")
1912 .relative()
1913 .justify_between()
1914 .py_1()
1915 .map(|element| {
1916 if is_status_finished {
1917 element.pl_2().pr_0p5()
1918 } else {
1919 element.px_2()
1920 }
1921 })
1922 .bg(self.tool_card_header_bg(cx))
1923 .map(|element| {
1924 if is_open {
1925 element.border_b_1().rounded_t_md()
1926 } else if needs_confirmation {
1927 element.rounded_t_md()
1928 } else {
1929 element.rounded_md()
1930 }
1931 })
1932 .border_color(self.tool_card_border_color(cx))
1933 .child(
1934 h_flex()
1935 .id("tool-label-container")
1936 .gap_1p5()
1937 .max_w_full()
1938 .overflow_x_scroll()
1939 .child(
1940 Icon::new(tool_use.icon)
1941 .size(IconSize::XSmall)
1942 .color(Color::Muted),
1943 )
1944 .child(
1945 h_flex().pr_8().text_ui_sm(cx).children(
1946 self.rendered_tool_use_labels
1947 .get(&tool_use.id)
1948 .cloned(),
1949 ),
1950 ),
1951 )
1952 .child(
1953 h_flex()
1954 .gap_1()
1955 .child(
1956 div().visible_on_hover("disclosure-header").child(
1957 Disclosure::new("tool-use-disclosure", is_open)
1958 .opened_icon(IconName::ChevronUp)
1959 .closed_icon(IconName::ChevronDown)
1960 .on_click(cx.listener({
1961 let tool_use_id = tool_use.id.clone();
1962 move |this, _event, _window, _cx| {
1963 let is_open = this
1964 .expanded_tool_uses
1965 .entry(tool_use_id.clone())
1966 .or_insert(false);
1967
1968 *is_open = !*is_open;
1969 }
1970 })),
1971 ),
1972 )
1973 .child(status_icons),
1974 )
1975 .child(gradient_overlay(self.tool_card_header_bg(cx))),
1976 )
1977 .map(|parent| {
1978 if !is_open {
1979 return parent;
1980 }
1981
1982 parent.child(
1983 v_flex()
1984 .bg(cx.theme().colors().editor_background)
1985 .map(|element| {
1986 if needs_confirmation {
1987 element.rounded_none()
1988 } else {
1989 element.rounded_b_lg()
1990 }
1991 })
1992 .child(results_content),
1993 )
1994 })
1995 .when(needs_confirmation, |this| {
1996 this.child(
1997 h_flex()
1998 .py_1()
1999 .pl_2()
2000 .pr_1()
2001 .gap_1()
2002 .justify_between()
2003 .bg(cx.theme().colors().editor_background)
2004 .border_t_1()
2005 .border_color(self.tool_card_border_color(cx))
2006 .rounded_b_lg()
2007 .child(Label::new("Action Confirmation").color(Color::Muted).size(LabelSize::Small))
2008 .child(
2009 h_flex()
2010 .gap_0p5()
2011 .child({
2012 let tool_id = tool_use.id.clone();
2013 Button::new(
2014 "always-allow-tool-action",
2015 "Always Allow",
2016 )
2017 .label_size(LabelSize::Small)
2018 .icon(IconName::CheckDouble)
2019 .icon_position(IconPosition::Start)
2020 .icon_size(IconSize::Small)
2021 .icon_color(Color::Success)
2022 .tooltip(move |window, cx| {
2023 Tooltip::with_meta(
2024 "Never ask for permission",
2025 None,
2026 "Restore the original behavior in your Agent Panel settings",
2027 window,
2028 cx,
2029 )
2030 })
2031 .on_click(cx.listener(
2032 move |this, event, window, cx| {
2033 if let Some(fs) = fs.clone() {
2034 update_settings_file::<AssistantSettings>(
2035 fs.clone(),
2036 cx,
2037 |settings, _| {
2038 settings.set_always_allow_tool_actions(true);
2039 },
2040 );
2041 }
2042 this.handle_allow_tool(
2043 tool_id.clone(),
2044 event,
2045 window,
2046 cx,
2047 )
2048 },
2049 ))
2050 })
2051 .child(ui::Divider::vertical())
2052 .child({
2053 let tool_id = tool_use.id.clone();
2054 Button::new("allow-tool-action", "Allow")
2055 .label_size(LabelSize::Small)
2056 .icon(IconName::Check)
2057 .icon_position(IconPosition::Start)
2058 .icon_size(IconSize::Small)
2059 .icon_color(Color::Success)
2060 .on_click(cx.listener(
2061 move |this, event, window, cx| {
2062 this.handle_allow_tool(
2063 tool_id.clone(),
2064 event,
2065 window,
2066 cx,
2067 )
2068 },
2069 ))
2070 })
2071 .child({
2072 let tool_id = tool_use.id.clone();
2073 let tool_name: Arc<str> = tool_use.name.into();
2074 Button::new("deny-tool", "Deny")
2075 .label_size(LabelSize::Small)
2076 .icon(IconName::Close)
2077 .icon_position(IconPosition::Start)
2078 .icon_size(IconSize::Small)
2079 .icon_color(Color::Error)
2080 .on_click(cx.listener(
2081 move |this, event, window, cx| {
2082 this.handle_deny_tool(
2083 tool_id.clone(),
2084 tool_name.clone(),
2085 event,
2086 window,
2087 cx,
2088 )
2089 },
2090 ))
2091 }),
2092 ),
2093 )
2094 }),
2095 )
2096 }
2097 })
2098 }
2099
2100 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
2101 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2102 else {
2103 return div().into_any();
2104 };
2105
2106 let rules_files = system_prompt_context
2107 .worktrees
2108 .iter()
2109 .filter_map(|worktree| worktree.rules_file.as_ref())
2110 .collect::<Vec<_>>();
2111
2112 let label_text = match rules_files.as_slice() {
2113 &[] => return div().into_any(),
2114 &[rules_file] => {
2115 format!("Using {:?} file", rules_file.rel_path)
2116 }
2117 rules_files => {
2118 format!("Using {} rules files", rules_files.len())
2119 }
2120 };
2121
2122 div()
2123 .pt_1()
2124 .px_2p5()
2125 .child(
2126 h_flex()
2127 .w_full()
2128 .gap_0p5()
2129 .child(
2130 h_flex()
2131 .gap_1p5()
2132 .child(
2133 Icon::new(IconName::File)
2134 .size(IconSize::XSmall)
2135 .color(Color::Disabled),
2136 )
2137 .child(
2138 Label::new(label_text)
2139 .size(LabelSize::XSmall)
2140 .color(Color::Muted)
2141 .buffer_font(cx),
2142 ),
2143 )
2144 .child(
2145 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
2146 .shape(ui::IconButtonShape::Square)
2147 .icon_size(IconSize::XSmall)
2148 .icon_color(Color::Ignored)
2149 .on_click(cx.listener(Self::handle_open_rules))
2150 .tooltip(Tooltip::text("View Rules")),
2151 ),
2152 )
2153 .into_any()
2154 }
2155
2156 fn handle_allow_tool(
2157 &mut self,
2158 tool_use_id: LanguageModelToolUseId,
2159 _: &ClickEvent,
2160 _window: &mut Window,
2161 cx: &mut Context<Self>,
2162 ) {
2163 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
2164 .thread
2165 .read(cx)
2166 .pending_tool(&tool_use_id)
2167 .map(|tool_use| tool_use.status.clone())
2168 {
2169 self.thread.update(cx, |thread, cx| {
2170 thread.run_tool(
2171 c.tool_use_id.clone(),
2172 c.ui_text.clone(),
2173 c.input.clone(),
2174 &c.messages,
2175 c.tool.clone(),
2176 cx,
2177 );
2178 });
2179 }
2180 }
2181
2182 fn handle_deny_tool(
2183 &mut self,
2184 tool_use_id: LanguageModelToolUseId,
2185 tool_name: Arc<str>,
2186 _: &ClickEvent,
2187 _window: &mut Window,
2188 cx: &mut Context<Self>,
2189 ) {
2190 self.thread.update(cx, |thread, cx| {
2191 thread.deny_tool_use(tool_use_id, tool_name, cx);
2192 });
2193 }
2194
2195 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
2196 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2197 else {
2198 return;
2199 };
2200
2201 let abs_paths = system_prompt_context
2202 .worktrees
2203 .iter()
2204 .flat_map(|worktree| worktree.rules_file.as_ref())
2205 .map(|rules_file| rules_file.abs_path.to_path_buf())
2206 .collect::<Vec<_>>();
2207
2208 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
2209 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
2210 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
2211 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
2212 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
2213 }) {
2214 task.detach();
2215 }
2216 }
2217
2218 fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
2219 for window in self.notifications.drain(..) {
2220 window
2221 .update(cx, |_, window, _| {
2222 window.remove_window();
2223 })
2224 .ok();
2225
2226 self.notification_subscriptions.remove(&window);
2227 }
2228 }
2229
2230 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
2231 div()
2232 .occlude()
2233 .id("active-thread-scrollbar")
2234 .on_mouse_move(cx.listener(|_, _, _, cx| {
2235 cx.notify();
2236 cx.stop_propagation()
2237 }))
2238 .on_hover(|_, _, cx| {
2239 cx.stop_propagation();
2240 })
2241 .on_any_mouse_down(|_, _, cx| {
2242 cx.stop_propagation();
2243 })
2244 .on_mouse_up(
2245 MouseButton::Left,
2246 cx.listener(|_, _, _, cx| {
2247 cx.stop_propagation();
2248 }),
2249 )
2250 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2251 cx.notify();
2252 }))
2253 .h_full()
2254 .absolute()
2255 .right_1()
2256 .top_1()
2257 .bottom_0()
2258 .w(px(12.))
2259 .cursor_default()
2260 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
2261 }
2262}
2263
2264impl Render for ActiveThread {
2265 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2266 v_flex()
2267 .size_full()
2268 .relative()
2269 .child(list(self.list_state.clone()).flex_grow())
2270 .child(self.render_vertical_scrollbar(cx))
2271 }
2272}
2273
2274pub(crate) fn open_context(
2275 id: ContextId,
2276 context_store: Entity<ContextStore>,
2277 workspace: Entity<Workspace>,
2278 window: &mut Window,
2279 cx: &mut App,
2280) {
2281 let Some(context) = context_store.read(cx).context_for_id(id) else {
2282 return;
2283 };
2284
2285 match context {
2286 AssistantContext::File(file_context) => {
2287 if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
2288 {
2289 workspace.update(cx, |workspace, cx| {
2290 workspace
2291 .open_path(project_path, None, true, window, cx)
2292 .detach_and_log_err(cx);
2293 });
2294 }
2295 }
2296 AssistantContext::Directory(directory_context) => {
2297 let path = directory_context.project_path.clone();
2298 workspace.update(cx, |workspace, cx| {
2299 workspace.project().update(cx, |project, cx| {
2300 if let Some(entry) = project.entry_for_path(&path, cx) {
2301 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2302 }
2303 })
2304 })
2305 }
2306 AssistantContext::Symbol(symbol_context) => {
2307 if let Some(project_path) = symbol_context
2308 .context_symbol
2309 .buffer
2310 .read(cx)
2311 .project_path(cx)
2312 {
2313 let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
2314 let target_position = symbol_context
2315 .context_symbol
2316 .id
2317 .range
2318 .start
2319 .to_point(&snapshot);
2320
2321 let open_task = workspace.update(cx, |workspace, cx| {
2322 workspace.open_path(project_path, None, true, window, cx)
2323 });
2324 window
2325 .spawn(cx, async move |cx| {
2326 if let Some(active_editor) = open_task
2327 .await
2328 .log_err()
2329 .and_then(|item| item.downcast::<Editor>())
2330 {
2331 active_editor
2332 .downgrade()
2333 .update_in(cx, |editor, window, cx| {
2334 editor.go_to_singleton_buffer_point(
2335 target_position,
2336 window,
2337 cx,
2338 );
2339 })
2340 .log_err();
2341 }
2342 })
2343 .detach();
2344 }
2345 }
2346 AssistantContext::FetchedUrl(fetched_url_context) => {
2347 cx.open_url(&fetched_url_context.url);
2348 }
2349 AssistantContext::Thread(thread_context) => {
2350 let thread_id = thread_context.thread.read(cx).id().clone();
2351 workspace.update(cx, |workspace, cx| {
2352 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
2353 panel.update(cx, |panel, cx| {
2354 panel
2355 .open_thread(&thread_id, window, cx)
2356 .detach_and_log_err(cx)
2357 });
2358 }
2359 })
2360 }
2361 }
2362}