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