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