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