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