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 let Some(editor) = self.feedback_comments_editor.clone() else {
957 return;
958 };
959
960 let report_task = self.thread.update(cx, |thread, cx| {
961 thread.report_feedback(ThreadFeedback::Negative, cx)
962 });
963
964 let comments = editor.read(cx).text(cx);
965 if !comments.is_empty() {
966 let thread_id = self.thread.read(cx).id().clone();
967
968 telemetry::event!("Assistant Thread Feedback Comments", thread_id, comments);
969 }
970
971 self.showing_feedback_comments = false;
972 self.feedback_comments_editor = None;
973
974 let this = cx.entity().downgrade();
975 cx.spawn(async move |_, cx| {
976 report_task.await?;
977 this.update(cx, |_this, cx| cx.notify())
978 })
979 .detach_and_log_err(cx);
980 }
981
982 fn handle_cancel_comments(
983 &mut self,
984 _: &ClickEvent,
985 _window: &mut Window,
986 cx: &mut Context<Self>,
987 ) {
988 self.showing_feedback_comments = false;
989 self.feedback_comments_editor = None;
990 cx.notify();
991 }
992
993 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
994 let message_id = self.messages[ix];
995 let Some(message) = self.thread.read(cx).message(message_id) else {
996 return Empty.into_any();
997 };
998
999 let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
1000 return Empty.into_any();
1001 };
1002
1003 let context_store = self.context_store.clone();
1004 let workspace = self.workspace.clone();
1005
1006 let thread = self.thread.read(cx);
1007 // Get all the data we need from thread before we start using it in closures
1008 let checkpoint = thread.checkpoint_for_message(message_id);
1009 let context = thread.context_for_message(message_id).collect::<Vec<_>>();
1010 let tool_uses = thread.tool_uses_for_message(message_id, cx);
1011 let has_tool_uses = !tool_uses.is_empty();
1012
1013 // Don't render user messages that are just there for returning tool results.
1014 if message.role == Role::User && thread.message_has_tool_results(message_id) {
1015 return Empty.into_any();
1016 }
1017
1018 let allow_editing_message =
1019 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
1020
1021 let edit_message_editor = self
1022 .editing_message
1023 .as_ref()
1024 .filter(|(id, _)| *id == message_id)
1025 .map(|(_, state)| state.editor.clone());
1026
1027 let first_message = ix == 0;
1028 let show_feedback = ix == self.messages.len() - 1 && message.role != Role::User;
1029
1030 let colors = cx.theme().colors();
1031 let active_color = colors.element_active;
1032 let editor_bg_color = colors.editor_background;
1033 let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
1034
1035 let feedback_container = h_flex().pt_2().pb_4().px_4().gap_1().justify_between();
1036 let feedback_items = match self.thread.read(cx).feedback() {
1037 Some(feedback) => feedback_container
1038 .child(
1039 Label::new(match feedback {
1040 ThreadFeedback::Positive => "Thanks for your feedback!",
1041 ThreadFeedback::Negative => {
1042 "We appreciate your feedback and will use it to improve."
1043 }
1044 })
1045 .color(Color::Muted)
1046 .size(LabelSize::XSmall),
1047 )
1048 .child(
1049 h_flex()
1050 .gap_1()
1051 .child(
1052 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
1053 .icon_size(IconSize::XSmall)
1054 .icon_color(match feedback {
1055 ThreadFeedback::Positive => Color::Accent,
1056 ThreadFeedback::Negative => Color::Ignored,
1057 })
1058 .shape(ui::IconButtonShape::Square)
1059 .tooltip(Tooltip::text("Helpful Response"))
1060 .on_click(cx.listener(move |this, _, window, cx| {
1061 this.handle_feedback_click(
1062 ThreadFeedback::Positive,
1063 window,
1064 cx,
1065 );
1066 })),
1067 )
1068 .child(
1069 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
1070 .icon_size(IconSize::XSmall)
1071 .icon_color(match feedback {
1072 ThreadFeedback::Positive => Color::Ignored,
1073 ThreadFeedback::Negative => Color::Accent,
1074 })
1075 .shape(ui::IconButtonShape::Square)
1076 .tooltip(Tooltip::text("Not Helpful"))
1077 .on_click(cx.listener(move |this, _, window, cx| {
1078 this.handle_feedback_click(
1079 ThreadFeedback::Negative,
1080 window,
1081 cx,
1082 );
1083 })),
1084 ),
1085 )
1086 .into_any_element(),
1087 None => feedback_container
1088 .child(
1089 Label::new(
1090 "Rating the thread sends all of your current conversation to the Zed team.",
1091 )
1092 .color(Color::Muted)
1093 .size(LabelSize::XSmall),
1094 )
1095 .child(
1096 h_flex()
1097 .gap_1()
1098 .child(
1099 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
1100 .icon_size(IconSize::XSmall)
1101 .icon_color(Color::Ignored)
1102 .shape(ui::IconButtonShape::Square)
1103 .tooltip(Tooltip::text("Helpful Response"))
1104 .on_click(cx.listener(move |this, _, window, cx| {
1105 this.handle_feedback_click(
1106 ThreadFeedback::Positive,
1107 window,
1108 cx,
1109 );
1110 })),
1111 )
1112 .child(
1113 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
1114 .icon_size(IconSize::XSmall)
1115 .icon_color(Color::Ignored)
1116 .shape(ui::IconButtonShape::Square)
1117 .tooltip(Tooltip::text("Not Helpful"))
1118 .on_click(cx.listener(move |this, _, window, cx| {
1119 this.handle_feedback_click(
1120 ThreadFeedback::Negative,
1121 window,
1122 cx,
1123 );
1124 })),
1125 ),
1126 )
1127 .into_any_element(),
1128 };
1129
1130 let message_content = v_flex()
1131 .gap_1p5()
1132 .child(
1133 if let Some(edit_message_editor) = edit_message_editor.clone() {
1134 div()
1135 .key_context("EditMessageEditor")
1136 .on_action(cx.listener(Self::cancel_editing_message))
1137 .on_action(cx.listener(Self::confirm_editing_message))
1138 .min_h_6()
1139 .child(edit_message_editor)
1140 } else {
1141 div()
1142 .min_h_6()
1143 .text_ui(cx)
1144 .child(self.render_message_content(
1145 message_id,
1146 rendered_message,
1147 has_tool_uses,
1148 cx,
1149 ))
1150 },
1151 )
1152 .when(!context.is_empty(), |parent| {
1153 parent.child(
1154 h_flex()
1155 .flex_wrap()
1156 .gap_1()
1157 .children(context.into_iter().map(|context| {
1158 let context_id = context.id();
1159 ContextPill::added(AddedContext::new(context, cx), false, false, None)
1160 .on_click(Rc::new(cx.listener({
1161 let workspace = workspace.clone();
1162 let context_store = context_store.clone();
1163 move |_, _, window, cx| {
1164 if let Some(workspace) = workspace.upgrade() {
1165 open_context(
1166 context_id,
1167 context_store.clone(),
1168 workspace,
1169 window,
1170 cx,
1171 );
1172 cx.notify();
1173 }
1174 }
1175 })))
1176 })),
1177 )
1178 });
1179
1180 let styled_message = match message.role {
1181 Role::User => v_flex()
1182 .id(("message-container", ix))
1183 .map(|this| {
1184 if first_message {
1185 this.pt_2()
1186 } else {
1187 this.pt_4()
1188 }
1189 })
1190 .pb_4()
1191 .pl_2()
1192 .pr_2p5()
1193 .child(
1194 v_flex()
1195 .bg(colors.editor_background)
1196 .rounded_lg()
1197 .border_1()
1198 .border_color(colors.border)
1199 .shadow_md()
1200 .child(
1201 h_flex()
1202 .py_1()
1203 .pl_2()
1204 .pr_1()
1205 .bg(bg_user_message_header)
1206 .border_b_1()
1207 .border_color(colors.border)
1208 .justify_between()
1209 .rounded_t_md()
1210 .child(
1211 h_flex()
1212 .gap_1p5()
1213 .child(
1214 Icon::new(IconName::PersonCircle)
1215 .size(IconSize::XSmall)
1216 .color(Color::Muted),
1217 )
1218 .child(
1219 Label::new("You")
1220 .size(LabelSize::Small)
1221 .color(Color::Muted),
1222 ),
1223 )
1224 .child(
1225 h_flex()
1226 // DL: To double-check whether we want to fully remove
1227 // the editing feature from meassages. Checkpoint sort of
1228 // solve the same problem.
1229 .invisible()
1230 .gap_1()
1231 .when_some(
1232 edit_message_editor.clone(),
1233 |this, edit_message_editor| {
1234 let focus_handle =
1235 edit_message_editor.focus_handle(cx);
1236 this.child(
1237 Button::new("cancel-edit-message", "Cancel")
1238 .label_size(LabelSize::Small)
1239 .key_binding(
1240 KeyBinding::for_action_in(
1241 &menu::Cancel,
1242 &focus_handle,
1243 window,
1244 cx,
1245 )
1246 .map(|kb| kb.size(rems_from_px(12.))),
1247 )
1248 .on_click(
1249 cx.listener(Self::handle_cancel_click),
1250 ),
1251 )
1252 .child(
1253 Button::new(
1254 "confirm-edit-message",
1255 "Regenerate",
1256 )
1257 .label_size(LabelSize::Small)
1258 .key_binding(
1259 KeyBinding::for_action_in(
1260 &menu::Confirm,
1261 &focus_handle,
1262 window,
1263 cx,
1264 )
1265 .map(|kb| kb.size(rems_from_px(12.))),
1266 )
1267 .on_click(
1268 cx.listener(Self::handle_regenerate_click),
1269 ),
1270 )
1271 },
1272 )
1273 .when(
1274 edit_message_editor.is_none() && allow_editing_message,
1275 |this| {
1276 this.child(
1277 Button::new("edit-message", "Edit")
1278 .label_size(LabelSize::Small)
1279 .on_click(cx.listener({
1280 let message_segments =
1281 message.segments.clone();
1282 move |this, _, window, cx| {
1283 this.start_editing_message(
1284 message_id,
1285 &message_segments,
1286 window,
1287 cx,
1288 );
1289 }
1290 })),
1291 )
1292 },
1293 ),
1294 ),
1295 )
1296 .child(div().p_2().child(message_content)),
1297 ),
1298 Role::Assistant => v_flex()
1299 .id(("message-container", ix))
1300 .ml_2()
1301 .pl_2()
1302 .pr_4()
1303 .border_l_1()
1304 .border_color(cx.theme().colors().border_variant)
1305 .child(message_content)
1306 .when(!tool_uses.is_empty(), |parent| {
1307 parent.child(
1308 v_flex().children(
1309 tool_uses
1310 .into_iter()
1311 .map(|tool_use| self.render_tool_use(tool_use, cx)),
1312 ),
1313 )
1314 }),
1315 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1316 v_flex()
1317 .bg(colors.editor_background)
1318 .rounded_sm()
1319 .child(div().p_4().child(message_content)),
1320 ),
1321 };
1322
1323 v_flex()
1324 .w_full()
1325 .when(first_message, |parent| {
1326 parent.child(self.render_rules_item(cx))
1327 })
1328 .when_some(checkpoint, |parent, checkpoint| {
1329 let mut is_pending = false;
1330 let mut error = None;
1331 if let Some(last_restore_checkpoint) =
1332 self.thread.read(cx).last_restore_checkpoint()
1333 {
1334 if last_restore_checkpoint.message_id() == message_id {
1335 match last_restore_checkpoint {
1336 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1337 LastRestoreCheckpoint::Error { error: err, .. } => {
1338 error = Some(err.clone());
1339 }
1340 }
1341 }
1342 }
1343
1344 let restore_checkpoint_button =
1345 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1346 .icon(if error.is_some() {
1347 IconName::XCircle
1348 } else {
1349 IconName::Undo
1350 })
1351 .icon_size(IconSize::XSmall)
1352 .icon_position(IconPosition::Start)
1353 .icon_color(if error.is_some() {
1354 Some(Color::Error)
1355 } else {
1356 None
1357 })
1358 .label_size(LabelSize::XSmall)
1359 .disabled(is_pending)
1360 .on_click(cx.listener(move |this, _, _window, cx| {
1361 this.thread.update(cx, |thread, cx| {
1362 thread
1363 .restore_checkpoint(checkpoint.clone(), cx)
1364 .detach_and_log_err(cx);
1365 });
1366 }));
1367
1368 let restore_checkpoint_button = if is_pending {
1369 restore_checkpoint_button
1370 .with_animation(
1371 ("pulsating-restore-checkpoint-button", ix),
1372 Animation::new(Duration::from_secs(2))
1373 .repeat()
1374 .with_easing(pulsating_between(0.6, 1.)),
1375 |label, delta| label.alpha(delta),
1376 )
1377 .into_any_element()
1378 } else if let Some(error) = error {
1379 restore_checkpoint_button
1380 .tooltip(Tooltip::text(error.to_string()))
1381 .into_any_element()
1382 } else {
1383 restore_checkpoint_button.into_any_element()
1384 };
1385
1386 parent.child(
1387 h_flex()
1388 .pt_2p5()
1389 .px_2p5()
1390 .w_full()
1391 .gap_1()
1392 .child(ui::Divider::horizontal())
1393 .child(restore_checkpoint_button)
1394 .child(ui::Divider::horizontal()),
1395 )
1396 })
1397 .child(styled_message)
1398 .when(
1399 show_feedback && !self.thread.read(cx).is_generating(),
1400 |parent| {
1401 parent
1402 .child(feedback_items)
1403 .when(self.showing_feedback_comments, |parent| {
1404 parent.child(
1405 v_flex()
1406 .gap_1()
1407 .px_4()
1408 .child(
1409 Label::new(
1410 "Please share your feedback to help us improve:",
1411 )
1412 .size(LabelSize::Small),
1413 )
1414 .child(
1415 div()
1416 .p_2()
1417 .rounded_md()
1418 .border_1()
1419 .border_color(cx.theme().colors().border)
1420 .bg(cx.theme().colors().editor_background)
1421 .child(
1422 self.feedback_comments_editor
1423 .as_ref()
1424 .unwrap()
1425 .clone(),
1426 ),
1427 )
1428 .child(
1429 h_flex()
1430 .gap_1()
1431 .justify_end()
1432 .pb_2()
1433 .child(
1434 Button::new("cancel-comments", "Cancel").on_click(
1435 cx.listener(Self::handle_cancel_comments),
1436 ),
1437 )
1438 .child(
1439 Button::new("submit-comments", "Submit").on_click(
1440 cx.listener(Self::handle_submit_comments),
1441 ),
1442 ),
1443 ),
1444 )
1445 })
1446 },
1447 )
1448 .into_any()
1449 }
1450
1451 fn render_message_content(
1452 &self,
1453 message_id: MessageId,
1454 rendered_message: &RenderedMessage,
1455 has_tool_uses: bool,
1456 cx: &Context<Self>,
1457 ) -> impl IntoElement {
1458 let is_last_message = self.messages.last() == Some(&message_id);
1459 let pending_thinking_segment_index = if is_last_message && !has_tool_uses {
1460 rendered_message
1461 .segments
1462 .iter()
1463 .enumerate()
1464 .last()
1465 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1466 .map(|(index, _)| index)
1467 } else {
1468 None
1469 };
1470
1471 div()
1472 .text_ui(cx)
1473 .gap_2()
1474 .children(
1475 rendered_message.segments.iter().enumerate().map(
1476 |(index, segment)| match segment {
1477 RenderedMessageSegment::Thinking {
1478 content,
1479 scroll_handle,
1480 } => self
1481 .render_message_thinking_segment(
1482 message_id,
1483 index,
1484 content.clone(),
1485 &scroll_handle,
1486 Some(index) == pending_thinking_segment_index,
1487 cx,
1488 )
1489 .into_any_element(),
1490 RenderedMessageSegment::Text(markdown) => {
1491 div().child(markdown.clone()).into_any_element()
1492 }
1493 },
1494 ),
1495 )
1496 }
1497
1498 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1499 cx.theme().colors().border.opacity(0.5)
1500 }
1501
1502 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1503 cx.theme()
1504 .colors()
1505 .element_background
1506 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1507 }
1508
1509 fn render_message_thinking_segment(
1510 &self,
1511 message_id: MessageId,
1512 ix: usize,
1513 markdown: Entity<Markdown>,
1514 scroll_handle: &ScrollHandle,
1515 pending: bool,
1516 cx: &Context<Self>,
1517 ) -> impl IntoElement {
1518 let is_open = self
1519 .expanded_thinking_segments
1520 .get(&(message_id, ix))
1521 .copied()
1522 .unwrap_or_default();
1523
1524 let editor_bg = cx.theme().colors().editor_background;
1525
1526 div().pt_0p5().pb_2().child(
1527 v_flex()
1528 .rounded_lg()
1529 .border_1()
1530 .border_color(self.tool_card_border_color(cx))
1531 .child(
1532 h_flex()
1533 .group("disclosure-header")
1534 .justify_between()
1535 .py_1()
1536 .px_2()
1537 .bg(self.tool_card_header_bg(cx))
1538 .map(|this| {
1539 if pending || is_open {
1540 this.rounded_t_md()
1541 .border_b_1()
1542 .border_color(self.tool_card_border_color(cx))
1543 } else {
1544 this.rounded_md()
1545 }
1546 })
1547 .child(
1548 h_flex()
1549 .gap_1p5()
1550 .child(
1551 Icon::new(IconName::Brain)
1552 .size(IconSize::XSmall)
1553 .color(Color::Muted),
1554 )
1555 .child({
1556 if pending {
1557 Label::new("Thinking…")
1558 .size(LabelSize::Small)
1559 .buffer_font(cx)
1560 .with_animation(
1561 "pulsating-label",
1562 Animation::new(Duration::from_secs(2))
1563 .repeat()
1564 .with_easing(pulsating_between(0.4, 0.8)),
1565 |label, delta| label.alpha(delta),
1566 )
1567 .into_any_element()
1568 } else {
1569 Label::new("Thought Process")
1570 .size(LabelSize::Small)
1571 .buffer_font(cx)
1572 .into_any_element()
1573 }
1574 }),
1575 )
1576 .child(
1577 h_flex()
1578 .gap_1()
1579 .child(
1580 div().visible_on_hover("disclosure-header").child(
1581 Disclosure::new("thinking-disclosure", is_open)
1582 .opened_icon(IconName::ChevronUp)
1583 .closed_icon(IconName::ChevronDown)
1584 .on_click(cx.listener({
1585 move |this, _event, _window, _cx| {
1586 let is_open = this
1587 .expanded_thinking_segments
1588 .entry((message_id, ix))
1589 .or_insert(false);
1590
1591 *is_open = !*is_open;
1592 }
1593 })),
1594 ),
1595 )
1596 .child({
1597 let (icon_name, color, animated) = if pending {
1598 (IconName::ArrowCircle, Color::Accent, true)
1599 } else {
1600 (IconName::Check, Color::Success, false)
1601 };
1602
1603 let icon =
1604 Icon::new(icon_name).color(color).size(IconSize::Small);
1605
1606 if animated {
1607 icon.with_animation(
1608 "arrow-circle",
1609 Animation::new(Duration::from_secs(2)).repeat(),
1610 |icon, delta| {
1611 icon.transform(Transformation::rotate(percentage(
1612 delta,
1613 )))
1614 },
1615 )
1616 .into_any_element()
1617 } else {
1618 icon.into_any_element()
1619 }
1620 }),
1621 ),
1622 )
1623 .when(pending && !is_open, |this| {
1624 let gradient_overlay = div()
1625 .rounded_b_lg()
1626 .h_20()
1627 .absolute()
1628 .w_full()
1629 .bottom_0()
1630 .left_0()
1631 .bg(linear_gradient(
1632 180.,
1633 linear_color_stop(editor_bg, 1.),
1634 linear_color_stop(editor_bg.opacity(0.2), 0.),
1635 ));
1636
1637 this.child(
1638 div()
1639 .relative()
1640 .bg(editor_bg)
1641 .rounded_b_lg()
1642 .child(
1643 div()
1644 .id(("thinking-content", ix))
1645 .p_2()
1646 .h_20()
1647 .track_scroll(scroll_handle)
1648 .text_ui_sm(cx)
1649 .child(markdown.clone())
1650 .overflow_hidden(),
1651 )
1652 .child(gradient_overlay),
1653 )
1654 })
1655 .when(is_open, |this| {
1656 this.child(
1657 div()
1658 .id(("thinking-content", ix))
1659 .h_full()
1660 .p_2()
1661 .rounded_b_lg()
1662 .bg(editor_bg)
1663 .text_ui_sm(cx)
1664 .child(markdown.clone()),
1665 )
1666 }),
1667 )
1668 }
1669
1670 fn render_tool_use(
1671 &self,
1672 tool_use: ToolUse,
1673 cx: &mut Context<Self>,
1674 ) -> impl IntoElement + use<> {
1675 let is_open = self
1676 .expanded_tool_uses
1677 .get(&tool_use.id)
1678 .copied()
1679 .unwrap_or_default();
1680
1681 let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
1682
1683 let fs = self
1684 .workspace
1685 .upgrade()
1686 .map(|workspace| workspace.read(cx).app_state().fs.clone());
1687 let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
1688
1689 let status_icons = div().child(match &tool_use.status {
1690 ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
1691 let icon = Icon::new(IconName::Warning)
1692 .color(Color::Warning)
1693 .size(IconSize::Small);
1694 icon.into_any_element()
1695 }
1696 ToolUseStatus::Running => {
1697 let icon = Icon::new(IconName::ArrowCircle)
1698 .color(Color::Accent)
1699 .size(IconSize::Small);
1700 icon.with_animation(
1701 "arrow-circle",
1702 Animation::new(Duration::from_secs(2)).repeat(),
1703 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1704 )
1705 .into_any_element()
1706 }
1707 ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
1708 ToolUseStatus::Error(_) => {
1709 let icon = Icon::new(IconName::Close)
1710 .color(Color::Error)
1711 .size(IconSize::Small);
1712 icon.into_any_element()
1713 }
1714 });
1715
1716 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1717 let results_content = v_flex()
1718 .gap_1()
1719 .child(
1720 content_container()
1721 .child(
1722 Label::new("Input")
1723 .size(LabelSize::XSmall)
1724 .color(Color::Muted)
1725 .buffer_font(cx),
1726 )
1727 .child(
1728 Label::new(
1729 serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1730 )
1731 .size(LabelSize::Small)
1732 .buffer_font(cx),
1733 ),
1734 )
1735 .map(|container| match tool_use.status {
1736 ToolUseStatus::Finished(output) => container.child(
1737 content_container()
1738 .border_t_1()
1739 .border_color(self.tool_card_border_color(cx))
1740 .child(
1741 Label::new("Result")
1742 .size(LabelSize::XSmall)
1743 .color(Color::Muted)
1744 .buffer_font(cx),
1745 )
1746 .child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
1747 ),
1748 ToolUseStatus::Running => container.child(
1749 content_container().child(
1750 h_flex()
1751 .gap_1()
1752 .pb_1()
1753 .border_t_1()
1754 .border_color(self.tool_card_border_color(cx))
1755 .child(
1756 Icon::new(IconName::ArrowCircle)
1757 .size(IconSize::Small)
1758 .color(Color::Accent)
1759 .with_animation(
1760 "arrow-circle",
1761 Animation::new(Duration::from_secs(2)).repeat(),
1762 |icon, delta| {
1763 icon.transform(Transformation::rotate(percentage(
1764 delta,
1765 )))
1766 },
1767 ),
1768 )
1769 .child(
1770 Label::new("Running…")
1771 .size(LabelSize::XSmall)
1772 .color(Color::Muted)
1773 .buffer_font(cx),
1774 ),
1775 ),
1776 ),
1777 ToolUseStatus::Error(err) => container.child(
1778 content_container()
1779 .border_t_1()
1780 .border_color(self.tool_card_border_color(cx))
1781 .child(
1782 Label::new("Error")
1783 .size(LabelSize::XSmall)
1784 .color(Color::Muted)
1785 .buffer_font(cx),
1786 )
1787 .child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
1788 ),
1789 ToolUseStatus::Pending => container,
1790 ToolUseStatus::NeedsConfirmation => container.child(
1791 content_container()
1792 .border_t_1()
1793 .border_color(self.tool_card_border_color(cx))
1794 .child(
1795 Label::new("Asking Permission")
1796 .size(LabelSize::Small)
1797 .color(Color::Muted)
1798 .buffer_font(cx),
1799 ),
1800 ),
1801 });
1802
1803 let gradient_overlay = |color: Hsla| {
1804 div()
1805 .h_full()
1806 .absolute()
1807 .w_8()
1808 .bottom_0()
1809 .map(|element| {
1810 if is_status_finished {
1811 element.right_7()
1812 } else {
1813 element.right(px(46.))
1814 }
1815 })
1816 .bg(linear_gradient(
1817 90.,
1818 linear_color_stop(color, 1.),
1819 linear_color_stop(color.opacity(0.2), 0.),
1820 ))
1821 };
1822
1823 div().map(|element| {
1824 if !tool_use.needs_confirmation {
1825 element.py_2p5().child(
1826 v_flex()
1827 .child(
1828 h_flex()
1829 .group("disclosure-header")
1830 .relative()
1831 .gap_1p5()
1832 .justify_between()
1833 .opacity(0.8)
1834 .hover(|style| style.opacity(1.))
1835 .when(!is_status_finished, |this| this.pr_2())
1836 .child(
1837 h_flex()
1838 .id("tool-label-container")
1839 .gap_1p5()
1840 .max_w_full()
1841 .overflow_x_scroll()
1842 .child(
1843 Icon::new(tool_use.icon)
1844 .size(IconSize::XSmall)
1845 .color(Color::Muted),
1846 )
1847 .child(
1848 h_flex().pr_8().text_ui_sm(cx).children(
1849 self.rendered_tool_use_labels
1850 .get(&tool_use.id)
1851 .cloned(),
1852 ),
1853 ),
1854 )
1855 .child(
1856 h_flex()
1857 .gap_1()
1858 .child(
1859 div().visible_on_hover("disclosure-header").child(
1860 Disclosure::new("tool-use-disclosure", is_open)
1861 .opened_icon(IconName::ChevronUp)
1862 .closed_icon(IconName::ChevronDown)
1863 .on_click(cx.listener({
1864 let tool_use_id = tool_use.id.clone();
1865 move |this, _event, _window, _cx| {
1866 let is_open = this
1867 .expanded_tool_uses
1868 .entry(tool_use_id.clone())
1869 .or_insert(false);
1870
1871 *is_open = !*is_open;
1872 }
1873 })),
1874 ),
1875 )
1876 .child(status_icons),
1877 )
1878 .child(gradient_overlay(cx.theme().colors().panel_background)),
1879 )
1880 .map(|parent| {
1881 if !is_open {
1882 return parent;
1883 }
1884
1885 parent.child(
1886 v_flex()
1887 .mt_1()
1888 .border_1()
1889 .border_color(self.tool_card_border_color(cx))
1890 .bg(cx.theme().colors().editor_background)
1891 .rounded_lg()
1892 .child(results_content),
1893 )
1894 }),
1895 )
1896 } else {
1897 element.py_2().child(
1898 v_flex()
1899 .rounded_lg()
1900 .border_1()
1901 .border_color(self.tool_card_border_color(cx))
1902 .overflow_hidden()
1903 .child(
1904 h_flex()
1905 .group("disclosure-header")
1906 .relative()
1907 .justify_between()
1908 .py_1()
1909 .map(|element| {
1910 if is_status_finished {
1911 element.pl_2().pr_0p5()
1912 } else {
1913 element.px_2()
1914 }
1915 })
1916 .bg(self.tool_card_header_bg(cx))
1917 .map(|element| {
1918 if is_open {
1919 element.border_b_1().rounded_t_md()
1920 } else if needs_confirmation {
1921 element.rounded_t_md()
1922 } else {
1923 element.rounded_md()
1924 }
1925 })
1926 .border_color(self.tool_card_border_color(cx))
1927 .child(
1928 h_flex()
1929 .id("tool-label-container")
1930 .gap_1p5()
1931 .max_w_full()
1932 .overflow_x_scroll()
1933 .child(
1934 Icon::new(tool_use.icon)
1935 .size(IconSize::XSmall)
1936 .color(Color::Muted),
1937 )
1938 .child(
1939 h_flex().pr_8().text_ui_sm(cx).children(
1940 self.rendered_tool_use_labels
1941 .get(&tool_use.id)
1942 .cloned(),
1943 ),
1944 ),
1945 )
1946 .child(
1947 h_flex()
1948 .gap_1()
1949 .child(
1950 div().visible_on_hover("disclosure-header").child(
1951 Disclosure::new("tool-use-disclosure", is_open)
1952 .opened_icon(IconName::ChevronUp)
1953 .closed_icon(IconName::ChevronDown)
1954 .on_click(cx.listener({
1955 let tool_use_id = tool_use.id.clone();
1956 move |this, _event, _window, _cx| {
1957 let is_open = this
1958 .expanded_tool_uses
1959 .entry(tool_use_id.clone())
1960 .or_insert(false);
1961
1962 *is_open = !*is_open;
1963 }
1964 })),
1965 ),
1966 )
1967 .child(status_icons),
1968 )
1969 .child(gradient_overlay(self.tool_card_header_bg(cx))),
1970 )
1971 .map(|parent| {
1972 if !is_open {
1973 return parent;
1974 }
1975
1976 parent.child(
1977 v_flex()
1978 .bg(cx.theme().colors().editor_background)
1979 .map(|element| {
1980 if needs_confirmation {
1981 element.rounded_none()
1982 } else {
1983 element.rounded_b_lg()
1984 }
1985 })
1986 .child(results_content),
1987 )
1988 })
1989 .when(needs_confirmation, |this| {
1990 this.child(
1991 h_flex()
1992 .py_1()
1993 .pl_2()
1994 .pr_1()
1995 .gap_1()
1996 .justify_between()
1997 .bg(cx.theme().colors().editor_background)
1998 .border_t_1()
1999 .border_color(self.tool_card_border_color(cx))
2000 .rounded_b_lg()
2001 .child(Label::new("Action Confirmation").color(Color::Muted).size(LabelSize::Small))
2002 .child(
2003 h_flex()
2004 .gap_0p5()
2005 .child({
2006 let tool_id = tool_use.id.clone();
2007 Button::new(
2008 "always-allow-tool-action",
2009 "Always Allow",
2010 )
2011 .label_size(LabelSize::Small)
2012 .icon(IconName::CheckDouble)
2013 .icon_position(IconPosition::Start)
2014 .icon_size(IconSize::Small)
2015 .icon_color(Color::Success)
2016 .tooltip(move |window, cx| {
2017 Tooltip::with_meta(
2018 "Never ask for permission",
2019 None,
2020 "Restore the original behavior in your Agent Panel settings",
2021 window,
2022 cx,
2023 )
2024 })
2025 .on_click(cx.listener(
2026 move |this, event, window, cx| {
2027 if let Some(fs) = fs.clone() {
2028 update_settings_file::<AssistantSettings>(
2029 fs.clone(),
2030 cx,
2031 |settings, _| {
2032 settings.set_always_allow_tool_actions(true);
2033 },
2034 );
2035 }
2036 this.handle_allow_tool(
2037 tool_id.clone(),
2038 event,
2039 window,
2040 cx,
2041 )
2042 },
2043 ))
2044 })
2045 .child(ui::Divider::vertical())
2046 .child({
2047 let tool_id = tool_use.id.clone();
2048 Button::new("allow-tool-action", "Allow")
2049 .label_size(LabelSize::Small)
2050 .icon(IconName::Check)
2051 .icon_position(IconPosition::Start)
2052 .icon_size(IconSize::Small)
2053 .icon_color(Color::Success)
2054 .on_click(cx.listener(
2055 move |this, event, window, cx| {
2056 this.handle_allow_tool(
2057 tool_id.clone(),
2058 event,
2059 window,
2060 cx,
2061 )
2062 },
2063 ))
2064 })
2065 .child({
2066 let tool_id = tool_use.id.clone();
2067 let tool_name: Arc<str> = tool_use.name.into();
2068 Button::new("deny-tool", "Deny")
2069 .label_size(LabelSize::Small)
2070 .icon(IconName::Close)
2071 .icon_position(IconPosition::Start)
2072 .icon_size(IconSize::Small)
2073 .icon_color(Color::Error)
2074 .on_click(cx.listener(
2075 move |this, event, window, cx| {
2076 this.handle_deny_tool(
2077 tool_id.clone(),
2078 tool_name.clone(),
2079 event,
2080 window,
2081 cx,
2082 )
2083 },
2084 ))
2085 }),
2086 ),
2087 )
2088 }),
2089 )
2090 }
2091 })
2092 }
2093
2094 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
2095 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2096 else {
2097 return div().into_any();
2098 };
2099
2100 let rules_files = system_prompt_context
2101 .worktrees
2102 .iter()
2103 .filter_map(|worktree| worktree.rules_file.as_ref())
2104 .collect::<Vec<_>>();
2105
2106 let label_text = match rules_files.as_slice() {
2107 &[] => return div().into_any(),
2108 &[rules_file] => {
2109 format!("Using {:?} file", rules_file.rel_path)
2110 }
2111 rules_files => {
2112 format!("Using {} rules files", rules_files.len())
2113 }
2114 };
2115
2116 div()
2117 .pt_1()
2118 .px_2p5()
2119 .child(
2120 h_flex()
2121 .w_full()
2122 .gap_0p5()
2123 .child(
2124 h_flex()
2125 .gap_1p5()
2126 .child(
2127 Icon::new(IconName::File)
2128 .size(IconSize::XSmall)
2129 .color(Color::Disabled),
2130 )
2131 .child(
2132 Label::new(label_text)
2133 .size(LabelSize::XSmall)
2134 .color(Color::Muted)
2135 .buffer_font(cx),
2136 ),
2137 )
2138 .child(
2139 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
2140 .shape(ui::IconButtonShape::Square)
2141 .icon_size(IconSize::XSmall)
2142 .icon_color(Color::Ignored)
2143 .on_click(cx.listener(Self::handle_open_rules))
2144 .tooltip(Tooltip::text("View Rules")),
2145 ),
2146 )
2147 .into_any()
2148 }
2149
2150 fn handle_allow_tool(
2151 &mut self,
2152 tool_use_id: LanguageModelToolUseId,
2153 _: &ClickEvent,
2154 _window: &mut Window,
2155 cx: &mut Context<Self>,
2156 ) {
2157 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
2158 .thread
2159 .read(cx)
2160 .pending_tool(&tool_use_id)
2161 .map(|tool_use| tool_use.status.clone())
2162 {
2163 self.thread.update(cx, |thread, cx| {
2164 thread.run_tool(
2165 c.tool_use_id.clone(),
2166 c.ui_text.clone(),
2167 c.input.clone(),
2168 &c.messages,
2169 c.tool.clone(),
2170 cx,
2171 );
2172 });
2173 }
2174 }
2175
2176 fn handle_deny_tool(
2177 &mut self,
2178 tool_use_id: LanguageModelToolUseId,
2179 tool_name: Arc<str>,
2180 _: &ClickEvent,
2181 _window: &mut Window,
2182 cx: &mut Context<Self>,
2183 ) {
2184 self.thread.update(cx, |thread, cx| {
2185 thread.deny_tool_use(tool_use_id, tool_name, cx);
2186 });
2187 }
2188
2189 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
2190 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2191 else {
2192 return;
2193 };
2194
2195 let abs_paths = system_prompt_context
2196 .worktrees
2197 .iter()
2198 .flat_map(|worktree| worktree.rules_file.as_ref())
2199 .map(|rules_file| rules_file.abs_path.to_path_buf())
2200 .collect::<Vec<_>>();
2201
2202 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
2203 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
2204 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
2205 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
2206 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
2207 }) {
2208 task.detach();
2209 }
2210 }
2211
2212 fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
2213 for window in self.notifications.drain(..) {
2214 window
2215 .update(cx, |_, window, _| {
2216 window.remove_window();
2217 })
2218 .ok();
2219
2220 self.notification_subscriptions.remove(&window);
2221 }
2222 }
2223
2224 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
2225 div()
2226 .occlude()
2227 .id("active-thread-scrollbar")
2228 .on_mouse_move(cx.listener(|_, _, _, cx| {
2229 cx.notify();
2230 cx.stop_propagation()
2231 }))
2232 .on_hover(|_, _, cx| {
2233 cx.stop_propagation();
2234 })
2235 .on_any_mouse_down(|_, _, cx| {
2236 cx.stop_propagation();
2237 })
2238 .on_mouse_up(
2239 MouseButton::Left,
2240 cx.listener(|_, _, _, cx| {
2241 cx.stop_propagation();
2242 }),
2243 )
2244 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2245 cx.notify();
2246 }))
2247 .h_full()
2248 .absolute()
2249 .right_1()
2250 .top_1()
2251 .bottom_0()
2252 .w(px(12.))
2253 .cursor_default()
2254 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
2255 }
2256}
2257
2258impl Render for ActiveThread {
2259 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2260 v_flex()
2261 .size_full()
2262 .relative()
2263 .child(list(self.list_state.clone()).flex_grow())
2264 .child(self.render_vertical_scrollbar(cx))
2265 }
2266}
2267
2268pub(crate) fn open_context(
2269 id: ContextId,
2270 context_store: Entity<ContextStore>,
2271 workspace: Entity<Workspace>,
2272 window: &mut Window,
2273 cx: &mut App,
2274) {
2275 let Some(context) = context_store.read(cx).context_for_id(id) else {
2276 return;
2277 };
2278
2279 match context {
2280 AssistantContext::File(file_context) => {
2281 if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
2282 {
2283 workspace.update(cx, |workspace, cx| {
2284 workspace
2285 .open_path(project_path, None, true, window, cx)
2286 .detach_and_log_err(cx);
2287 });
2288 }
2289 }
2290 AssistantContext::Directory(directory_context) => {
2291 let path = directory_context.project_path.clone();
2292 workspace.update(cx, |workspace, cx| {
2293 workspace.project().update(cx, |project, cx| {
2294 if let Some(entry) = project.entry_for_path(&path, cx) {
2295 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2296 }
2297 })
2298 })
2299 }
2300 AssistantContext::Symbol(symbol_context) => {
2301 if let Some(project_path) = symbol_context
2302 .context_symbol
2303 .buffer
2304 .read(cx)
2305 .project_path(cx)
2306 {
2307 let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
2308 let target_position = symbol_context
2309 .context_symbol
2310 .id
2311 .range
2312 .start
2313 .to_point(&snapshot);
2314
2315 let open_task = workspace.update(cx, |workspace, cx| {
2316 workspace.open_path(project_path, None, true, window, cx)
2317 });
2318 window
2319 .spawn(cx, async move |cx| {
2320 if let Some(active_editor) = open_task
2321 .await
2322 .log_err()
2323 .and_then(|item| item.downcast::<Editor>())
2324 {
2325 active_editor
2326 .downgrade()
2327 .update_in(cx, |editor, window, cx| {
2328 editor.go_to_singleton_buffer_point(
2329 target_position,
2330 window,
2331 cx,
2332 );
2333 })
2334 .log_err();
2335 }
2336 })
2337 .detach();
2338 }
2339 }
2340 AssistantContext::FetchedUrl(fetched_url_context) => {
2341 cx.open_url(&fetched_url_context.url);
2342 }
2343 AssistantContext::Thread(thread_context) => {
2344 let thread_id = thread_context.thread.read(cx).id().clone();
2345 workspace.update(cx, |workspace, cx| {
2346 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
2347 panel.update(cx, |panel, cx| {
2348 panel
2349 .open_thread(&thread_id, window, cx)
2350 .detach_and_log_err(cx)
2351 });
2352 }
2353 })
2354 }
2355 }
2356}