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