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;
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 model_registry = LanguageModelRegistry::read_global(cx);
597 if let Some(model) = model_registry.active_model() {
598 self.thread.update(cx, |thread, cx| {
599 thread.attach_tool_results(cx);
600 if !canceled {
601 thread.send_to_model(model, RequestKind::Chat, cx);
602 }
603 });
604 }
605 }
606 }
607 ThreadEvent::CheckpointChanged => cx.notify(),
608 }
609 }
610
611 fn show_notification(
612 &mut self,
613 caption: impl Into<SharedString>,
614 icon: IconName,
615 window: &mut Window,
616 cx: &mut Context<ActiveThread>,
617 ) {
618 if window.is_window_active() || !self.notifications.is_empty() {
619 return;
620 }
621
622 let title = self
623 .thread
624 .read(cx)
625 .summary()
626 .unwrap_or("Agent Panel".into());
627
628 match AssistantSettings::get_global(cx).notify_when_agent_waiting {
629 NotifyWhenAgentWaiting::PrimaryScreen => {
630 if let Some(primary) = cx.primary_display() {
631 self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
632 }
633 }
634 NotifyWhenAgentWaiting::AllScreens => {
635 let caption = caption.into();
636 for screen in cx.displays() {
637 self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
638 }
639 }
640 NotifyWhenAgentWaiting::Never => {
641 // Don't show anything
642 }
643 }
644 }
645
646 fn pop_up(
647 &mut self,
648 icon: IconName,
649 caption: SharedString,
650 title: SharedString,
651 window: &mut Window,
652 screen: Rc<dyn PlatformDisplay>,
653 cx: &mut Context<'_, ActiveThread>,
654 ) {
655 let options = AgentNotification::window_options(screen, cx);
656
657 if let Some(screen_window) = cx
658 .open_window(options, |_, cx| {
659 cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
660 })
661 .log_err()
662 {
663 if let Some(pop_up) = screen_window.entity(cx).log_err() {
664 self.notification_subscriptions
665 .entry(screen_window)
666 .or_insert_with(Vec::new)
667 .push(cx.subscribe_in(&pop_up, window, {
668 |this, _, event, window, cx| match event {
669 AgentNotificationEvent::Accepted => {
670 let handle = window.window_handle();
671 cx.activate(true); // Switch back to the Zed application
672
673 let workspace_handle = this.workspace.clone();
674
675 // If there are multiple Zed windows, activate the correct one.
676 cx.defer(move |cx| {
677 handle
678 .update(cx, |_view, window, _cx| {
679 window.activate_window();
680
681 if let Some(workspace) = workspace_handle.upgrade() {
682 workspace.update(_cx, |workspace, cx| {
683 workspace
684 .focus_panel::<AssistantPanel>(window, cx);
685 });
686 }
687 })
688 .log_err();
689 });
690
691 this.dismiss_notifications(cx);
692 }
693 AgentNotificationEvent::Dismissed => {
694 this.dismiss_notifications(cx);
695 }
696 }
697 }));
698
699 self.notifications.push(screen_window);
700
701 // If the user manually refocuses the original window, dismiss the popup.
702 self.notification_subscriptions
703 .entry(screen_window)
704 .or_insert_with(Vec::new)
705 .push({
706 let pop_up_weak = pop_up.downgrade();
707
708 cx.observe_window_activation(window, move |_, window, cx| {
709 if window.is_window_active() {
710 if let Some(pop_up) = pop_up_weak.upgrade() {
711 pop_up.update(cx, |_, cx| {
712 cx.emit(AgentNotificationEvent::Dismissed);
713 });
714 }
715 }
716 })
717 });
718 }
719 }
720 }
721
722 /// Spawns a task to save the active thread.
723 ///
724 /// Only one task to save the thread will be in flight at a time.
725 fn save_thread(&mut self, cx: &mut Context<Self>) {
726 let thread = self.thread.clone();
727 self.save_thread_task = Some(cx.spawn(async move |this, cx| {
728 let task = this
729 .update(cx, |this, cx| {
730 this.thread_store
731 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
732 })
733 .ok();
734
735 if let Some(task) = task {
736 task.await.log_err();
737 }
738 }));
739 }
740
741 fn start_editing_message(
742 &mut self,
743 message_id: MessageId,
744 message_segments: &[MessageSegment],
745 window: &mut Window,
746 cx: &mut Context<Self>,
747 ) {
748 // User message should always consist of a single text segment,
749 // therefore we can skip returning early if it's not a text segment.
750 let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
751 return;
752 };
753
754 let buffer = cx.new(|cx| {
755 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
756 });
757 let editor = cx.new(|cx| {
758 let mut editor = Editor::new(
759 editor::EditorMode::AutoHeight { max_lines: 8 },
760 buffer,
761 None,
762 window,
763 cx,
764 );
765 editor.focus_handle(cx).focus(window);
766 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
767 editor
768 });
769 self.editing_message = Some((
770 message_id,
771 EditMessageState {
772 editor: editor.clone(),
773 },
774 ));
775 cx.notify();
776 }
777
778 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
779 self.editing_message.take();
780 cx.notify();
781 }
782
783 fn confirm_editing_message(
784 &mut self,
785 _: &menu::Confirm,
786 _: &mut Window,
787 cx: &mut Context<Self>,
788 ) {
789 let Some((message_id, state)) = self.editing_message.take() else {
790 return;
791 };
792 let edited_text = state.editor.read(cx).text(cx);
793 self.thread.update(cx, |thread, cx| {
794 thread.edit_message(
795 message_id,
796 Role::User,
797 vec![MessageSegment::Text(edited_text)],
798 cx,
799 );
800 for message_id in self.messages_after(message_id) {
801 thread.delete_message(*message_id, cx);
802 }
803 });
804
805 let provider = LanguageModelRegistry::read_global(cx).active_provider();
806 if provider
807 .as_ref()
808 .map_or(false, |provider| provider.must_accept_terms(cx))
809 {
810 cx.notify();
811 return;
812 }
813 let model_registry = LanguageModelRegistry::read_global(cx);
814 let Some(model) = model_registry.active_model() else {
815 return;
816 };
817
818 self.thread.update(cx, |thread, cx| {
819 thread.send_to_model(model, RequestKind::Chat, cx)
820 });
821 cx.notify();
822 }
823
824 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
825 self.messages
826 .iter()
827 .position(|id| *id == message_id)
828 .map(|index| &self.messages[index + 1..])
829 .unwrap_or(&[])
830 }
831
832 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
833 self.cancel_editing_message(&menu::Cancel, window, cx);
834 }
835
836 fn handle_regenerate_click(
837 &mut self,
838 _: &ClickEvent,
839 window: &mut Window,
840 cx: &mut Context<Self>,
841 ) {
842 self.confirm_editing_message(&menu::Confirm, window, cx);
843 }
844
845 fn handle_feedback_click(
846 &mut self,
847 feedback: ThreadFeedback,
848 window: &mut Window,
849 cx: &mut Context<Self>,
850 ) {
851 match feedback {
852 ThreadFeedback::Positive => {
853 let report = self
854 .thread
855 .update(cx, |thread, cx| thread.report_feedback(feedback, cx));
856
857 let this = cx.entity().downgrade();
858 cx.spawn(async move |_, cx| {
859 report.await?;
860 this.update(cx, |_this, cx| cx.notify())
861 })
862 .detach_and_log_err(cx);
863 }
864 ThreadFeedback::Negative => {
865 self.handle_show_feedback_comments(window, cx);
866 }
867 }
868 }
869
870 fn handle_show_feedback_comments(&mut self, window: &mut Window, cx: &mut Context<Self>) {
871 if self.feedback_message_editor.is_some() {
872 return;
873 }
874
875 let buffer = cx.new(|cx| {
876 let empty_string = String::new();
877 MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
878 });
879
880 let editor = cx.new(|cx| {
881 let mut editor = Editor::new(
882 editor::EditorMode::AutoHeight { max_lines: 4 },
883 buffer,
884 None,
885 window,
886 cx,
887 );
888 editor.set_placeholder_text(
889 "What went wrong? Share your feedback so we can improve.",
890 cx,
891 );
892 editor
893 });
894
895 editor.read(cx).focus_handle(cx).focus(window);
896 self.feedback_message_editor = Some(editor);
897 cx.notify();
898 }
899
900 fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
901 let Some(editor) = self.feedback_message_editor.clone() else {
902 return;
903 };
904
905 let report_task = self.thread.update(cx, |thread, cx| {
906 thread.report_feedback(ThreadFeedback::Negative, cx)
907 });
908
909 let comments = editor.read(cx).text(cx);
910 if !comments.is_empty() {
911 let thread_id = self.thread.read(cx).id().clone();
912
913 telemetry::event!("Assistant Thread Feedback Comments", thread_id, comments);
914 }
915
916 self.feedback_message_editor = None;
917
918 let this = cx.entity().downgrade();
919 cx.spawn(async move |_, cx| {
920 report_task.await?;
921 this.update(cx, |_this, cx| cx.notify())
922 })
923 .detach_and_log_err(cx);
924 }
925
926 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
927 let message_id = self.messages[ix];
928 let Some(message) = self.thread.read(cx).message(message_id) else {
929 return Empty.into_any();
930 };
931
932 let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
933 return Empty.into_any();
934 };
935
936 let context_store = self.context_store.clone();
937 let workspace = self.workspace.clone();
938
939 let thread = self.thread.read(cx);
940 // Get all the data we need from thread before we start using it in closures
941 let checkpoint = thread.checkpoint_for_message(message_id);
942 let context = thread.context_for_message(message_id).collect::<Vec<_>>();
943 let tool_uses = thread.tool_uses_for_message(message_id, cx);
944 let has_tool_uses = !tool_uses.is_empty();
945
946 // Don't render user messages that are just there for returning tool results.
947 if message.role == Role::User && thread.message_has_tool_results(message_id) {
948 return Empty.into_any();
949 }
950
951 let allow_editing_message = message.role == Role::User;
952
953 let edit_message_editor = self
954 .editing_message
955 .as_ref()
956 .filter(|(id, _)| *id == message_id)
957 .map(|(_, state)| state.editor.clone());
958
959 let first_message = ix == 0;
960 let show_feedback = ix == self.messages.len() - 1 && message.role != Role::User;
961
962 let colors = cx.theme().colors();
963 let active_color = colors.element_active;
964 let editor_bg_color = colors.editor_background;
965 let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
966
967 let feedback_container = h_flex().pt_2().pb_4().px_4().gap_1().justify_between();
968 let feedback_items = match self.thread.read(cx).feedback() {
969 Some(feedback) => feedback_container
970 .child(
971 Label::new(match feedback {
972 ThreadFeedback::Positive => "Thanks for your feedback!",
973 ThreadFeedback::Negative => {
974 "We appreciate your feedback and will use it to improve."
975 }
976 })
977 .color(Color::Muted)
978 .size(LabelSize::XSmall),
979 )
980 .child(
981 h_flex()
982 .gap_1()
983 .child(
984 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
985 .icon_size(IconSize::XSmall)
986 .icon_color(match feedback {
987 ThreadFeedback::Positive => Color::Accent,
988 ThreadFeedback::Negative => Color::Ignored,
989 })
990 .shape(ui::IconButtonShape::Square)
991 .tooltip(Tooltip::text("Helpful Response"))
992 .on_click(cx.listener(move |this, _, window, cx| {
993 this.handle_feedback_click(
994 ThreadFeedback::Positive,
995 window,
996 cx,
997 );
998 })),
999 )
1000 .child(
1001 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
1002 .icon_size(IconSize::XSmall)
1003 .icon_color(match feedback {
1004 ThreadFeedback::Positive => Color::Ignored,
1005 ThreadFeedback::Negative => Color::Accent,
1006 })
1007 .shape(ui::IconButtonShape::Square)
1008 .tooltip(Tooltip::text("Not Helpful"))
1009 .on_click(cx.listener(move |this, _, window, cx| {
1010 this.handle_feedback_click(
1011 ThreadFeedback::Negative,
1012 window,
1013 cx,
1014 );
1015 })),
1016 ),
1017 )
1018 .into_any_element(),
1019 None => feedback_container
1020 .child(
1021 Label::new(
1022 "Rating the thread sends all of your current conversation to the Zed team.",
1023 )
1024 .color(Color::Muted)
1025 .size(LabelSize::XSmall),
1026 )
1027 .child(
1028 h_flex()
1029 .gap_1()
1030 .child(
1031 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
1032 .icon_size(IconSize::XSmall)
1033 .icon_color(Color::Ignored)
1034 .shape(ui::IconButtonShape::Square)
1035 .tooltip(Tooltip::text("Helpful Response"))
1036 .on_click(cx.listener(move |this, _, window, cx| {
1037 this.handle_feedback_click(
1038 ThreadFeedback::Positive,
1039 window,
1040 cx,
1041 );
1042 })),
1043 )
1044 .child(
1045 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
1046 .icon_size(IconSize::XSmall)
1047 .icon_color(Color::Ignored)
1048 .shape(ui::IconButtonShape::Square)
1049 .tooltip(Tooltip::text("Not Helpful"))
1050 .on_click(cx.listener(move |this, _, window, cx| {
1051 this.handle_feedback_click(
1052 ThreadFeedback::Negative,
1053 window,
1054 cx,
1055 );
1056 })),
1057 ),
1058 )
1059 .into_any_element(),
1060 };
1061
1062 let message_is_empty = message.should_display_content();
1063 let has_content = !message_is_empty || !context.is_empty();
1064
1065 let message_content =
1066 has_content.then(|| {
1067 v_flex()
1068 .gap_1p5()
1069 .when(!message_is_empty, |parent| {
1070 parent.child(
1071 if let Some(edit_message_editor) = edit_message_editor.clone() {
1072 div()
1073 .key_context("EditMessageEditor")
1074 .on_action(cx.listener(Self::cancel_editing_message))
1075 .on_action(cx.listener(Self::confirm_editing_message))
1076 .min_h_6()
1077 .child(edit_message_editor)
1078 .into_any()
1079 } else {
1080 div()
1081 .min_h_6()
1082 .text_ui(cx)
1083 .child(self.render_message_content(
1084 message_id,
1085 rendered_message,
1086 has_tool_uses,
1087 cx,
1088 ))
1089 .into_any()
1090 },
1091 )
1092 })
1093 .when(!context.is_empty(), |parent| {
1094 parent.child(h_flex().flex_wrap().gap_1().children(
1095 context.into_iter().map(|context| {
1096 let context_id = context.id();
1097 ContextPill::added(
1098 AddedContext::new(context, cx),
1099 false,
1100 false,
1101 None,
1102 )
1103 .on_click(Rc::new(cx.listener({
1104 let workspace = workspace.clone();
1105 let context_store = context_store.clone();
1106 move |_, _, window, cx| {
1107 if let Some(workspace) = workspace.upgrade() {
1108 open_context(
1109 context_id,
1110 context_store.clone(),
1111 workspace,
1112 window,
1113 cx,
1114 );
1115 cx.notify();
1116 }
1117 }
1118 })))
1119 }),
1120 ))
1121 })
1122 });
1123
1124 let styled_message = match message.role {
1125 Role::User => v_flex()
1126 .id(("message-container", ix))
1127 .map(|this| {
1128 if first_message {
1129 this.pt_2()
1130 } else {
1131 this.pt_4()
1132 }
1133 })
1134 .pb_4()
1135 .pl_2()
1136 .pr_2p5()
1137 .child(
1138 v_flex()
1139 .bg(colors.editor_background)
1140 .rounded_lg()
1141 .border_1()
1142 .border_color(colors.border)
1143 .shadow_md()
1144 .child(
1145 h_flex()
1146 .py_1()
1147 .pl_2()
1148 .pr_1()
1149 .bg(bg_user_message_header)
1150 .border_b_1()
1151 .border_color(colors.border)
1152 .justify_between()
1153 .rounded_t_md()
1154 .child(
1155 h_flex()
1156 .gap_1p5()
1157 .child(
1158 Icon::new(IconName::PersonCircle)
1159 .size(IconSize::XSmall)
1160 .color(Color::Muted),
1161 )
1162 .child(
1163 Label::new("You")
1164 .size(LabelSize::Small)
1165 .color(Color::Muted),
1166 ),
1167 )
1168 .child(
1169 h_flex()
1170 .gap_1()
1171 .when_some(
1172 edit_message_editor.clone(),
1173 |this, edit_message_editor| {
1174 let focus_handle =
1175 edit_message_editor.focus_handle(cx);
1176 this.child(
1177 Button::new("cancel-edit-message", "Cancel")
1178 .label_size(LabelSize::Small)
1179 .key_binding(
1180 KeyBinding::for_action_in(
1181 &menu::Cancel,
1182 &focus_handle,
1183 window,
1184 cx,
1185 )
1186 .map(|kb| kb.size(rems_from_px(12.))),
1187 )
1188 .on_click(
1189 cx.listener(Self::handle_cancel_click),
1190 ),
1191 )
1192 .child(
1193 Button::new(
1194 "confirm-edit-message",
1195 "Regenerate",
1196 )
1197 .label_size(LabelSize::Small)
1198 .key_binding(
1199 KeyBinding::for_action_in(
1200 &menu::Confirm,
1201 &focus_handle,
1202 window,
1203 cx,
1204 )
1205 .map(|kb| kb.size(rems_from_px(12.))),
1206 )
1207 .on_click(
1208 cx.listener(Self::handle_regenerate_click),
1209 ),
1210 )
1211 },
1212 )
1213 .when(
1214 edit_message_editor.is_none() && allow_editing_message,
1215 |this| {
1216 this.child(
1217 Button::new("edit-message", "Edit")
1218 .label_size(LabelSize::Small)
1219 .on_click(cx.listener({
1220 let message_segments =
1221 message.segments.clone();
1222 move |this, _, window, cx| {
1223 this.start_editing_message(
1224 message_id,
1225 &message_segments,
1226 window,
1227 cx,
1228 );
1229 }
1230 })),
1231 )
1232 },
1233 ),
1234 ),
1235 )
1236 .child(div().p_2().children(message_content)),
1237 ),
1238 Role::Assistant => v_flex()
1239 .id(("message-container", ix))
1240 .ml_2()
1241 .pl_2()
1242 .pr_4()
1243 .border_l_1()
1244 .border_color(cx.theme().colors().border_variant)
1245 .children(message_content)
1246 .gap_2p5()
1247 .pb_2p5()
1248 .when(!tool_uses.is_empty(), |parent| {
1249 parent.child(
1250 v_flex().children(
1251 tool_uses
1252 .into_iter()
1253 .map(|tool_use| self.render_tool_use(tool_use, cx)),
1254 ),
1255 )
1256 }),
1257 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1258 v_flex()
1259 .bg(colors.editor_background)
1260 .rounded_sm()
1261 .child(div().p_4().children(message_content)),
1262 ),
1263 };
1264
1265 v_flex()
1266 .w_full()
1267 .when(first_message, |parent| {
1268 parent.child(self.render_rules_item(cx))
1269 })
1270 .when_some(checkpoint, |parent, checkpoint| {
1271 let mut is_pending = false;
1272 let mut error = None;
1273 if let Some(last_restore_checkpoint) =
1274 self.thread.read(cx).last_restore_checkpoint()
1275 {
1276 if last_restore_checkpoint.message_id() == message_id {
1277 match last_restore_checkpoint {
1278 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1279 LastRestoreCheckpoint::Error { error: err, .. } => {
1280 error = Some(err.clone());
1281 }
1282 }
1283 }
1284 }
1285
1286 let restore_checkpoint_button =
1287 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1288 .icon(if error.is_some() {
1289 IconName::XCircle
1290 } else {
1291 IconName::Undo
1292 })
1293 .icon_size(IconSize::XSmall)
1294 .icon_position(IconPosition::Start)
1295 .icon_color(if error.is_some() {
1296 Some(Color::Error)
1297 } else {
1298 None
1299 })
1300 .label_size(LabelSize::XSmall)
1301 .disabled(is_pending)
1302 .on_click(cx.listener(move |this, _, _window, cx| {
1303 this.thread.update(cx, |thread, cx| {
1304 thread
1305 .restore_checkpoint(checkpoint.clone(), cx)
1306 .detach_and_log_err(cx);
1307 });
1308 }));
1309
1310 let restore_checkpoint_button = if is_pending {
1311 restore_checkpoint_button
1312 .with_animation(
1313 ("pulsating-restore-checkpoint-button", ix),
1314 Animation::new(Duration::from_secs(2))
1315 .repeat()
1316 .with_easing(pulsating_between(0.6, 1.)),
1317 |label, delta| label.alpha(delta),
1318 )
1319 .into_any_element()
1320 } else if let Some(error) = error {
1321 restore_checkpoint_button
1322 .tooltip(Tooltip::text(error.to_string()))
1323 .into_any_element()
1324 } else {
1325 restore_checkpoint_button.into_any_element()
1326 };
1327
1328 parent.child(
1329 h_flex()
1330 .pt_2p5()
1331 .px_2p5()
1332 .w_full()
1333 .gap_1()
1334 .child(ui::Divider::horizontal())
1335 .child(restore_checkpoint_button)
1336 .child(ui::Divider::horizontal()),
1337 )
1338 })
1339 .child(styled_message)
1340 .when(
1341 show_feedback && !self.thread.read(cx).is_generating(),
1342 |parent| {
1343 parent.child(feedback_items).when_some(
1344 self.feedback_message_editor.clone(),
1345 |parent, feedback_editor| {
1346 let focus_handle = feedback_editor.focus_handle(cx);
1347 parent.child(
1348 v_flex()
1349 .key_context("AgentFeedbackMessageEditor")
1350 .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
1351 this.feedback_message_editor = None;
1352 cx.notify();
1353 }))
1354 .on_action(cx.listener(|this, _: &menu::Confirm, _, cx| {
1355 this.submit_feedback_message(cx);
1356 cx.notify();
1357 }))
1358 .on_action(cx.listener(Self::confirm_editing_message))
1359 .mx_4()
1360 .mb_3()
1361 .p_2()
1362 .rounded_md()
1363 .border_1()
1364 .border_color(cx.theme().colors().border)
1365 .bg(cx.theme().colors().editor_background)
1366 .child(feedback_editor)
1367 .child(
1368 h_flex()
1369 .gap_1()
1370 .justify_end()
1371 .child(
1372 Button::new("dismiss-feedback-message", "Cancel")
1373 .label_size(LabelSize::Small)
1374 .key_binding(
1375 KeyBinding::for_action_in(
1376 &menu::Cancel,
1377 &focus_handle,
1378 window,
1379 cx,
1380 )
1381 .map(|kb| kb.size(rems_from_px(10.))),
1382 )
1383 .on_click(cx.listener(|this, _, _, cx| {
1384 this.feedback_message_editor = None;
1385 cx.notify();
1386 })),
1387 )
1388 .child(
1389 Button::new(
1390 "submit-feedback-message",
1391 "Share Feedback",
1392 )
1393 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
1394 .label_size(LabelSize::Small)
1395 .key_binding(
1396 KeyBinding::for_action_in(
1397 &menu::Confirm,
1398 &focus_handle,
1399 window,
1400 cx,
1401 )
1402 .map(|kb| kb.size(rems_from_px(10.))),
1403 )
1404 .on_click(cx.listener(|this, _, _, cx| {
1405 this.submit_feedback_message(cx);
1406 cx.notify();
1407 })),
1408 ),
1409 ),
1410 )
1411 },
1412 )
1413 },
1414 )
1415 .into_any()
1416 }
1417
1418 fn render_message_content(
1419 &self,
1420 message_id: MessageId,
1421 rendered_message: &RenderedMessage,
1422 has_tool_uses: bool,
1423 cx: &Context<Self>,
1424 ) -> impl IntoElement {
1425 let is_last_message = self.messages.last() == Some(&message_id);
1426 let is_generating = self.thread.read(cx).is_generating();
1427 let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses {
1428 rendered_message
1429 .segments
1430 .iter()
1431 .enumerate()
1432 .next_back()
1433 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1434 .map(|(index, _)| index)
1435 } else {
1436 None
1437 };
1438
1439 div()
1440 .text_ui(cx)
1441 .gap_2()
1442 .children(
1443 rendered_message.segments.iter().enumerate().map(
1444 |(index, segment)| match segment {
1445 RenderedMessageSegment::Thinking {
1446 content,
1447 scroll_handle,
1448 } => self
1449 .render_message_thinking_segment(
1450 message_id,
1451 index,
1452 content.clone(),
1453 &scroll_handle,
1454 Some(index) == pending_thinking_segment_index,
1455 cx,
1456 )
1457 .into_any_element(),
1458 RenderedMessageSegment::Text(markdown) => {
1459 div().child(markdown.clone()).into_any_element()
1460 }
1461 },
1462 ),
1463 )
1464 }
1465
1466 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1467 cx.theme().colors().border.opacity(0.5)
1468 }
1469
1470 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1471 cx.theme()
1472 .colors()
1473 .element_background
1474 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1475 }
1476
1477 fn render_message_thinking_segment(
1478 &self,
1479 message_id: MessageId,
1480 ix: usize,
1481 markdown: Entity<Markdown>,
1482 scroll_handle: &ScrollHandle,
1483 pending: bool,
1484 cx: &Context<Self>,
1485 ) -> impl IntoElement {
1486 let is_open = self
1487 .expanded_thinking_segments
1488 .get(&(message_id, ix))
1489 .copied()
1490 .unwrap_or_default();
1491
1492 let editor_bg = cx.theme().colors().editor_background;
1493
1494 div().pt_0p5().pb_2().child(
1495 v_flex()
1496 .rounded_lg()
1497 .border_1()
1498 .border_color(self.tool_card_border_color(cx))
1499 .child(
1500 h_flex()
1501 .group("disclosure-header")
1502 .justify_between()
1503 .py_1()
1504 .px_2()
1505 .bg(self.tool_card_header_bg(cx))
1506 .map(|this| {
1507 if pending || is_open {
1508 this.rounded_t_md()
1509 .border_b_1()
1510 .border_color(self.tool_card_border_color(cx))
1511 } else {
1512 this.rounded_md()
1513 }
1514 })
1515 .child(
1516 h_flex()
1517 .gap_1p5()
1518 .child(
1519 Icon::new(IconName::Brain)
1520 .size(IconSize::XSmall)
1521 .color(Color::Muted),
1522 )
1523 .child({
1524 if pending {
1525 Label::new("Thinking…")
1526 .size(LabelSize::Small)
1527 .buffer_font(cx)
1528 .with_animation(
1529 "pulsating-label",
1530 Animation::new(Duration::from_secs(2))
1531 .repeat()
1532 .with_easing(pulsating_between(0.4, 0.8)),
1533 |label, delta| label.alpha(delta),
1534 )
1535 .into_any_element()
1536 } else {
1537 Label::new("Thought Process")
1538 .size(LabelSize::Small)
1539 .buffer_font(cx)
1540 .into_any_element()
1541 }
1542 }),
1543 )
1544 .child(
1545 h_flex()
1546 .gap_1()
1547 .child(
1548 div().visible_on_hover("disclosure-header").child(
1549 Disclosure::new("thinking-disclosure", is_open)
1550 .opened_icon(IconName::ChevronUp)
1551 .closed_icon(IconName::ChevronDown)
1552 .on_click(cx.listener({
1553 move |this, _event, _window, _cx| {
1554 let is_open = this
1555 .expanded_thinking_segments
1556 .entry((message_id, ix))
1557 .or_insert(false);
1558
1559 *is_open = !*is_open;
1560 }
1561 })),
1562 ),
1563 )
1564 .child({
1565 let (icon_name, color, animated) = if pending {
1566 (IconName::ArrowCircle, Color::Accent, true)
1567 } else {
1568 (IconName::Check, Color::Success, false)
1569 };
1570
1571 let icon =
1572 Icon::new(icon_name).color(color).size(IconSize::Small);
1573
1574 if animated {
1575 icon.with_animation(
1576 "arrow-circle",
1577 Animation::new(Duration::from_secs(2)).repeat(),
1578 |icon, delta| {
1579 icon.transform(Transformation::rotate(percentage(
1580 delta,
1581 )))
1582 },
1583 )
1584 .into_any_element()
1585 } else {
1586 icon.into_any_element()
1587 }
1588 }),
1589 ),
1590 )
1591 .when(pending && !is_open, |this| {
1592 let gradient_overlay = div()
1593 .rounded_b_lg()
1594 .h_20()
1595 .absolute()
1596 .w_full()
1597 .bottom_0()
1598 .left_0()
1599 .bg(linear_gradient(
1600 180.,
1601 linear_color_stop(editor_bg, 1.),
1602 linear_color_stop(editor_bg.opacity(0.2), 0.),
1603 ));
1604
1605 this.child(
1606 div()
1607 .relative()
1608 .bg(editor_bg)
1609 .rounded_b_lg()
1610 .child(
1611 div()
1612 .id(("thinking-content", ix))
1613 .p_2()
1614 .h_20()
1615 .track_scroll(scroll_handle)
1616 .text_ui_sm(cx)
1617 .child(markdown.clone())
1618 .overflow_hidden(),
1619 )
1620 .child(gradient_overlay),
1621 )
1622 })
1623 .when(is_open, |this| {
1624 this.child(
1625 div()
1626 .id(("thinking-content", ix))
1627 .h_full()
1628 .p_2()
1629 .rounded_b_lg()
1630 .bg(editor_bg)
1631 .text_ui_sm(cx)
1632 .child(markdown.clone()),
1633 )
1634 }),
1635 )
1636 }
1637
1638 fn render_tool_use(
1639 &self,
1640 tool_use: ToolUse,
1641 cx: &mut Context<Self>,
1642 ) -> impl IntoElement + use<> {
1643 let is_open = self
1644 .expanded_tool_uses
1645 .get(&tool_use.id)
1646 .copied()
1647 .unwrap_or_default();
1648
1649 let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
1650
1651 let fs = self
1652 .workspace
1653 .upgrade()
1654 .map(|workspace| workspace.read(cx).app_state().fs.clone());
1655 let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
1656
1657 let status_icons = div().child(match &tool_use.status {
1658 ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
1659 let icon = Icon::new(IconName::Warning)
1660 .color(Color::Warning)
1661 .size(IconSize::Small);
1662 icon.into_any_element()
1663 }
1664 ToolUseStatus::Running => {
1665 let icon = Icon::new(IconName::ArrowCircle)
1666 .color(Color::Accent)
1667 .size(IconSize::Small);
1668 icon.with_animation(
1669 "arrow-circle",
1670 Animation::new(Duration::from_secs(2)).repeat(),
1671 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1672 )
1673 .into_any_element()
1674 }
1675 ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
1676 ToolUseStatus::Error(_) => {
1677 let icon = Icon::new(IconName::Close)
1678 .color(Color::Error)
1679 .size(IconSize::Small);
1680 icon.into_any_element()
1681 }
1682 });
1683
1684 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1685 let results_content = v_flex()
1686 .gap_1()
1687 .child(
1688 content_container()
1689 .child(
1690 Label::new("Input")
1691 .size(LabelSize::XSmall)
1692 .color(Color::Muted)
1693 .buffer_font(cx),
1694 )
1695 .child(
1696 Label::new(
1697 serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1698 )
1699 .size(LabelSize::Small)
1700 .buffer_font(cx),
1701 ),
1702 )
1703 .map(|container| match tool_use.status {
1704 ToolUseStatus::Finished(output) => container.child(
1705 content_container()
1706 .border_t_1()
1707 .border_color(self.tool_card_border_color(cx))
1708 .child(
1709 Label::new("Result")
1710 .size(LabelSize::XSmall)
1711 .color(Color::Muted)
1712 .buffer_font(cx),
1713 )
1714 .child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
1715 ),
1716 ToolUseStatus::Running => container.child(
1717 content_container().child(
1718 h_flex()
1719 .gap_1()
1720 .pb_1()
1721 .border_t_1()
1722 .border_color(self.tool_card_border_color(cx))
1723 .child(
1724 Icon::new(IconName::ArrowCircle)
1725 .size(IconSize::Small)
1726 .color(Color::Accent)
1727 .with_animation(
1728 "arrow-circle",
1729 Animation::new(Duration::from_secs(2)).repeat(),
1730 |icon, delta| {
1731 icon.transform(Transformation::rotate(percentage(
1732 delta,
1733 )))
1734 },
1735 ),
1736 )
1737 .child(
1738 Label::new("Running…")
1739 .size(LabelSize::XSmall)
1740 .color(Color::Muted)
1741 .buffer_font(cx),
1742 ),
1743 ),
1744 ),
1745 ToolUseStatus::Error(err) => container.child(
1746 content_container()
1747 .border_t_1()
1748 .border_color(self.tool_card_border_color(cx))
1749 .child(
1750 Label::new("Error")
1751 .size(LabelSize::XSmall)
1752 .color(Color::Muted)
1753 .buffer_font(cx),
1754 )
1755 .child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
1756 ),
1757 ToolUseStatus::Pending => container,
1758 ToolUseStatus::NeedsConfirmation => container.child(
1759 content_container()
1760 .border_t_1()
1761 .border_color(self.tool_card_border_color(cx))
1762 .child(
1763 Label::new("Asking Permission")
1764 .size(LabelSize::Small)
1765 .color(Color::Muted)
1766 .buffer_font(cx),
1767 ),
1768 ),
1769 });
1770
1771 let gradient_overlay = |color: Hsla| {
1772 div()
1773 .h_full()
1774 .absolute()
1775 .w_8()
1776 .bottom_0()
1777 .map(|element| {
1778 if is_status_finished {
1779 element.right_7()
1780 } else {
1781 element.right(px(46.))
1782 }
1783 })
1784 .bg(linear_gradient(
1785 90.,
1786 linear_color_stop(color, 1.),
1787 linear_color_stop(color.opacity(0.2), 0.),
1788 ))
1789 };
1790
1791 div().map(|element| {
1792 if !tool_use.needs_confirmation {
1793 element.child(
1794 v_flex()
1795 .child(
1796 h_flex()
1797 .group("disclosure-header")
1798 .relative()
1799 .gap_1p5()
1800 .justify_between()
1801 .opacity(0.8)
1802 .hover(|style| style.opacity(1.))
1803 .when(!is_status_finished, |this| this.pr_2())
1804 .child(
1805 h_flex()
1806 .id("tool-label-container")
1807 .gap_1p5()
1808 .max_w_full()
1809 .overflow_x_scroll()
1810 .child(
1811 Icon::new(tool_use.icon)
1812 .size(IconSize::XSmall)
1813 .color(Color::Muted),
1814 )
1815 .child(
1816 h_flex().pr_8().text_ui_sm(cx).children(
1817 self.rendered_tool_use_labels
1818 .get(&tool_use.id)
1819 .cloned(),
1820 ),
1821 ),
1822 )
1823 .child(
1824 h_flex()
1825 .gap_1()
1826 .child(
1827 div().visible_on_hover("disclosure-header").child(
1828 Disclosure::new("tool-use-disclosure", is_open)
1829 .opened_icon(IconName::ChevronUp)
1830 .closed_icon(IconName::ChevronDown)
1831 .on_click(cx.listener({
1832 let tool_use_id = tool_use.id.clone();
1833 move |this, _event, _window, _cx| {
1834 let is_open = this
1835 .expanded_tool_uses
1836 .entry(tool_use_id.clone())
1837 .or_insert(false);
1838
1839 *is_open = !*is_open;
1840 }
1841 })),
1842 ),
1843 )
1844 .child(status_icons),
1845 )
1846 .child(gradient_overlay(cx.theme().colors().panel_background)),
1847 )
1848 .map(|parent| {
1849 if !is_open {
1850 return parent;
1851 }
1852
1853 parent.child(
1854 v_flex()
1855 .mt_1()
1856 .border_1()
1857 .border_color(self.tool_card_border_color(cx))
1858 .bg(cx.theme().colors().editor_background)
1859 .rounded_lg()
1860 .child(results_content),
1861 )
1862 }),
1863 )
1864 } else {
1865 v_flex()
1866 .rounded_lg()
1867 .border_1()
1868 .border_color(self.tool_card_border_color(cx))
1869 .overflow_hidden()
1870 .child(
1871 h_flex()
1872 .group("disclosure-header")
1873 .relative()
1874 .justify_between()
1875 .py_1()
1876 .map(|element| {
1877 if is_status_finished {
1878 element.pl_2().pr_0p5()
1879 } else {
1880 element.px_2()
1881 }
1882 })
1883 .bg(self.tool_card_header_bg(cx))
1884 .map(|element| {
1885 if is_open {
1886 element.border_b_1().rounded_t_md()
1887 } else if needs_confirmation {
1888 element.rounded_t_md()
1889 } else {
1890 element.rounded_md()
1891 }
1892 })
1893 .border_color(self.tool_card_border_color(cx))
1894 .child(
1895 h_flex()
1896 .id("tool-label-container")
1897 .gap_1p5()
1898 .max_w_full()
1899 .overflow_x_scroll()
1900 .child(
1901 Icon::new(tool_use.icon)
1902 .size(IconSize::XSmall)
1903 .color(Color::Muted),
1904 )
1905 .child(
1906 h_flex().pr_8().text_ui_sm(cx).children(
1907 self.rendered_tool_use_labels
1908 .get(&tool_use.id)
1909 .cloned(),
1910 ),
1911 ),
1912 )
1913 .child(
1914 h_flex()
1915 .gap_1()
1916 .child(
1917 div().visible_on_hover("disclosure-header").child(
1918 Disclosure::new("tool-use-disclosure", is_open)
1919 .opened_icon(IconName::ChevronUp)
1920 .closed_icon(IconName::ChevronDown)
1921 .on_click(cx.listener({
1922 let tool_use_id = tool_use.id.clone();
1923 move |this, _event, _window, _cx| {
1924 let is_open = this
1925 .expanded_tool_uses
1926 .entry(tool_use_id.clone())
1927 .or_insert(false);
1928
1929 *is_open = !*is_open;
1930 }
1931 })),
1932 ),
1933 )
1934 .child(status_icons),
1935 )
1936 .child(gradient_overlay(self.tool_card_header_bg(cx))),
1937 )
1938 .map(|parent| {
1939 if !is_open {
1940 return parent;
1941 }
1942
1943 parent.child(
1944 v_flex()
1945 .bg(cx.theme().colors().editor_background)
1946 .map(|element| {
1947 if needs_confirmation {
1948 element.rounded_none()
1949 } else {
1950 element.rounded_b_lg()
1951 }
1952 })
1953 .child(results_content),
1954 )
1955 })
1956 .when(needs_confirmation, |this| {
1957 this.child(
1958 h_flex()
1959 .py_1()
1960 .pl_2()
1961 .pr_1()
1962 .gap_1()
1963 .justify_between()
1964 .bg(cx.theme().colors().editor_background)
1965 .border_t_1()
1966 .border_color(self.tool_card_border_color(cx))
1967 .rounded_b_lg()
1968 .child(Label::new("Action Confirmation").color(Color::Muted).size(LabelSize::Small))
1969 .child(
1970 h_flex()
1971 .gap_0p5()
1972 .child({
1973 let tool_id = tool_use.id.clone();
1974 Button::new(
1975 "always-allow-tool-action",
1976 "Always Allow",
1977 )
1978 .label_size(LabelSize::Small)
1979 .icon(IconName::CheckDouble)
1980 .icon_position(IconPosition::Start)
1981 .icon_size(IconSize::Small)
1982 .icon_color(Color::Success)
1983 .tooltip(move |window, cx| {
1984 Tooltip::with_meta(
1985 "Never ask for permission",
1986 None,
1987 "Restore the original behavior in your Agent Panel settings",
1988 window,
1989 cx,
1990 )
1991 })
1992 .on_click(cx.listener(
1993 move |this, event, window, cx| {
1994 if let Some(fs) = fs.clone() {
1995 update_settings_file::<AssistantSettings>(
1996 fs.clone(),
1997 cx,
1998 |settings, _| {
1999 settings.set_always_allow_tool_actions(true);
2000 },
2001 );
2002 }
2003 this.handle_allow_tool(
2004 tool_id.clone(),
2005 event,
2006 window,
2007 cx,
2008 )
2009 },
2010 ))
2011 })
2012 .child(ui::Divider::vertical())
2013 .child({
2014 let tool_id = tool_use.id.clone();
2015 Button::new("allow-tool-action", "Allow")
2016 .label_size(LabelSize::Small)
2017 .icon(IconName::Check)
2018 .icon_position(IconPosition::Start)
2019 .icon_size(IconSize::Small)
2020 .icon_color(Color::Success)
2021 .on_click(cx.listener(
2022 move |this, event, window, cx| {
2023 this.handle_allow_tool(
2024 tool_id.clone(),
2025 event,
2026 window,
2027 cx,
2028 )
2029 },
2030 ))
2031 })
2032 .child({
2033 let tool_id = tool_use.id.clone();
2034 let tool_name: Arc<str> = tool_use.name.into();
2035 Button::new("deny-tool", "Deny")
2036 .label_size(LabelSize::Small)
2037 .icon(IconName::Close)
2038 .icon_position(IconPosition::Start)
2039 .icon_size(IconSize::Small)
2040 .icon_color(Color::Error)
2041 .on_click(cx.listener(
2042 move |this, event, window, cx| {
2043 this.handle_deny_tool(
2044 tool_id.clone(),
2045 tool_name.clone(),
2046 event,
2047 window,
2048 cx,
2049 )
2050 },
2051 ))
2052 }),
2053 ),
2054 )
2055 })
2056 }
2057 })
2058 }
2059
2060 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
2061 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2062 else {
2063 return div().into_any();
2064 };
2065
2066 let rules_files = system_prompt_context
2067 .worktrees
2068 .iter()
2069 .filter_map(|worktree| worktree.rules_file.as_ref())
2070 .collect::<Vec<_>>();
2071
2072 let label_text = match rules_files.as_slice() {
2073 &[] => return div().into_any(),
2074 &[rules_file] => {
2075 format!("Using {:?} file", rules_file.rel_path)
2076 }
2077 rules_files => {
2078 format!("Using {} rules files", rules_files.len())
2079 }
2080 };
2081
2082 div()
2083 .pt_1()
2084 .px_2p5()
2085 .child(
2086 h_flex()
2087 .w_full()
2088 .gap_0p5()
2089 .child(
2090 h_flex()
2091 .gap_1p5()
2092 .child(
2093 Icon::new(IconName::File)
2094 .size(IconSize::XSmall)
2095 .color(Color::Disabled),
2096 )
2097 .child(
2098 Label::new(label_text)
2099 .size(LabelSize::XSmall)
2100 .color(Color::Muted)
2101 .buffer_font(cx),
2102 ),
2103 )
2104 .child(
2105 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
2106 .shape(ui::IconButtonShape::Square)
2107 .icon_size(IconSize::XSmall)
2108 .icon_color(Color::Ignored)
2109 .on_click(cx.listener(Self::handle_open_rules))
2110 .tooltip(Tooltip::text("View Rules")),
2111 ),
2112 )
2113 .into_any()
2114 }
2115
2116 fn handle_allow_tool(
2117 &mut self,
2118 tool_use_id: LanguageModelToolUseId,
2119 _: &ClickEvent,
2120 _window: &mut Window,
2121 cx: &mut Context<Self>,
2122 ) {
2123 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
2124 .thread
2125 .read(cx)
2126 .pending_tool(&tool_use_id)
2127 .map(|tool_use| tool_use.status.clone())
2128 {
2129 self.thread.update(cx, |thread, cx| {
2130 thread.run_tool(
2131 c.tool_use_id.clone(),
2132 c.ui_text.clone(),
2133 c.input.clone(),
2134 &c.messages,
2135 c.tool.clone(),
2136 cx,
2137 );
2138 });
2139 }
2140 }
2141
2142 fn handle_deny_tool(
2143 &mut self,
2144 tool_use_id: LanguageModelToolUseId,
2145 tool_name: Arc<str>,
2146 _: &ClickEvent,
2147 _window: &mut Window,
2148 cx: &mut Context<Self>,
2149 ) {
2150 self.thread.update(cx, |thread, cx| {
2151 thread.deny_tool_use(tool_use_id, tool_name, cx);
2152 });
2153 }
2154
2155 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
2156 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2157 else {
2158 return;
2159 };
2160
2161 let abs_paths = system_prompt_context
2162 .worktrees
2163 .iter()
2164 .flat_map(|worktree| worktree.rules_file.as_ref())
2165 .map(|rules_file| rules_file.abs_path.to_path_buf())
2166 .collect::<Vec<_>>();
2167
2168 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
2169 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
2170 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
2171 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
2172 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
2173 }) {
2174 task.detach();
2175 }
2176 }
2177
2178 fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
2179 for window in self.notifications.drain(..) {
2180 window
2181 .update(cx, |_, window, _| {
2182 window.remove_window();
2183 })
2184 .ok();
2185
2186 self.notification_subscriptions.remove(&window);
2187 }
2188 }
2189
2190 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
2191 div()
2192 .occlude()
2193 .id("active-thread-scrollbar")
2194 .on_mouse_move(cx.listener(|_, _, _, cx| {
2195 cx.notify();
2196 cx.stop_propagation()
2197 }))
2198 .on_hover(|_, _, cx| {
2199 cx.stop_propagation();
2200 })
2201 .on_any_mouse_down(|_, _, cx| {
2202 cx.stop_propagation();
2203 })
2204 .on_mouse_up(
2205 MouseButton::Left,
2206 cx.listener(|_, _, _, cx| {
2207 cx.stop_propagation();
2208 }),
2209 )
2210 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2211 cx.notify();
2212 }))
2213 .h_full()
2214 .absolute()
2215 .right_1()
2216 .top_1()
2217 .bottom_0()
2218 .w(px(12.))
2219 .cursor_default()
2220 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
2221 }
2222}
2223
2224impl Render for ActiveThread {
2225 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2226 v_flex()
2227 .size_full()
2228 .relative()
2229 .child(list(self.list_state.clone()).flex_grow())
2230 .child(self.render_vertical_scrollbar(cx))
2231 }
2232}
2233
2234pub(crate) fn open_context(
2235 id: ContextId,
2236 context_store: Entity<ContextStore>,
2237 workspace: Entity<Workspace>,
2238 window: &mut Window,
2239 cx: &mut App,
2240) {
2241 let Some(context) = context_store.read(cx).context_for_id(id) else {
2242 return;
2243 };
2244
2245 match context {
2246 AssistantContext::File(file_context) => {
2247 if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
2248 {
2249 workspace.update(cx, |workspace, cx| {
2250 workspace
2251 .open_path(project_path, None, true, window, cx)
2252 .detach_and_log_err(cx);
2253 });
2254 }
2255 }
2256 AssistantContext::Directory(directory_context) => {
2257 let path = directory_context.project_path.clone();
2258 workspace.update(cx, |workspace, cx| {
2259 workspace.project().update(cx, |project, cx| {
2260 if let Some(entry) = project.entry_for_path(&path, cx) {
2261 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2262 }
2263 })
2264 })
2265 }
2266 AssistantContext::Symbol(symbol_context) => {
2267 if let Some(project_path) = symbol_context
2268 .context_symbol
2269 .buffer
2270 .read(cx)
2271 .project_path(cx)
2272 {
2273 let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
2274 let target_position = symbol_context
2275 .context_symbol
2276 .id
2277 .range
2278 .start
2279 .to_point(&snapshot);
2280
2281 let open_task = workspace.update(cx, |workspace, cx| {
2282 workspace.open_path(project_path, None, true, window, cx)
2283 });
2284 window
2285 .spawn(cx, async move |cx| {
2286 if let Some(active_editor) = open_task
2287 .await
2288 .log_err()
2289 .and_then(|item| item.downcast::<Editor>())
2290 {
2291 active_editor
2292 .downgrade()
2293 .update_in(cx, |editor, window, cx| {
2294 editor.go_to_singleton_buffer_point(
2295 target_position,
2296 window,
2297 cx,
2298 );
2299 })
2300 .log_err();
2301 }
2302 })
2303 .detach();
2304 }
2305 }
2306 AssistantContext::FetchedUrl(fetched_url_context) => {
2307 cx.open_url(&fetched_url_context.url);
2308 }
2309 AssistantContext::Thread(thread_context) => {
2310 let thread_id = thread_context.thread.read(cx).id().clone();
2311 workspace.update(cx, |workspace, cx| {
2312 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
2313 panel.update(cx, |panel, cx| {
2314 panel
2315 .open_thread(&thread_id, window, cx)
2316 .detach_and_log_err(cx)
2317 });
2318 }
2319 })
2320 }
2321 }
2322}