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