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