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