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