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