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