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