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