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