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