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