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 let has_tool_uses = !tool_uses.is_empty();
933
934 // Don't render user messages that are just there for returning tool results.
935 if message.role == Role::User && thread.message_has_tool_results(message_id) {
936 return Empty.into_any();
937 }
938
939 let allow_editing_message =
940 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
941
942 let edit_message_editor = self
943 .editing_message
944 .as_ref()
945 .filter(|(id, _)| *id == message_id)
946 .map(|(_, state)| state.editor.clone());
947
948 let first_message = ix == 0;
949 let show_feedback = ix == self.messages.len() - 1 && message.role != Role::User;
950
951 let colors = cx.theme().colors();
952 let active_color = colors.element_active;
953 let editor_bg_color = colors.editor_background;
954 let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
955
956 let feedback_container = h_flex().pt_2().pb_4().px_4().gap_1().justify_between();
957 let feedback_items = match self.thread.read(cx).feedback() {
958 Some(feedback) => feedback_container
959 .child(
960 Label::new(match feedback {
961 ThreadFeedback::Positive => "Thanks for your feedback!",
962 ThreadFeedback::Negative => {
963 "We appreciate your feedback and will use it to improve."
964 }
965 })
966 .color(Color::Muted)
967 .size(LabelSize::XSmall),
968 )
969 .child(
970 h_flex()
971 .gap_1()
972 .child(
973 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
974 .icon_size(IconSize::XSmall)
975 .icon_color(match feedback {
976 ThreadFeedback::Positive => Color::Accent,
977 ThreadFeedback::Negative => Color::Ignored,
978 })
979 .shape(ui::IconButtonShape::Square)
980 .tooltip(Tooltip::text("Helpful Response"))
981 .on_click(cx.listener(move |this, _, window, cx| {
982 this.handle_feedback_click(
983 ThreadFeedback::Positive,
984 window,
985 cx,
986 );
987 })),
988 )
989 .child(
990 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
991 .icon_size(IconSize::XSmall)
992 .icon_color(match feedback {
993 ThreadFeedback::Positive => Color::Ignored,
994 ThreadFeedback::Negative => Color::Accent,
995 })
996 .shape(ui::IconButtonShape::Square)
997 .tooltip(Tooltip::text("Not Helpful"))
998 .on_click(cx.listener(move |this, _, window, cx| {
999 this.handle_feedback_click(
1000 ThreadFeedback::Negative,
1001 window,
1002 cx,
1003 );
1004 })),
1005 ),
1006 )
1007 .into_any_element(),
1008 None => feedback_container
1009 .child(
1010 Label::new(
1011 "Rating the thread sends all of your current conversation to the Zed team.",
1012 )
1013 .color(Color::Muted)
1014 .size(LabelSize::XSmall),
1015 )
1016 .child(
1017 h_flex()
1018 .gap_1()
1019 .child(
1020 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
1021 .icon_size(IconSize::XSmall)
1022 .icon_color(Color::Ignored)
1023 .shape(ui::IconButtonShape::Square)
1024 .tooltip(Tooltip::text("Helpful Response"))
1025 .on_click(cx.listener(move |this, _, window, cx| {
1026 this.handle_feedback_click(
1027 ThreadFeedback::Positive,
1028 window,
1029 cx,
1030 );
1031 })),
1032 )
1033 .child(
1034 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
1035 .icon_size(IconSize::XSmall)
1036 .icon_color(Color::Ignored)
1037 .shape(ui::IconButtonShape::Square)
1038 .tooltip(Tooltip::text("Not Helpful"))
1039 .on_click(cx.listener(move |this, _, window, cx| {
1040 this.handle_feedback_click(
1041 ThreadFeedback::Negative,
1042 window,
1043 cx,
1044 );
1045 })),
1046 ),
1047 )
1048 .into_any_element(),
1049 };
1050
1051 let message_content = v_flex()
1052 .gap_1p5()
1053 .child(
1054 if let Some(edit_message_editor) = edit_message_editor.clone() {
1055 div()
1056 .key_context("EditMessageEditor")
1057 .on_action(cx.listener(Self::cancel_editing_message))
1058 .on_action(cx.listener(Self::confirm_editing_message))
1059 .min_h_6()
1060 .child(edit_message_editor)
1061 } else {
1062 div()
1063 .min_h_6()
1064 .text_ui(cx)
1065 .child(self.render_message_content(
1066 message_id,
1067 rendered_message,
1068 has_tool_uses,
1069 cx,
1070 ))
1071 },
1072 )
1073 .when(!context.is_empty(), |parent| {
1074 parent.child(
1075 h_flex()
1076 .flex_wrap()
1077 .gap_1()
1078 .children(context.into_iter().map(|context| {
1079 let context_id = context.id();
1080 ContextPill::added(AddedContext::new(context, cx), false, false, None)
1081 .on_click(Rc::new(cx.listener({
1082 let workspace = workspace.clone();
1083 let context_store = context_store.clone();
1084 move |_, _, window, cx| {
1085 if let Some(workspace) = workspace.upgrade() {
1086 open_context(
1087 context_id,
1088 context_store.clone(),
1089 workspace,
1090 window,
1091 cx,
1092 );
1093 cx.notify();
1094 }
1095 }
1096 })))
1097 })),
1098 )
1099 });
1100
1101 let styled_message = match message.role {
1102 Role::User => v_flex()
1103 .id(("message-container", ix))
1104 .map(|this| {
1105 if first_message {
1106 this.pt_2()
1107 } else {
1108 this.pt_4()
1109 }
1110 })
1111 .pb_4()
1112 .pl_2()
1113 .pr_2p5()
1114 .child(
1115 v_flex()
1116 .bg(colors.editor_background)
1117 .rounded_lg()
1118 .border_1()
1119 .border_color(colors.border)
1120 .shadow_md()
1121 .child(
1122 h_flex()
1123 .py_1()
1124 .pl_2()
1125 .pr_1()
1126 .bg(bg_user_message_header)
1127 .border_b_1()
1128 .border_color(colors.border)
1129 .justify_between()
1130 .rounded_t_md()
1131 .child(
1132 h_flex()
1133 .gap_1p5()
1134 .child(
1135 Icon::new(IconName::PersonCircle)
1136 .size(IconSize::XSmall)
1137 .color(Color::Muted),
1138 )
1139 .child(
1140 Label::new("You")
1141 .size(LabelSize::Small)
1142 .color(Color::Muted),
1143 ),
1144 )
1145 .child(
1146 h_flex()
1147 // DL: To double-check whether we want to fully remove
1148 // the editing feature from meassages. Checkpoint sort of
1149 // solve the same problem.
1150 .invisible()
1151 .gap_1()
1152 .when_some(
1153 edit_message_editor.clone(),
1154 |this, edit_message_editor| {
1155 let focus_handle =
1156 edit_message_editor.focus_handle(cx);
1157 this.child(
1158 Button::new("cancel-edit-message", "Cancel")
1159 .label_size(LabelSize::Small)
1160 .key_binding(
1161 KeyBinding::for_action_in(
1162 &menu::Cancel,
1163 &focus_handle,
1164 window,
1165 cx,
1166 )
1167 .map(|kb| kb.size(rems_from_px(12.))),
1168 )
1169 .on_click(
1170 cx.listener(Self::handle_cancel_click),
1171 ),
1172 )
1173 .child(
1174 Button::new(
1175 "confirm-edit-message",
1176 "Regenerate",
1177 )
1178 .label_size(LabelSize::Small)
1179 .key_binding(
1180 KeyBinding::for_action_in(
1181 &menu::Confirm,
1182 &focus_handle,
1183 window,
1184 cx,
1185 )
1186 .map(|kb| kb.size(rems_from_px(12.))),
1187 )
1188 .on_click(
1189 cx.listener(Self::handle_regenerate_click),
1190 ),
1191 )
1192 },
1193 )
1194 .when(
1195 edit_message_editor.is_none() && allow_editing_message,
1196 |this| {
1197 this.child(
1198 Button::new("edit-message", "Edit")
1199 .label_size(LabelSize::Small)
1200 .on_click(cx.listener({
1201 let message_segments =
1202 message.segments.clone();
1203 move |this, _, window, cx| {
1204 this.start_editing_message(
1205 message_id,
1206 &message_segments,
1207 window,
1208 cx,
1209 );
1210 }
1211 })),
1212 )
1213 },
1214 ),
1215 ),
1216 )
1217 .child(div().p_2().child(message_content)),
1218 ),
1219 Role::Assistant => v_flex()
1220 .id(("message-container", ix))
1221 .ml_2()
1222 .pl_2()
1223 .pr_4()
1224 .border_l_1()
1225 .border_color(cx.theme().colors().border_variant)
1226 .child(message_content)
1227 .when(!tool_uses.is_empty(), |parent| {
1228 parent.child(
1229 v_flex().children(
1230 tool_uses
1231 .into_iter()
1232 .map(|tool_use| self.render_tool_use(tool_use, cx)),
1233 ),
1234 )
1235 }),
1236 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1237 v_flex()
1238 .bg(colors.editor_background)
1239 .rounded_sm()
1240 .child(div().p_4().child(message_content)),
1241 ),
1242 };
1243
1244 v_flex()
1245 .w_full()
1246 .when(first_message, |parent| {
1247 parent.child(self.render_rules_item(cx))
1248 })
1249 .when_some(checkpoint, |parent, checkpoint| {
1250 let mut is_pending = false;
1251 let mut error = None;
1252 if let Some(last_restore_checkpoint) =
1253 self.thread.read(cx).last_restore_checkpoint()
1254 {
1255 if last_restore_checkpoint.message_id() == message_id {
1256 match last_restore_checkpoint {
1257 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1258 LastRestoreCheckpoint::Error { error: err, .. } => {
1259 error = Some(err.clone());
1260 }
1261 }
1262 }
1263 }
1264
1265 let restore_checkpoint_button =
1266 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1267 .icon(if error.is_some() {
1268 IconName::XCircle
1269 } else {
1270 IconName::Undo
1271 })
1272 .icon_size(IconSize::XSmall)
1273 .icon_position(IconPosition::Start)
1274 .icon_color(if error.is_some() {
1275 Some(Color::Error)
1276 } else {
1277 None
1278 })
1279 .label_size(LabelSize::XSmall)
1280 .disabled(is_pending)
1281 .on_click(cx.listener(move |this, _, _window, cx| {
1282 this.thread.update(cx, |thread, cx| {
1283 thread
1284 .restore_checkpoint(checkpoint.clone(), cx)
1285 .detach_and_log_err(cx);
1286 });
1287 }));
1288
1289 let restore_checkpoint_button = if is_pending {
1290 restore_checkpoint_button
1291 .with_animation(
1292 ("pulsating-restore-checkpoint-button", ix),
1293 Animation::new(Duration::from_secs(2))
1294 .repeat()
1295 .with_easing(pulsating_between(0.6, 1.)),
1296 |label, delta| label.alpha(delta),
1297 )
1298 .into_any_element()
1299 } else if let Some(error) = error {
1300 restore_checkpoint_button
1301 .tooltip(Tooltip::text(error.to_string()))
1302 .into_any_element()
1303 } else {
1304 restore_checkpoint_button.into_any_element()
1305 };
1306
1307 parent.child(
1308 h_flex()
1309 .pt_2p5()
1310 .px_2p5()
1311 .w_full()
1312 .gap_1()
1313 .child(ui::Divider::horizontal())
1314 .child(restore_checkpoint_button)
1315 .child(ui::Divider::horizontal()),
1316 )
1317 })
1318 .child(styled_message)
1319 .when(
1320 show_feedback && !self.thread.read(cx).is_generating(),
1321 |parent| parent.child(feedback_items),
1322 )
1323 .into_any()
1324 }
1325
1326 fn render_message_content(
1327 &self,
1328 message_id: MessageId,
1329 rendered_message: &RenderedMessage,
1330 has_tool_uses: bool,
1331 cx: &Context<Self>,
1332 ) -> impl IntoElement {
1333 let is_last_message = self.messages.last() == Some(&message_id);
1334 let pending_thinking_segment_index = if is_last_message && !has_tool_uses {
1335 rendered_message
1336 .segments
1337 .iter()
1338 .enumerate()
1339 .last()
1340 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1341 .map(|(index, _)| index)
1342 } else {
1343 None
1344 };
1345
1346 div()
1347 .text_ui(cx)
1348 .gap_2()
1349 .children(
1350 rendered_message.segments.iter().enumerate().map(
1351 |(index, segment)| match segment {
1352 RenderedMessageSegment::Thinking {
1353 content,
1354 scroll_handle,
1355 } => self
1356 .render_message_thinking_segment(
1357 message_id,
1358 index,
1359 content.clone(),
1360 &scroll_handle,
1361 Some(index) == pending_thinking_segment_index,
1362 cx,
1363 )
1364 .into_any_element(),
1365 RenderedMessageSegment::Text(markdown) => {
1366 div().child(markdown.clone()).into_any_element()
1367 }
1368 },
1369 ),
1370 )
1371 }
1372
1373 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1374 cx.theme().colors().border.opacity(0.5)
1375 }
1376
1377 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1378 cx.theme()
1379 .colors()
1380 .element_background
1381 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1382 }
1383
1384 fn render_message_thinking_segment(
1385 &self,
1386 message_id: MessageId,
1387 ix: usize,
1388 markdown: Entity<Markdown>,
1389 scroll_handle: &ScrollHandle,
1390 pending: bool,
1391 cx: &Context<Self>,
1392 ) -> impl IntoElement {
1393 let is_open = self
1394 .expanded_thinking_segments
1395 .get(&(message_id, ix))
1396 .copied()
1397 .unwrap_or_default();
1398
1399 let editor_bg = cx.theme().colors().editor_background;
1400
1401 div().pt_0p5().pb_2().child(
1402 v_flex()
1403 .rounded_lg()
1404 .border_1()
1405 .border_color(self.tool_card_border_color(cx))
1406 .child(
1407 h_flex()
1408 .group("disclosure-header")
1409 .justify_between()
1410 .py_1()
1411 .px_2()
1412 .bg(self.tool_card_header_bg(cx))
1413 .map(|this| {
1414 if pending || is_open {
1415 this.rounded_t_md()
1416 .border_b_1()
1417 .border_color(self.tool_card_border_color(cx))
1418 } else {
1419 this.rounded_md()
1420 }
1421 })
1422 .child(
1423 h_flex()
1424 .gap_1p5()
1425 .child(
1426 Icon::new(IconName::Brain)
1427 .size(IconSize::XSmall)
1428 .color(Color::Muted),
1429 )
1430 .child({
1431 if pending {
1432 Label::new("Thinking…")
1433 .size(LabelSize::Small)
1434 .buffer_font(cx)
1435 .with_animation(
1436 "pulsating-label",
1437 Animation::new(Duration::from_secs(2))
1438 .repeat()
1439 .with_easing(pulsating_between(0.4, 0.8)),
1440 |label, delta| label.alpha(delta),
1441 )
1442 .into_any_element()
1443 } else {
1444 Label::new("Thought Process")
1445 .size(LabelSize::Small)
1446 .buffer_font(cx)
1447 .into_any_element()
1448 }
1449 }),
1450 )
1451 .child(
1452 h_flex()
1453 .gap_1()
1454 .child(
1455 div().visible_on_hover("disclosure-header").child(
1456 Disclosure::new("thinking-disclosure", is_open)
1457 .opened_icon(IconName::ChevronUp)
1458 .closed_icon(IconName::ChevronDown)
1459 .on_click(cx.listener({
1460 move |this, _event, _window, _cx| {
1461 let is_open = this
1462 .expanded_thinking_segments
1463 .entry((message_id, ix))
1464 .or_insert(false);
1465
1466 *is_open = !*is_open;
1467 }
1468 })),
1469 ),
1470 )
1471 .child({
1472 let (icon_name, color, animated) = if pending {
1473 (IconName::ArrowCircle, Color::Accent, true)
1474 } else {
1475 (IconName::Check, Color::Success, false)
1476 };
1477
1478 let icon =
1479 Icon::new(icon_name).color(color).size(IconSize::Small);
1480
1481 if animated {
1482 icon.with_animation(
1483 "arrow-circle",
1484 Animation::new(Duration::from_secs(2)).repeat(),
1485 |icon, delta| {
1486 icon.transform(Transformation::rotate(percentage(
1487 delta,
1488 )))
1489 },
1490 )
1491 .into_any_element()
1492 } else {
1493 icon.into_any_element()
1494 }
1495 }),
1496 ),
1497 )
1498 .when(pending && !is_open, |this| {
1499 let gradient_overlay = div()
1500 .rounded_b_lg()
1501 .h_20()
1502 .absolute()
1503 .w_full()
1504 .bottom_0()
1505 .left_0()
1506 .bg(linear_gradient(
1507 180.,
1508 linear_color_stop(editor_bg, 1.),
1509 linear_color_stop(editor_bg.opacity(0.2), 0.),
1510 ));
1511
1512 this.child(
1513 div()
1514 .relative()
1515 .bg(editor_bg)
1516 .rounded_b_lg()
1517 .child(
1518 div()
1519 .id(("thinking-content", ix))
1520 .p_2()
1521 .h_20()
1522 .track_scroll(scroll_handle)
1523 .text_ui_sm(cx)
1524 .child(markdown.clone())
1525 .overflow_hidden(),
1526 )
1527 .child(gradient_overlay),
1528 )
1529 })
1530 .when(is_open, |this| {
1531 this.child(
1532 div()
1533 .id(("thinking-content", ix))
1534 .h_full()
1535 .p_2()
1536 .rounded_b_lg()
1537 .bg(editor_bg)
1538 .text_ui_sm(cx)
1539 .child(markdown.clone()),
1540 )
1541 }),
1542 )
1543 }
1544
1545 fn render_tool_use(
1546 &self,
1547 tool_use: ToolUse,
1548 cx: &mut Context<Self>,
1549 ) -> impl IntoElement + use<> {
1550 let is_open = self
1551 .expanded_tool_uses
1552 .get(&tool_use.id)
1553 .copied()
1554 .unwrap_or_default();
1555
1556 let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
1557
1558 let status_icons = div().child(match &tool_use.status {
1559 ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
1560 let icon = Icon::new(IconName::Warning)
1561 .color(Color::Warning)
1562 .size(IconSize::Small);
1563 icon.into_any_element()
1564 }
1565 ToolUseStatus::Running => {
1566 let icon = Icon::new(IconName::ArrowCircle)
1567 .color(Color::Accent)
1568 .size(IconSize::Small);
1569 icon.with_animation(
1570 "arrow-circle",
1571 Animation::new(Duration::from_secs(2)).repeat(),
1572 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1573 )
1574 .into_any_element()
1575 }
1576 ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
1577 ToolUseStatus::Error(_) => {
1578 let icon = Icon::new(IconName::Close)
1579 .color(Color::Error)
1580 .size(IconSize::Small);
1581 icon.into_any_element()
1582 }
1583 });
1584
1585 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1586 let results_content = v_flex()
1587 .gap_1()
1588 .child(
1589 content_container()
1590 .child(
1591 Label::new("Input")
1592 .size(LabelSize::XSmall)
1593 .color(Color::Muted)
1594 .buffer_font(cx),
1595 )
1596 .child(
1597 Label::new(
1598 serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1599 )
1600 .size(LabelSize::Small)
1601 .buffer_font(cx),
1602 ),
1603 )
1604 .map(|container| match tool_use.status {
1605 ToolUseStatus::Finished(output) => container.child(
1606 content_container()
1607 .border_t_1()
1608 .border_color(self.tool_card_border_color(cx))
1609 .child(
1610 Label::new("Result")
1611 .size(LabelSize::XSmall)
1612 .color(Color::Muted)
1613 .buffer_font(cx),
1614 )
1615 .child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
1616 ),
1617 ToolUseStatus::Running => container.child(
1618 content_container().child(
1619 h_flex()
1620 .gap_1()
1621 .pb_1()
1622 .border_t_1()
1623 .border_color(self.tool_card_border_color(cx))
1624 .child(
1625 Icon::new(IconName::ArrowCircle)
1626 .size(IconSize::Small)
1627 .color(Color::Accent)
1628 .with_animation(
1629 "arrow-circle",
1630 Animation::new(Duration::from_secs(2)).repeat(),
1631 |icon, delta| {
1632 icon.transform(Transformation::rotate(percentage(
1633 delta,
1634 )))
1635 },
1636 ),
1637 )
1638 .child(
1639 Label::new("Running…")
1640 .size(LabelSize::XSmall)
1641 .color(Color::Muted)
1642 .buffer_font(cx),
1643 ),
1644 ),
1645 ),
1646 ToolUseStatus::Error(err) => container.child(
1647 content_container()
1648 .border_t_1()
1649 .border_color(self.tool_card_border_color(cx))
1650 .child(
1651 Label::new("Error")
1652 .size(LabelSize::XSmall)
1653 .color(Color::Muted)
1654 .buffer_font(cx),
1655 )
1656 .child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
1657 ),
1658 ToolUseStatus::Pending => container,
1659 ToolUseStatus::NeedsConfirmation => container.child(
1660 content_container()
1661 .border_t_1()
1662 .border_color(self.tool_card_border_color(cx))
1663 .child(
1664 Label::new("Asking Permission")
1665 .size(LabelSize::Small)
1666 .color(Color::Muted)
1667 .buffer_font(cx),
1668 ),
1669 ),
1670 });
1671
1672 let gradient_overlay = |color: Hsla| {
1673 div()
1674 .h_full()
1675 .absolute()
1676 .w_8()
1677 .bottom_0()
1678 .map(|element| {
1679 if is_status_finished {
1680 element.right_7()
1681 } else {
1682 element.right_12()
1683 }
1684 })
1685 .bg(linear_gradient(
1686 90.,
1687 linear_color_stop(color, 1.),
1688 linear_color_stop(color.opacity(0.2), 0.),
1689 ))
1690 };
1691
1692 div().map(|element| {
1693 if !tool_use.needs_confirmation {
1694 element.py_2p5().child(
1695 v_flex()
1696 .child(
1697 h_flex()
1698 .group("disclosure-header")
1699 .relative()
1700 .gap_1p5()
1701 .justify_between()
1702 .opacity(0.8)
1703 .hover(|style| style.opacity(1.))
1704 .when(!is_status_finished, |this| this.pr_2())
1705 .child(
1706 h_flex()
1707 .id("tool-label-container")
1708 .gap_1p5()
1709 .max_w_full()
1710 .overflow_x_scroll()
1711 .child(
1712 Icon::new(tool_use.icon)
1713 .size(IconSize::XSmall)
1714 .color(Color::Muted),
1715 )
1716 .child(
1717 h_flex().pr_8().text_ui_sm(cx).children(
1718 self.rendered_tool_use_labels
1719 .get(&tool_use.id)
1720 .cloned(),
1721 ),
1722 ),
1723 )
1724 .child(
1725 h_flex()
1726 .gap_1()
1727 .child(
1728 div().visible_on_hover("disclosure-header").child(
1729 Disclosure::new("tool-use-disclosure", is_open)
1730 .opened_icon(IconName::ChevronUp)
1731 .closed_icon(IconName::ChevronDown)
1732 .on_click(cx.listener({
1733 let tool_use_id = tool_use.id.clone();
1734 move |this, _event, _window, _cx| {
1735 let is_open = this
1736 .expanded_tool_uses
1737 .entry(tool_use_id.clone())
1738 .or_insert(false);
1739
1740 *is_open = !*is_open;
1741 }
1742 })),
1743 ),
1744 )
1745 .child(status_icons),
1746 )
1747 .child(gradient_overlay(cx.theme().colors().panel_background)),
1748 )
1749 .map(|parent| {
1750 if !is_open {
1751 return parent;
1752 }
1753
1754 parent.child(
1755 v_flex()
1756 .mt_1()
1757 .border_1()
1758 .border_color(self.tool_card_border_color(cx))
1759 .bg(cx.theme().colors().editor_background)
1760 .rounded_lg()
1761 .child(results_content),
1762 )
1763 }),
1764 )
1765 } else {
1766 element.py_2().child(
1767 v_flex()
1768 .rounded_lg()
1769 .border_1()
1770 .border_color(self.tool_card_border_color(cx))
1771 .overflow_hidden()
1772 .child(
1773 h_flex()
1774 .group("disclosure-header")
1775 .relative()
1776 .gap_1p5()
1777 .justify_between()
1778 .py_1()
1779 .map(|element| {
1780 if is_status_finished {
1781 element.pl_2().pr_0p5()
1782 } else {
1783 element.px_2()
1784 }
1785 })
1786 .bg(self.tool_card_header_bg(cx))
1787 .map(|element| {
1788 if is_open {
1789 element.border_b_1().rounded_t_md()
1790 } else {
1791 element.rounded_md()
1792 }
1793 })
1794 .border_color(self.tool_card_border_color(cx))
1795 .child(
1796 h_flex()
1797 .id("tool-label-container")
1798 .gap_1p5()
1799 .max_w_full()
1800 .overflow_x_scroll()
1801 .child(
1802 Icon::new(tool_use.icon)
1803 .size(IconSize::XSmall)
1804 .color(Color::Muted),
1805 )
1806 .child(
1807 h_flex().pr_8().text_ui_sm(cx).children(
1808 self.rendered_tool_use_labels
1809 .get(&tool_use.id)
1810 .cloned(),
1811 ),
1812 ),
1813 )
1814 .child(
1815 h_flex()
1816 .gap_1()
1817 .child(
1818 div().visible_on_hover("disclosure-header").child(
1819 Disclosure::new("tool-use-disclosure", is_open)
1820 .opened_icon(IconName::ChevronUp)
1821 .closed_icon(IconName::ChevronDown)
1822 .on_click(cx.listener({
1823 let tool_use_id = tool_use.id.clone();
1824 move |this, _event, _window, _cx| {
1825 let is_open = this
1826 .expanded_tool_uses
1827 .entry(tool_use_id.clone())
1828 .or_insert(false);
1829
1830 *is_open = !*is_open;
1831 }
1832 })),
1833 ),
1834 )
1835 .child(status_icons),
1836 )
1837 .child(gradient_overlay(self.tool_card_header_bg(cx))),
1838 )
1839 .map(|parent| {
1840 if !is_open {
1841 return parent;
1842 }
1843
1844 parent.child(
1845 v_flex()
1846 .bg(cx.theme().colors().editor_background)
1847 .rounded_b_lg()
1848 .child(results_content),
1849 )
1850 }),
1851 )
1852 }
1853 })
1854 }
1855
1856 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1857 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1858 else {
1859 return div().into_any();
1860 };
1861
1862 let rules_files = system_prompt_context
1863 .worktrees
1864 .iter()
1865 .filter_map(|worktree| worktree.rules_file.as_ref())
1866 .collect::<Vec<_>>();
1867
1868 let label_text = match rules_files.as_slice() {
1869 &[] => return div().into_any(),
1870 &[rules_file] => {
1871 format!("Using {:?} file", rules_file.rel_path)
1872 }
1873 rules_files => {
1874 format!("Using {} rules files", rules_files.len())
1875 }
1876 };
1877
1878 div()
1879 .pt_1()
1880 .px_2p5()
1881 .child(
1882 h_flex()
1883 .w_full()
1884 .gap_0p5()
1885 .child(
1886 h_flex()
1887 .gap_1p5()
1888 .child(
1889 Icon::new(IconName::File)
1890 .size(IconSize::XSmall)
1891 .color(Color::Disabled),
1892 )
1893 .child(
1894 Label::new(label_text)
1895 .size(LabelSize::XSmall)
1896 .color(Color::Muted)
1897 .buffer_font(cx),
1898 ),
1899 )
1900 .child(
1901 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1902 .shape(ui::IconButtonShape::Square)
1903 .icon_size(IconSize::XSmall)
1904 .icon_color(Color::Ignored)
1905 .on_click(cx.listener(Self::handle_open_rules))
1906 .tooltip(Tooltip::text("View Rules")),
1907 ),
1908 )
1909 .into_any()
1910 }
1911
1912 fn handle_allow_tool(
1913 &mut self,
1914 tool_use_id: LanguageModelToolUseId,
1915 _: &ClickEvent,
1916 _window: &mut Window,
1917 cx: &mut Context<Self>,
1918 ) {
1919 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
1920 .thread
1921 .read(cx)
1922 .pending_tool(&tool_use_id)
1923 .map(|tool_use| tool_use.status.clone())
1924 {
1925 self.thread.update(cx, |thread, cx| {
1926 thread.run_tool(
1927 c.tool_use_id.clone(),
1928 c.ui_text.clone(),
1929 c.input.clone(),
1930 &c.messages,
1931 c.tool.clone(),
1932 cx,
1933 );
1934 });
1935 }
1936 }
1937
1938 fn handle_deny_tool(
1939 &mut self,
1940 tool_use_id: LanguageModelToolUseId,
1941 tool_name: Arc<str>,
1942 _: &ClickEvent,
1943 _window: &mut Window,
1944 cx: &mut Context<Self>,
1945 ) {
1946 self.thread.update(cx, |thread, cx| {
1947 thread.deny_tool_use(tool_use_id, tool_name, cx);
1948 });
1949 }
1950
1951 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1952 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1953 else {
1954 return;
1955 };
1956
1957 let abs_paths = system_prompt_context
1958 .worktrees
1959 .iter()
1960 .flat_map(|worktree| worktree.rules_file.as_ref())
1961 .map(|rules_file| rules_file.abs_path.to_path_buf())
1962 .collect::<Vec<_>>();
1963
1964 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1965 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1966 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1967 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1968 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1969 }) {
1970 task.detach();
1971 }
1972 }
1973
1974 fn render_confirmations<'a>(
1975 &'a mut self,
1976 cx: &'a mut Context<Self>,
1977 ) -> impl Iterator<Item = AnyElement> + 'a {
1978 let thread = self.thread.read(cx);
1979
1980 thread.tools_needing_confirmation().map(|tool| {
1981 // Note: This element should be removed once a more full-fledged permission UX is implemented.
1982 let beta_tag = h_flex()
1983 .id("beta-tag")
1984 .h(px(18.))
1985 .px_1()
1986 .gap_1()
1987 .border_1()
1988 .border_color(cx.theme().colors().text_accent.opacity(0.2))
1989 .border_dashed()
1990 .rounded_sm()
1991 .bg(cx.theme().colors().text_accent.opacity(0.1))
1992 .hover(|style| style.bg(cx.theme().colors().text_accent.opacity(0.2)))
1993 .child(Label::new("Beta").size(LabelSize::XSmall))
1994 .child(Icon::new(IconName::Info).color(Color::Accent).size(IconSize::Indicator))
1995 .tooltip(
1996 Tooltip::text(
1997 "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."
1998 )
1999 );
2000
2001 v_flex()
2002 .mt_2()
2003 .mx_4()
2004 .border_1()
2005 .border_color(self.tool_card_border_color(cx))
2006 .rounded_lg()
2007 .child(
2008 h_flex()
2009 .py_1()
2010 .pl_2()
2011 .pr_1()
2012 .justify_between()
2013 .rounded_t_lg()
2014 .border_b_1()
2015 .border_color(self.tool_card_border_color(cx))
2016 .bg(self.tool_card_header_bg(cx))
2017 .child(
2018 h_flex()
2019 .gap_1()
2020 .child(Label::new("Action Confirmation").size(LabelSize::Small))
2021 .child(beta_tag),
2022 )
2023 .child(
2024 h_flex()
2025 .gap_1()
2026 .child({
2027 let tool_id = tool.id.clone();
2028 Button::new("allow-tool-action", "Allow")
2029 .label_size(LabelSize::Small)
2030 .icon(IconName::Check)
2031 .icon_position(IconPosition::Start)
2032 .icon_size(IconSize::Small)
2033 .icon_color(Color::Success)
2034 .on_click(cx.listener(move |this, event, window, cx| {
2035 this.handle_allow_tool(
2036 tool_id.clone(),
2037 event,
2038 window,
2039 cx,
2040 )
2041 }))
2042 })
2043 .child({
2044 let tool_id = tool.id.clone();
2045 let tool_name = tool.name.clone();
2046 Button::new("deny-tool", "Deny")
2047 .label_size(LabelSize::Small)
2048 .icon(IconName::Close)
2049 .icon_position(IconPosition::Start)
2050 .icon_size(IconSize::Small)
2051 .icon_color(Color::Error)
2052 .on_click(cx.listener(move |this, event, window, cx| {
2053 this.handle_deny_tool(
2054 tool_id.clone(),
2055 tool_name.clone(),
2056 event,
2057 window,
2058 cx,
2059 )
2060 }))
2061 }),
2062 ),
2063 )
2064 .child(
2065 div()
2066 .id("action_container")
2067 .rounded_b_lg()
2068 .bg(cx.theme().colors().editor_background)
2069 .overflow_y_scroll()
2070 .max_h_40()
2071 .p_2p5()
2072 .child(
2073 Label::new(&tool.ui_text)
2074 .size(LabelSize::Small)
2075 .buffer_font(cx),
2076 ),
2077 )
2078 .into_any()
2079 })
2080 }
2081
2082 fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
2083 for window in self.notifications.drain(..) {
2084 window
2085 .update(cx, |_, window, _| {
2086 window.remove_window();
2087 })
2088 .ok();
2089
2090 self.notification_subscriptions.remove(&window);
2091 }
2092 }
2093
2094 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
2095 div()
2096 .occlude()
2097 .id("active-thread-scrollbar")
2098 .on_mouse_move(cx.listener(|_, _, _, cx| {
2099 cx.notify();
2100 cx.stop_propagation()
2101 }))
2102 .on_hover(|_, _, cx| {
2103 cx.stop_propagation();
2104 })
2105 .on_any_mouse_down(|_, _, cx| {
2106 cx.stop_propagation();
2107 })
2108 .on_mouse_up(
2109 MouseButton::Left,
2110 cx.listener(|_, _, _, cx| {
2111 cx.stop_propagation();
2112 }),
2113 )
2114 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2115 cx.notify();
2116 }))
2117 .h_full()
2118 .absolute()
2119 .right_1()
2120 .top_1()
2121 .bottom_0()
2122 .w(px(12.))
2123 .cursor_default()
2124 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
2125 }
2126}
2127
2128impl Render for ActiveThread {
2129 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2130 v_flex()
2131 .size_full()
2132 .relative()
2133 .child(list(self.list_state.clone()).flex_grow())
2134 .children(self.render_confirmations(cx))
2135 .child(self.render_vertical_scrollbar(cx))
2136 }
2137}
2138
2139pub(crate) fn open_context(
2140 id: ContextId,
2141 context_store: Entity<ContextStore>,
2142 workspace: Entity<Workspace>,
2143 window: &mut Window,
2144 cx: &mut App,
2145) {
2146 let Some(context) = context_store.read(cx).context_for_id(id) else {
2147 return;
2148 };
2149
2150 match context {
2151 AssistantContext::File(file_context) => {
2152 if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
2153 {
2154 workspace.update(cx, |workspace, cx| {
2155 workspace
2156 .open_path(project_path, None, true, window, cx)
2157 .detach_and_log_err(cx);
2158 });
2159 }
2160 }
2161 AssistantContext::Directory(directory_context) => {
2162 let path = directory_context.project_path.clone();
2163 workspace.update(cx, |workspace, cx| {
2164 workspace.project().update(cx, |project, cx| {
2165 if let Some(entry) = project.entry_for_path(&path, cx) {
2166 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2167 }
2168 })
2169 })
2170 }
2171 AssistantContext::Symbol(symbol_context) => {
2172 if let Some(project_path) = symbol_context
2173 .context_symbol
2174 .buffer
2175 .read(cx)
2176 .project_path(cx)
2177 {
2178 let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
2179 let target_position = symbol_context
2180 .context_symbol
2181 .id
2182 .range
2183 .start
2184 .to_point(&snapshot);
2185
2186 let open_task = workspace.update(cx, |workspace, cx| {
2187 workspace.open_path(project_path, None, true, window, cx)
2188 });
2189 window
2190 .spawn(cx, async move |cx| {
2191 if let Some(active_editor) = open_task
2192 .await
2193 .log_err()
2194 .and_then(|item| item.downcast::<Editor>())
2195 {
2196 active_editor
2197 .downgrade()
2198 .update_in(cx, |editor, window, cx| {
2199 editor.go_to_singleton_buffer_point(
2200 target_position,
2201 window,
2202 cx,
2203 );
2204 })
2205 .log_err();
2206 }
2207 })
2208 .detach();
2209 }
2210 }
2211 AssistantContext::FetchedUrl(fetched_url_context) => {
2212 cx.open_url(&fetched_url_context.url);
2213 }
2214 AssistantContext::Thread(thread_context) => {
2215 let thread_id = thread_context.thread.read(cx).id().clone();
2216 workspace.update(cx, |workspace, cx| {
2217 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
2218 panel.update(cx, |panel, cx| {
2219 panel
2220 .open_thread(&thread_id, window, cx)
2221 .detach_and_log_err(cx)
2222 });
2223 }
2224 })
2225 }
2226 }
2227}