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 .px_2p5()
1027 .w_full()
1028 .gap_1()
1029 .child(ui::Divider::horizontal())
1030 .child(restore_checkpoint_button)
1031 .child(ui::Divider::horizontal()),
1032 )
1033 })
1034 .child(styled_message)
1035 .when(
1036 is_last_message && !self.thread.read(cx).is_generating(),
1037 |parent| parent.child(feedback_items),
1038 )
1039 .into_any()
1040 }
1041
1042 fn render_message_content(
1043 &self,
1044 message_id: MessageId,
1045 rendered_message: &RenderedMessage,
1046 cx: &Context<Self>,
1047 ) -> impl IntoElement {
1048 let pending_thinking_segment_index = rendered_message
1049 .segments
1050 .iter()
1051 .enumerate()
1052 .last()
1053 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1054 .map(|(index, _)| index);
1055
1056 div()
1057 .text_ui(cx)
1058 .gap_2()
1059 .children(
1060 rendered_message.segments.iter().enumerate().map(
1061 |(index, segment)| match segment {
1062 RenderedMessageSegment::Thinking {
1063 content,
1064 scroll_handle,
1065 } => self
1066 .render_message_thinking_segment(
1067 message_id,
1068 index,
1069 content.clone(),
1070 &scroll_handle,
1071 Some(index) == pending_thinking_segment_index,
1072 cx,
1073 )
1074 .into_any_element(),
1075 RenderedMessageSegment::Text(markdown) => {
1076 div().child(markdown.clone()).into_any_element()
1077 }
1078 },
1079 ),
1080 )
1081 }
1082
1083 fn render_message_thinking_segment(
1084 &self,
1085 message_id: MessageId,
1086 ix: usize,
1087 markdown: Entity<Markdown>,
1088 scroll_handle: &ScrollHandle,
1089 pending: bool,
1090 cx: &Context<Self>,
1091 ) -> impl IntoElement {
1092 let is_open = self
1093 .expanded_thinking_segments
1094 .get(&(message_id, ix))
1095 .copied()
1096 .unwrap_or_default();
1097
1098 let lighter_border = cx.theme().colors().border.opacity(0.5);
1099 let editor_bg = cx.theme().colors().editor_background;
1100
1101 div().py_2().child(
1102 v_flex()
1103 .rounded_lg()
1104 .border_1()
1105 .border_color(lighter_border)
1106 .child(
1107 h_flex()
1108 .group("disclosure-header")
1109 .justify_between()
1110 .py_1()
1111 .px_2()
1112 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
1113 .map(|this| {
1114 if pending || is_open {
1115 this.rounded_t_md()
1116 .border_b_1()
1117 .border_color(lighter_border)
1118 } else {
1119 this.rounded_md()
1120 }
1121 })
1122 .child(
1123 h_flex()
1124 .gap_1p5()
1125 .child(
1126 Icon::new(IconName::Brain)
1127 .size(IconSize::XSmall)
1128 .color(Color::Muted),
1129 )
1130 .child({
1131 if pending {
1132 Label::new("Thinking…")
1133 .size(LabelSize::Small)
1134 .buffer_font(cx)
1135 .with_animation(
1136 "pulsating-label",
1137 Animation::new(Duration::from_secs(2))
1138 .repeat()
1139 .with_easing(pulsating_between(0.4, 0.8)),
1140 |label, delta| label.alpha(delta),
1141 )
1142 .into_any_element()
1143 } else {
1144 Label::new("Thought Process")
1145 .size(LabelSize::Small)
1146 .buffer_font(cx)
1147 .into_any_element()
1148 }
1149 }),
1150 )
1151 .child(
1152 h_flex()
1153 .gap_1()
1154 .child(
1155 div().visible_on_hover("disclosure-header").child(
1156 Disclosure::new("thinking-disclosure", is_open)
1157 .opened_icon(IconName::ChevronUp)
1158 .closed_icon(IconName::ChevronDown)
1159 .on_click(cx.listener({
1160 move |this, _event, _window, _cx| {
1161 let is_open = this
1162 .expanded_thinking_segments
1163 .entry((message_id, ix))
1164 .or_insert(false);
1165
1166 *is_open = !*is_open;
1167 }
1168 })),
1169 ),
1170 )
1171 .child({
1172 let (icon_name, color, animated) = if pending {
1173 (IconName::ArrowCircle, Color::Accent, true)
1174 } else {
1175 (IconName::Check, Color::Success, false)
1176 };
1177
1178 let icon =
1179 Icon::new(icon_name).color(color).size(IconSize::Small);
1180
1181 if animated {
1182 icon.with_animation(
1183 "arrow-circle",
1184 Animation::new(Duration::from_secs(2)).repeat(),
1185 |icon, delta| {
1186 icon.transform(Transformation::rotate(percentage(
1187 delta,
1188 )))
1189 },
1190 )
1191 .into_any_element()
1192 } else {
1193 icon.into_any_element()
1194 }
1195 }),
1196 ),
1197 )
1198 .when(pending && !is_open, |this| {
1199 let gradient_overlay = div()
1200 .rounded_b_lg()
1201 .h_20()
1202 .absolute()
1203 .w_full()
1204 .bottom_0()
1205 .left_0()
1206 .bg(linear_gradient(
1207 180.,
1208 linear_color_stop(editor_bg, 1.),
1209 linear_color_stop(editor_bg.opacity(0.2), 0.),
1210 ));
1211
1212 this.child(
1213 div()
1214 .relative()
1215 .bg(editor_bg)
1216 .rounded_b_lg()
1217 .child(
1218 div()
1219 .id(("thinking-content", ix))
1220 .p_2()
1221 .h_20()
1222 .track_scroll(scroll_handle)
1223 .text_ui_sm(cx)
1224 .child(markdown.clone())
1225 .overflow_hidden(),
1226 )
1227 .child(gradient_overlay),
1228 )
1229 })
1230 .when(is_open, |this| {
1231 this.child(
1232 div()
1233 .id(("thinking-content", ix))
1234 .h_full()
1235 .p_2()
1236 .rounded_b_lg()
1237 .bg(editor_bg)
1238 .text_ui_sm(cx)
1239 .child(markdown.clone()),
1240 )
1241 }),
1242 )
1243 }
1244
1245 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
1246 let is_open = self
1247 .expanded_tool_uses
1248 .get(&tool_use.id)
1249 .copied()
1250 .unwrap_or_default();
1251
1252 let lighter_border = cx.theme().colors().border.opacity(0.5);
1253
1254 let tool_icon = match tool_use.name.as_ref() {
1255 "bash" => IconName::Terminal,
1256 "copy-path" => IconName::Clipboard,
1257 "delete-path" => IconName::Trash,
1258 "diagnostics" => IconName::Warning,
1259 "edit-files" => IconName::Pencil,
1260 "fetch" => IconName::Globe,
1261 "list-directory" => IconName::Folder,
1262 "move-path" => IconName::ArrowRightLeft,
1263 "now" => IconName::Info,
1264 "path-search" => IconName::SearchCode,
1265 "read-file" => IconName::Eye,
1266 "regex-search" => IconName::Regex,
1267 "thinking" => IconName::Brain,
1268 _ => IconName::Cog,
1269 };
1270
1271 div().py_2().child(
1272 v_flex()
1273 .rounded_lg()
1274 .border_1()
1275 .border_color(lighter_border)
1276 .overflow_hidden()
1277 .child(
1278 h_flex()
1279 .group("disclosure-header")
1280 .justify_between()
1281 .py_1()
1282 .px_2()
1283 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
1284 .map(|element| {
1285 if is_open {
1286 element.border_b_1().rounded_t_md()
1287 } else {
1288 element.rounded_md()
1289 }
1290 })
1291 .border_color(lighter_border)
1292 .child(
1293 h_flex()
1294 .gap_1p5()
1295 .child(
1296 Icon::new(tool_icon)
1297 .size(IconSize::XSmall)
1298 .color(Color::Muted),
1299 )
1300 .child(
1301 div()
1302 .text_ui_sm(cx)
1303 .children(
1304 self.rendered_tool_use_labels
1305 .get(&tool_use.id)
1306 .cloned(),
1307 )
1308 .truncate(),
1309 ),
1310 )
1311 .child(
1312 h_flex()
1313 .gap_1()
1314 .child(
1315 div().visible_on_hover("disclosure-header").child(
1316 Disclosure::new("tool-use-disclosure", is_open)
1317 .opened_icon(IconName::ChevronUp)
1318 .closed_icon(IconName::ChevronDown)
1319 .on_click(cx.listener({
1320 let tool_use_id = tool_use.id.clone();
1321 move |this, _event, _window, _cx| {
1322 let is_open = this
1323 .expanded_tool_uses
1324 .entry(tool_use_id.clone())
1325 .or_insert(false);
1326
1327 *is_open = !*is_open;
1328 }
1329 })),
1330 ),
1331 )
1332 .child({
1333 let (icon_name, color, animated) = match &tool_use.status {
1334 ToolUseStatus::Pending
1335 | ToolUseStatus::NeedsConfirmation => {
1336 (IconName::Warning, Color::Warning, false)
1337 }
1338 ToolUseStatus::Running => {
1339 (IconName::ArrowCircle, Color::Accent, true)
1340 }
1341 ToolUseStatus::Finished(_) => {
1342 (IconName::Check, Color::Success, false)
1343 }
1344 ToolUseStatus::Error(_) => {
1345 (IconName::Close, Color::Error, false)
1346 }
1347 };
1348
1349 let icon =
1350 Icon::new(icon_name).color(color).size(IconSize::Small);
1351
1352 if animated {
1353 icon.with_animation(
1354 "arrow-circle",
1355 Animation::new(Duration::from_secs(2)).repeat(),
1356 |icon, delta| {
1357 icon.transform(Transformation::rotate(percentage(
1358 delta,
1359 )))
1360 },
1361 )
1362 .into_any_element()
1363 } else {
1364 icon.into_any_element()
1365 }
1366 }),
1367 ),
1368 )
1369 .map(|parent| {
1370 if !is_open {
1371 return parent;
1372 }
1373
1374 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1375
1376 parent.child(
1377 v_flex()
1378 .gap_1()
1379 .bg(cx.theme().colors().editor_background)
1380 .rounded_b_lg()
1381 .child(
1382 content_container()
1383 .border_b_1()
1384 .border_color(lighter_border)
1385 .child(
1386 Label::new("Input")
1387 .size(LabelSize::XSmall)
1388 .color(Color::Muted)
1389 .buffer_font(cx),
1390 )
1391 .child(
1392 Label::new(
1393 serde_json::to_string_pretty(&tool_use.input)
1394 .unwrap_or_default(),
1395 )
1396 .size(LabelSize::Small)
1397 .buffer_font(cx),
1398 ),
1399 )
1400 .map(|container| match tool_use.status {
1401 ToolUseStatus::Finished(output) => container.child(
1402 content_container()
1403 .child(
1404 Label::new("Result")
1405 .size(LabelSize::XSmall)
1406 .color(Color::Muted)
1407 .buffer_font(cx),
1408 )
1409 .child(
1410 Label::new(output)
1411 .size(LabelSize::Small)
1412 .buffer_font(cx),
1413 ),
1414 ),
1415 ToolUseStatus::Running => container.child(
1416 content_container().child(
1417 h_flex()
1418 .gap_1()
1419 .pb_1()
1420 .child(
1421 Icon::new(IconName::ArrowCircle)
1422 .size(IconSize::Small)
1423 .color(Color::Accent)
1424 .with_animation(
1425 "arrow-circle",
1426 Animation::new(Duration::from_secs(2))
1427 .repeat(),
1428 |icon, delta| {
1429 icon.transform(Transformation::rotate(
1430 percentage(delta),
1431 ))
1432 },
1433 ),
1434 )
1435 .child(
1436 Label::new("Running…")
1437 .size(LabelSize::XSmall)
1438 .color(Color::Muted)
1439 .buffer_font(cx),
1440 ),
1441 ),
1442 ),
1443 ToolUseStatus::Error(err) => container.child(
1444 content_container()
1445 .child(
1446 Label::new("Error")
1447 .size(LabelSize::XSmall)
1448 .color(Color::Muted)
1449 .buffer_font(cx),
1450 )
1451 .child(
1452 Label::new(err).size(LabelSize::Small).buffer_font(cx),
1453 ),
1454 ),
1455 ToolUseStatus::Pending => container,
1456 ToolUseStatus::NeedsConfirmation => container.child(
1457 content_container().child(
1458 Label::new("Asking Permission")
1459 .size(LabelSize::Small)
1460 .color(Color::Muted)
1461 .buffer_font(cx),
1462 ),
1463 ),
1464 }),
1465 )
1466 }),
1467 )
1468 }
1469
1470 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1471 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1472 else {
1473 return div().into_any();
1474 };
1475
1476 let rules_files = system_prompt_context
1477 .worktrees
1478 .iter()
1479 .filter_map(|worktree| worktree.rules_file.as_ref())
1480 .collect::<Vec<_>>();
1481
1482 let label_text = match rules_files.as_slice() {
1483 &[] => return div().into_any(),
1484 &[rules_file] => {
1485 format!("Using {:?} file", rules_file.rel_path)
1486 }
1487 rules_files => {
1488 format!("Using {} rules files", rules_files.len())
1489 }
1490 };
1491
1492 div()
1493 .pt_1()
1494 .px_2p5()
1495 .child(
1496 h_flex()
1497 .w_full()
1498 .gap_0p5()
1499 .child(
1500 h_flex()
1501 .gap_1p5()
1502 .child(
1503 Icon::new(IconName::File)
1504 .size(IconSize::XSmall)
1505 .color(Color::Disabled),
1506 )
1507 .child(
1508 Label::new(label_text)
1509 .size(LabelSize::XSmall)
1510 .color(Color::Muted)
1511 .buffer_font(cx),
1512 ),
1513 )
1514 .child(
1515 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1516 .shape(ui::IconButtonShape::Square)
1517 .icon_size(IconSize::XSmall)
1518 .icon_color(Color::Ignored)
1519 .on_click(cx.listener(Self::handle_open_rules))
1520 .tooltip(Tooltip::text("View Rules")),
1521 ),
1522 )
1523 .into_any()
1524 }
1525
1526 fn handle_allow_tool(
1527 &mut self,
1528 tool_use_id: LanguageModelToolUseId,
1529 _: &ClickEvent,
1530 _window: &mut Window,
1531 cx: &mut Context<Self>,
1532 ) {
1533 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
1534 .thread
1535 .read(cx)
1536 .pending_tool(&tool_use_id)
1537 .map(|tool_use| tool_use.status.clone())
1538 {
1539 self.thread.update(cx, |thread, cx| {
1540 thread.run_tool(
1541 c.tool_use_id.clone(),
1542 c.ui_text.clone(),
1543 c.input.clone(),
1544 &c.messages,
1545 c.tool.clone(),
1546 cx,
1547 );
1548 });
1549 }
1550 }
1551
1552 fn handle_deny_tool(
1553 &mut self,
1554 tool_use_id: LanguageModelToolUseId,
1555 _: &ClickEvent,
1556 _window: &mut Window,
1557 cx: &mut Context<Self>,
1558 ) {
1559 self.thread.update(cx, |thread, cx| {
1560 thread.deny_tool_use(tool_use_id, cx);
1561 });
1562 }
1563
1564 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1565 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1566 else {
1567 return;
1568 };
1569
1570 let abs_paths = system_prompt_context
1571 .worktrees
1572 .iter()
1573 .flat_map(|worktree| worktree.rules_file.as_ref())
1574 .map(|rules_file| rules_file.abs_path.to_path_buf())
1575 .collect::<Vec<_>>();
1576
1577 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1578 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1579 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1580 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1581 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1582 }) {
1583 task.detach();
1584 }
1585 }
1586
1587 fn render_confirmations<'a>(
1588 &'a mut self,
1589 cx: &'a mut Context<Self>,
1590 ) -> impl Iterator<Item = AnyElement> + 'a {
1591 let thread = self.thread.read(cx);
1592
1593 thread
1594 .tools_needing_confirmation()
1595 .map(|tool| {
1596 div()
1597 .m_3()
1598 .p_2()
1599 .bg(cx.theme().colors().editor_background)
1600 .border_1()
1601 .border_color(cx.theme().colors().border)
1602 .rounded_lg()
1603 .child(
1604 v_flex()
1605 .gap_1()
1606 .child(
1607 v_flex()
1608 .gap_0p5()
1609 .child(
1610 Label::new("The agent wants to run this action:")
1611 .color(Color::Muted),
1612 )
1613 .child(div().p_3().child(Label::new(&tool.ui_text))),
1614 )
1615 .child(
1616 h_flex()
1617 .gap_1()
1618 .child({
1619 let tool_id = tool.id.clone();
1620 Button::new("allow-tool-action", "Allow").on_click(
1621 cx.listener(move |this, event, window, cx| {
1622 this.handle_allow_tool(
1623 tool_id.clone(),
1624 event,
1625 window,
1626 cx,
1627 )
1628 }),
1629 )
1630 })
1631 .child({
1632 let tool_id = tool.id.clone();
1633 Button::new("deny-tool", "Deny").on_click(cx.listener(
1634 move |this, event, window, cx| {
1635 this.handle_deny_tool(
1636 tool_id.clone(),
1637 event,
1638 window,
1639 cx,
1640 )
1641 },
1642 ))
1643 }),
1644 )
1645 .child(
1646 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.")
1647 .color(Color::Muted)
1648 .size(LabelSize::Small),
1649 ),
1650 )
1651 .into_any()
1652 })
1653 }
1654}
1655
1656impl Render for ActiveThread {
1657 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1658 v_flex()
1659 .size_full()
1660 .child(list(self.list_state.clone()).flex_grow())
1661 .children(self.render_confirmations(cx))
1662 }
1663}