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