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