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 self.thread
710 .read(cx)
711 .tool_result(&tool_use.id)
712 .map(|result| result.content.clone().into())
713 .unwrap_or("".into()),
714 window,
715 cx,
716 );
717 }
718
719 if self.thread.read(cx).all_tools_finished() {
720 let model_registry = LanguageModelRegistry::read_global(cx);
721 if let Some(ConfiguredModel { model, .. }) = model_registry.default_model() {
722 self.thread.update(cx, |thread, cx| {
723 thread.attach_tool_results(cx);
724 if !canceled {
725 thread.send_to_model(model, RequestKind::Chat, cx);
726 }
727 });
728 }
729 }
730 }
731 ThreadEvent::CheckpointChanged => cx.notify(),
732 }
733 }
734
735 fn show_notification(
736 &mut self,
737 caption: impl Into<SharedString>,
738 icon: IconName,
739 window: &mut Window,
740 cx: &mut Context<ActiveThread>,
741 ) {
742 if window.is_window_active() || !self.notifications.is_empty() {
743 return;
744 }
745
746 let title = self
747 .thread
748 .read(cx)
749 .summary()
750 .unwrap_or("Agent Panel".into());
751
752 match AssistantSettings::get_global(cx).notify_when_agent_waiting {
753 NotifyWhenAgentWaiting::PrimaryScreen => {
754 if let Some(primary) = cx.primary_display() {
755 self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
756 }
757 }
758 NotifyWhenAgentWaiting::AllScreens => {
759 let caption = caption.into();
760 for screen in cx.displays() {
761 self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
762 }
763 }
764 NotifyWhenAgentWaiting::Never => {
765 // Don't show anything
766 }
767 }
768 }
769
770 fn pop_up(
771 &mut self,
772 icon: IconName,
773 caption: SharedString,
774 title: SharedString,
775 window: &mut Window,
776 screen: Rc<dyn PlatformDisplay>,
777 cx: &mut Context<'_, ActiveThread>,
778 ) {
779 let options = AgentNotification::window_options(screen, cx);
780
781 if let Some(screen_window) = cx
782 .open_window(options, |_, cx| {
783 cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
784 })
785 .log_err()
786 {
787 if let Some(pop_up) = screen_window.entity(cx).log_err() {
788 self.notification_subscriptions
789 .entry(screen_window)
790 .or_insert_with(Vec::new)
791 .push(cx.subscribe_in(&pop_up, window, {
792 |this, _, event, window, cx| match event {
793 AgentNotificationEvent::Accepted => {
794 let handle = window.window_handle();
795 cx.activate(true); // Switch back to the Zed application
796
797 let workspace_handle = this.workspace.clone();
798
799 // If there are multiple Zed windows, activate the correct one.
800 cx.defer(move |cx| {
801 handle
802 .update(cx, |_view, window, _cx| {
803 window.activate_window();
804
805 if let Some(workspace) = workspace_handle.upgrade() {
806 workspace.update(_cx, |workspace, cx| {
807 workspace
808 .focus_panel::<AssistantPanel>(window, cx);
809 });
810 }
811 })
812 .log_err();
813 });
814
815 this.dismiss_notifications(cx);
816 }
817 AgentNotificationEvent::Dismissed => {
818 this.dismiss_notifications(cx);
819 }
820 }
821 }));
822
823 self.notifications.push(screen_window);
824
825 // If the user manually refocuses the original window, dismiss the popup.
826 self.notification_subscriptions
827 .entry(screen_window)
828 .or_insert_with(Vec::new)
829 .push({
830 let pop_up_weak = pop_up.downgrade();
831
832 cx.observe_window_activation(window, move |_, window, cx| {
833 if window.is_window_active() {
834 if let Some(pop_up) = pop_up_weak.upgrade() {
835 pop_up.update(cx, |_, cx| {
836 cx.emit(AgentNotificationEvent::Dismissed);
837 });
838 }
839 }
840 })
841 });
842 }
843 }
844 }
845
846 /// Spawns a task to save the active thread.
847 ///
848 /// Only one task to save the thread will be in flight at a time.
849 fn save_thread(&mut self, cx: &mut Context<Self>) {
850 let thread = self.thread.clone();
851 self.save_thread_task = Some(cx.spawn(async move |this, cx| {
852 let task = this
853 .update(cx, |this, cx| {
854 this.thread_store
855 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
856 })
857 .ok();
858
859 if let Some(task) = task {
860 task.await.log_err();
861 }
862 }));
863 }
864
865 fn start_editing_message(
866 &mut self,
867 message_id: MessageId,
868 message_segments: &[MessageSegment],
869 window: &mut Window,
870 cx: &mut Context<Self>,
871 ) {
872 // User message should always consist of a single text segment,
873 // therefore we can skip returning early if it's not a text segment.
874 let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
875 return;
876 };
877
878 let buffer = cx.new(|cx| {
879 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
880 });
881 let editor = cx.new(|cx| {
882 let mut editor = Editor::new(
883 editor::EditorMode::AutoHeight { max_lines: 8 },
884 buffer,
885 None,
886 window,
887 cx,
888 );
889 editor.focus_handle(cx).focus(window);
890 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
891 editor
892 });
893 self.editing_message = Some((
894 message_id,
895 EditMessageState {
896 editor: editor.clone(),
897 },
898 ));
899 cx.notify();
900 }
901
902 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
903 self.editing_message.take();
904 cx.notify();
905 }
906
907 fn confirm_editing_message(
908 &mut self,
909 _: &menu::Confirm,
910 _: &mut Window,
911 cx: &mut Context<Self>,
912 ) {
913 let Some((message_id, state)) = self.editing_message.take() else {
914 return;
915 };
916 let edited_text = state.editor.read(cx).text(cx);
917 self.thread.update(cx, |thread, cx| {
918 thread.edit_message(
919 message_id,
920 Role::User,
921 vec![MessageSegment::Text(edited_text)],
922 cx,
923 );
924 for message_id in self.messages_after(message_id) {
925 thread.delete_message(*message_id, cx);
926 }
927 });
928
929 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
930 return;
931 };
932
933 if model.provider.must_accept_terms(cx) {
934 cx.notify();
935 return;
936 }
937
938 self.thread.update(cx, |thread, cx| {
939 thread.send_to_model(model.model, RequestKind::Chat, cx)
940 });
941 cx.notify();
942 }
943
944 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
945 self.messages
946 .iter()
947 .position(|id| *id == message_id)
948 .map(|index| &self.messages[index + 1..])
949 .unwrap_or(&[])
950 }
951
952 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
953 self.cancel_editing_message(&menu::Cancel, window, cx);
954 }
955
956 fn handle_regenerate_click(
957 &mut self,
958 _: &ClickEvent,
959 window: &mut Window,
960 cx: &mut Context<Self>,
961 ) {
962 self.confirm_editing_message(&menu::Confirm, window, cx);
963 }
964
965 fn handle_feedback_click(
966 &mut self,
967 feedback: ThreadFeedback,
968 window: &mut Window,
969 cx: &mut Context<Self>,
970 ) {
971 match feedback {
972 ThreadFeedback::Positive => {
973 let report = self
974 .thread
975 .update(cx, |thread, cx| thread.report_feedback(feedback, cx));
976
977 let this = cx.entity().downgrade();
978 cx.spawn(async move |_, cx| {
979 report.await?;
980 this.update(cx, |_this, cx| cx.notify())
981 })
982 .detach_and_log_err(cx);
983 }
984 ThreadFeedback::Negative => {
985 self.handle_show_feedback_comments(window, cx);
986 }
987 }
988 }
989
990 fn handle_show_feedback_comments(&mut self, window: &mut Window, cx: &mut Context<Self>) {
991 if self.feedback_message_editor.is_some() {
992 return;
993 }
994
995 let buffer = cx.new(|cx| {
996 let empty_string = String::new();
997 MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
998 });
999
1000 let editor = cx.new(|cx| {
1001 let mut editor = Editor::new(
1002 editor::EditorMode::AutoHeight { max_lines: 4 },
1003 buffer,
1004 None,
1005 window,
1006 cx,
1007 );
1008 editor.set_placeholder_text(
1009 "What went wrong? Share your feedback so we can improve.",
1010 cx,
1011 );
1012 editor
1013 });
1014
1015 editor.read(cx).focus_handle(cx).focus(window);
1016 self.feedback_message_editor = Some(editor);
1017 cx.notify();
1018 }
1019
1020 fn submit_feedback_message(&mut self, cx: &mut Context<Self>) {
1021 let Some(editor) = self.feedback_message_editor.clone() else {
1022 return;
1023 };
1024
1025 let report_task = self.thread.update(cx, |thread, cx| {
1026 thread.report_feedback(ThreadFeedback::Negative, cx)
1027 });
1028
1029 let comments = editor.read(cx).text(cx);
1030 if !comments.is_empty() {
1031 let thread_id = self.thread.read(cx).id().clone();
1032
1033 telemetry::event!("Assistant Thread Feedback Comments", thread_id, comments);
1034 }
1035
1036 self.feedback_message_editor = None;
1037
1038 let this = cx.entity().downgrade();
1039 cx.spawn(async move |_, cx| {
1040 report_task.await?;
1041 this.update(cx, |_this, cx| cx.notify())
1042 })
1043 .detach_and_log_err(cx);
1044 }
1045
1046 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1047 let message_id = self.messages[ix];
1048 let Some(message) = self.thread.read(cx).message(message_id) else {
1049 return Empty.into_any();
1050 };
1051
1052 let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
1053 return Empty.into_any();
1054 };
1055
1056 let context_store = self.context_store.clone();
1057 let workspace = self.workspace.clone();
1058
1059 let thread = self.thread.read(cx);
1060 // Get all the data we need from thread before we start using it in closures
1061 let checkpoint = thread.checkpoint_for_message(message_id);
1062 let context = thread.context_for_message(message_id).collect::<Vec<_>>();
1063 let tool_uses = thread.tool_uses_for_message(message_id, cx);
1064 let has_tool_uses = !tool_uses.is_empty();
1065
1066 // Don't render user messages that are just there for returning tool results.
1067 if message.role == Role::User && thread.message_has_tool_results(message_id) {
1068 return Empty.into_any();
1069 }
1070
1071 let allow_editing_message = message.role == Role::User;
1072
1073 let edit_message_editor = self
1074 .editing_message
1075 .as_ref()
1076 .filter(|(id, _)| *id == message_id)
1077 .map(|(_, state)| state.editor.clone());
1078
1079 let first_message = ix == 0;
1080 let show_feedback = ix == self.messages.len() - 1 && message.role != Role::User;
1081
1082 let colors = cx.theme().colors();
1083 let active_color = colors.element_active;
1084 let editor_bg_color = colors.editor_background;
1085 let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
1086
1087 let feedback_container = h_flex().pt_2().pb_4().px_4().gap_1().justify_between();
1088 let feedback_items = match self.thread.read(cx).feedback() {
1089 Some(feedback) => feedback_container
1090 .child(
1091 Label::new(match feedback {
1092 ThreadFeedback::Positive => "Thanks for your feedback!",
1093 ThreadFeedback::Negative => {
1094 "We appreciate your feedback and will use it to improve."
1095 }
1096 })
1097 .color(Color::Muted)
1098 .size(LabelSize::XSmall),
1099 )
1100 .child(
1101 h_flex()
1102 .gap_1()
1103 .child(
1104 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
1105 .icon_size(IconSize::XSmall)
1106 .icon_color(match feedback {
1107 ThreadFeedback::Positive => Color::Accent,
1108 ThreadFeedback::Negative => Color::Ignored,
1109 })
1110 .shape(ui::IconButtonShape::Square)
1111 .tooltip(Tooltip::text("Helpful Response"))
1112 .on_click(cx.listener(move |this, _, window, cx| {
1113 this.handle_feedback_click(
1114 ThreadFeedback::Positive,
1115 window,
1116 cx,
1117 );
1118 })),
1119 )
1120 .child(
1121 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
1122 .icon_size(IconSize::XSmall)
1123 .icon_color(match feedback {
1124 ThreadFeedback::Positive => Color::Ignored,
1125 ThreadFeedback::Negative => Color::Accent,
1126 })
1127 .shape(ui::IconButtonShape::Square)
1128 .tooltip(Tooltip::text("Not Helpful"))
1129 .on_click(cx.listener(move |this, _, window, cx| {
1130 this.handle_feedback_click(
1131 ThreadFeedback::Negative,
1132 window,
1133 cx,
1134 );
1135 })),
1136 ),
1137 )
1138 .into_any_element(),
1139 None => feedback_container
1140 .child(
1141 Label::new(
1142 "Rating the thread sends all of your current conversation to the Zed team.",
1143 )
1144 .color(Color::Muted)
1145 .size(LabelSize::XSmall),
1146 )
1147 .child(
1148 h_flex()
1149 .gap_1()
1150 .child(
1151 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
1152 .icon_size(IconSize::XSmall)
1153 .icon_color(Color::Ignored)
1154 .shape(ui::IconButtonShape::Square)
1155 .tooltip(Tooltip::text("Helpful Response"))
1156 .on_click(cx.listener(move |this, _, window, cx| {
1157 this.handle_feedback_click(
1158 ThreadFeedback::Positive,
1159 window,
1160 cx,
1161 );
1162 })),
1163 )
1164 .child(
1165 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
1166 .icon_size(IconSize::XSmall)
1167 .icon_color(Color::Ignored)
1168 .shape(ui::IconButtonShape::Square)
1169 .tooltip(Tooltip::text("Not Helpful"))
1170 .on_click(cx.listener(move |this, _, window, cx| {
1171 this.handle_feedback_click(
1172 ThreadFeedback::Negative,
1173 window,
1174 cx,
1175 );
1176 })),
1177 ),
1178 )
1179 .into_any_element(),
1180 };
1181
1182 let message_is_empty = message.should_display_content();
1183 let has_content = !message_is_empty || !context.is_empty();
1184
1185 let message_content =
1186 has_content.then(|| {
1187 v_flex()
1188 .gap_1p5()
1189 .when(!message_is_empty, |parent| {
1190 parent.child(
1191 if let Some(edit_message_editor) = edit_message_editor.clone() {
1192 div()
1193 .key_context("EditMessageEditor")
1194 .on_action(cx.listener(Self::cancel_editing_message))
1195 .on_action(cx.listener(Self::confirm_editing_message))
1196 .min_h_6()
1197 .child(edit_message_editor)
1198 .into_any()
1199 } else {
1200 div()
1201 .min_h_6()
1202 .text_ui(cx)
1203 .child(self.render_message_content(
1204 message_id,
1205 rendered_message,
1206 has_tool_uses,
1207 cx,
1208 ))
1209 .into_any()
1210 },
1211 )
1212 })
1213 .when(!context.is_empty(), |parent| {
1214 parent.child(h_flex().flex_wrap().gap_1().children(
1215 context.into_iter().map(|context| {
1216 let context_id = context.id();
1217 ContextPill::added(
1218 AddedContext::new(context, cx),
1219 false,
1220 false,
1221 None,
1222 )
1223 .on_click(Rc::new(cx.listener({
1224 let workspace = workspace.clone();
1225 let context_store = context_store.clone();
1226 move |_, _, window, cx| {
1227 if let Some(workspace) = workspace.upgrade() {
1228 open_context(
1229 context_id,
1230 context_store.clone(),
1231 workspace,
1232 window,
1233 cx,
1234 );
1235 cx.notify();
1236 }
1237 }
1238 })))
1239 }),
1240 ))
1241 })
1242 });
1243
1244 let styled_message = match message.role {
1245 Role::User => v_flex()
1246 .id(("message-container", ix))
1247 .map(|this| {
1248 if first_message {
1249 this.pt_2()
1250 } else {
1251 this.pt_4()
1252 }
1253 })
1254 .pb_4()
1255 .pl_2()
1256 .pr_2p5()
1257 .child(
1258 v_flex()
1259 .bg(colors.editor_background)
1260 .rounded_lg()
1261 .border_1()
1262 .border_color(colors.border)
1263 .shadow_md()
1264 .child(
1265 h_flex()
1266 .py_1()
1267 .pl_2()
1268 .pr_1()
1269 .bg(bg_user_message_header)
1270 .border_b_1()
1271 .border_color(colors.border)
1272 .justify_between()
1273 .rounded_t_md()
1274 .child(
1275 h_flex()
1276 .gap_1p5()
1277 .child(
1278 Icon::new(IconName::PersonCircle)
1279 .size(IconSize::XSmall)
1280 .color(Color::Muted),
1281 )
1282 .child(
1283 Label::new("You")
1284 .size(LabelSize::Small)
1285 .color(Color::Muted),
1286 ),
1287 )
1288 .child(
1289 h_flex()
1290 .gap_1()
1291 .when_some(
1292 edit_message_editor.clone(),
1293 |this, edit_message_editor| {
1294 let focus_handle =
1295 edit_message_editor.focus_handle(cx);
1296 this.child(
1297 Button::new("cancel-edit-message", "Cancel")
1298 .label_size(LabelSize::Small)
1299 .key_binding(
1300 KeyBinding::for_action_in(
1301 &menu::Cancel,
1302 &focus_handle,
1303 window,
1304 cx,
1305 )
1306 .map(|kb| kb.size(rems_from_px(12.))),
1307 )
1308 .on_click(
1309 cx.listener(Self::handle_cancel_click),
1310 ),
1311 )
1312 .child(
1313 Button::new(
1314 "confirm-edit-message",
1315 "Regenerate",
1316 )
1317 .label_size(LabelSize::Small)
1318 .key_binding(
1319 KeyBinding::for_action_in(
1320 &menu::Confirm,
1321 &focus_handle,
1322 window,
1323 cx,
1324 )
1325 .map(|kb| kb.size(rems_from_px(12.))),
1326 )
1327 .on_click(
1328 cx.listener(Self::handle_regenerate_click),
1329 ),
1330 )
1331 },
1332 )
1333 .when(
1334 edit_message_editor.is_none() && allow_editing_message,
1335 |this| {
1336 this.child(
1337 Button::new("edit-message", "Edit")
1338 .label_size(LabelSize::Small)
1339 .on_click(cx.listener({
1340 let message_segments =
1341 message.segments.clone();
1342 move |this, _, window, cx| {
1343 this.start_editing_message(
1344 message_id,
1345 &message_segments,
1346 window,
1347 cx,
1348 );
1349 }
1350 })),
1351 )
1352 },
1353 ),
1354 ),
1355 )
1356 .child(div().p_2().children(message_content)),
1357 ),
1358 Role::Assistant => v_flex()
1359 .id(("message-container", ix))
1360 .ml_2()
1361 .pl_2()
1362 .pr_4()
1363 .border_l_1()
1364 .border_color(cx.theme().colors().border_variant)
1365 .children(message_content)
1366 .gap_2p5()
1367 .pb_2p5()
1368 .when(!tool_uses.is_empty(), |parent| {
1369 parent.child(
1370 div().children(
1371 tool_uses
1372 .into_iter()
1373 .map(|tool_use| self.render_tool_use(tool_use, cx)),
1374 ),
1375 )
1376 }),
1377 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1378 v_flex()
1379 .bg(colors.editor_background)
1380 .rounded_sm()
1381 .child(div().p_4().children(message_content)),
1382 ),
1383 };
1384
1385 v_flex()
1386 .w_full()
1387 .when(first_message, |parent| {
1388 parent.child(self.render_rules_item(cx))
1389 })
1390 .when_some(checkpoint, |parent, checkpoint| {
1391 let mut is_pending = false;
1392 let mut error = None;
1393 if let Some(last_restore_checkpoint) =
1394 self.thread.read(cx).last_restore_checkpoint()
1395 {
1396 if last_restore_checkpoint.message_id() == message_id {
1397 match last_restore_checkpoint {
1398 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1399 LastRestoreCheckpoint::Error { error: err, .. } => {
1400 error = Some(err.clone());
1401 }
1402 }
1403 }
1404 }
1405
1406 let restore_checkpoint_button =
1407 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1408 .icon(if error.is_some() {
1409 IconName::XCircle
1410 } else {
1411 IconName::Undo
1412 })
1413 .icon_size(IconSize::XSmall)
1414 .icon_position(IconPosition::Start)
1415 .icon_color(if error.is_some() {
1416 Some(Color::Error)
1417 } else {
1418 None
1419 })
1420 .label_size(LabelSize::XSmall)
1421 .disabled(is_pending)
1422 .on_click(cx.listener(move |this, _, _window, cx| {
1423 this.thread.update(cx, |thread, cx| {
1424 thread
1425 .restore_checkpoint(checkpoint.clone(), cx)
1426 .detach_and_log_err(cx);
1427 });
1428 }));
1429
1430 let restore_checkpoint_button = if is_pending {
1431 restore_checkpoint_button
1432 .with_animation(
1433 ("pulsating-restore-checkpoint-button", ix),
1434 Animation::new(Duration::from_secs(2))
1435 .repeat()
1436 .with_easing(pulsating_between(0.6, 1.)),
1437 |label, delta| label.alpha(delta),
1438 )
1439 .into_any_element()
1440 } else if let Some(error) = error {
1441 restore_checkpoint_button
1442 .tooltip(Tooltip::text(error.to_string()))
1443 .into_any_element()
1444 } else {
1445 restore_checkpoint_button.into_any_element()
1446 };
1447
1448 parent.child(
1449 h_flex()
1450 .pt_2p5()
1451 .px_2p5()
1452 .w_full()
1453 .gap_1()
1454 .child(ui::Divider::horizontal())
1455 .child(restore_checkpoint_button)
1456 .child(ui::Divider::horizontal()),
1457 )
1458 })
1459 .child(styled_message)
1460 .when(
1461 show_feedback && !self.thread.read(cx).is_generating(),
1462 |parent| {
1463 parent.child(feedback_items).when_some(
1464 self.feedback_message_editor.clone(),
1465 |parent, feedback_editor| {
1466 let focus_handle = feedback_editor.focus_handle(cx);
1467 parent.child(
1468 v_flex()
1469 .key_context("AgentFeedbackMessageEditor")
1470 .on_action(cx.listener(|this, _: &menu::Cancel, _, cx| {
1471 this.feedback_message_editor = None;
1472 cx.notify();
1473 }))
1474 .on_action(cx.listener(|this, _: &menu::Confirm, _, cx| {
1475 this.submit_feedback_message(cx);
1476 cx.notify();
1477 }))
1478 .on_action(cx.listener(Self::confirm_editing_message))
1479 .mx_4()
1480 .mb_3()
1481 .p_2()
1482 .rounded_md()
1483 .border_1()
1484 .border_color(cx.theme().colors().border)
1485 .bg(cx.theme().colors().editor_background)
1486 .child(feedback_editor)
1487 .child(
1488 h_flex()
1489 .gap_1()
1490 .justify_end()
1491 .child(
1492 Button::new("dismiss-feedback-message", "Cancel")
1493 .label_size(LabelSize::Small)
1494 .key_binding(
1495 KeyBinding::for_action_in(
1496 &menu::Cancel,
1497 &focus_handle,
1498 window,
1499 cx,
1500 )
1501 .map(|kb| kb.size(rems_from_px(10.))),
1502 )
1503 .on_click(cx.listener(|this, _, _, cx| {
1504 this.feedback_message_editor = None;
1505 cx.notify();
1506 })),
1507 )
1508 .child(
1509 Button::new(
1510 "submit-feedback-message",
1511 "Share Feedback",
1512 )
1513 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
1514 .label_size(LabelSize::Small)
1515 .key_binding(
1516 KeyBinding::for_action_in(
1517 &menu::Confirm,
1518 &focus_handle,
1519 window,
1520 cx,
1521 )
1522 .map(|kb| kb.size(rems_from_px(10.))),
1523 )
1524 .on_click(cx.listener(|this, _, _, cx| {
1525 this.submit_feedback_message(cx);
1526 cx.notify();
1527 })),
1528 ),
1529 ),
1530 )
1531 },
1532 )
1533 },
1534 )
1535 .into_any()
1536 }
1537
1538 fn render_message_content(
1539 &self,
1540 message_id: MessageId,
1541 rendered_message: &RenderedMessage,
1542 has_tool_uses: bool,
1543 cx: &Context<Self>,
1544 ) -> impl IntoElement {
1545 let is_last_message = self.messages.last() == Some(&message_id);
1546 let is_generating = self.thread.read(cx).is_generating();
1547 let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses {
1548 rendered_message
1549 .segments
1550 .iter()
1551 .enumerate()
1552 .next_back()
1553 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1554 .map(|(index, _)| index)
1555 } else {
1556 None
1557 };
1558
1559 div()
1560 .text_ui(cx)
1561 .gap_2()
1562 .children(
1563 rendered_message.segments.iter().enumerate().map(
1564 |(index, segment)| match segment {
1565 RenderedMessageSegment::Thinking {
1566 content,
1567 scroll_handle,
1568 } => self
1569 .render_message_thinking_segment(
1570 message_id,
1571 index,
1572 content.clone(),
1573 &scroll_handle,
1574 Some(index) == pending_thinking_segment_index,
1575 cx,
1576 )
1577 .into_any_element(),
1578 RenderedMessageSegment::Text(markdown) => {
1579 div().child(markdown.clone()).into_any_element()
1580 }
1581 },
1582 ),
1583 )
1584 }
1585
1586 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1587 cx.theme().colors().border.opacity(0.5)
1588 }
1589
1590 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1591 cx.theme()
1592 .colors()
1593 .element_background
1594 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1595 }
1596
1597 fn render_message_thinking_segment(
1598 &self,
1599 message_id: MessageId,
1600 ix: usize,
1601 markdown: Entity<Markdown>,
1602 scroll_handle: &ScrollHandle,
1603 pending: bool,
1604 cx: &Context<Self>,
1605 ) -> impl IntoElement {
1606 let is_open = self
1607 .expanded_thinking_segments
1608 .get(&(message_id, ix))
1609 .copied()
1610 .unwrap_or_default();
1611
1612 let editor_bg = cx.theme().colors().editor_background;
1613
1614 div().pt_0p5().pb_2().child(
1615 v_flex()
1616 .rounded_lg()
1617 .border_1()
1618 .border_color(self.tool_card_border_color(cx))
1619 .child(
1620 h_flex()
1621 .group("disclosure-header")
1622 .justify_between()
1623 .py_1()
1624 .px_2()
1625 .bg(self.tool_card_header_bg(cx))
1626 .map(|this| {
1627 if pending || is_open {
1628 this.rounded_t_md()
1629 .border_b_1()
1630 .border_color(self.tool_card_border_color(cx))
1631 } else {
1632 this.rounded_md()
1633 }
1634 })
1635 .child(
1636 h_flex()
1637 .gap_1p5()
1638 .child(
1639 Icon::new(IconName::Brain)
1640 .size(IconSize::XSmall)
1641 .color(Color::Muted),
1642 )
1643 .child({
1644 if pending {
1645 Label::new("Thinking…")
1646 .size(LabelSize::Small)
1647 .buffer_font(cx)
1648 .with_animation(
1649 "pulsating-label",
1650 Animation::new(Duration::from_secs(2))
1651 .repeat()
1652 .with_easing(pulsating_between(0.4, 0.8)),
1653 |label, delta| label.alpha(delta),
1654 )
1655 .into_any_element()
1656 } else {
1657 Label::new("Thought Process")
1658 .size(LabelSize::Small)
1659 .buffer_font(cx)
1660 .into_any_element()
1661 }
1662 }),
1663 )
1664 .child(
1665 h_flex()
1666 .gap_1()
1667 .child(
1668 div().visible_on_hover("disclosure-header").child(
1669 Disclosure::new("thinking-disclosure", is_open)
1670 .opened_icon(IconName::ChevronUp)
1671 .closed_icon(IconName::ChevronDown)
1672 .on_click(cx.listener({
1673 move |this, _event, _window, _cx| {
1674 let is_open = this
1675 .expanded_thinking_segments
1676 .entry((message_id, ix))
1677 .or_insert(false);
1678
1679 *is_open = !*is_open;
1680 }
1681 })),
1682 ),
1683 )
1684 .child({
1685 let (icon_name, color, animated) = if pending {
1686 (IconName::ArrowCircle, Color::Accent, true)
1687 } else {
1688 (IconName::Check, Color::Success, false)
1689 };
1690
1691 let icon =
1692 Icon::new(icon_name).color(color).size(IconSize::Small);
1693
1694 if animated {
1695 icon.with_animation(
1696 "arrow-circle",
1697 Animation::new(Duration::from_secs(2)).repeat(),
1698 |icon, delta| {
1699 icon.transform(Transformation::rotate(percentage(
1700 delta,
1701 )))
1702 },
1703 )
1704 .into_any_element()
1705 } else {
1706 icon.into_any_element()
1707 }
1708 }),
1709 ),
1710 )
1711 .when(pending && !is_open, |this| {
1712 let gradient_overlay = div()
1713 .rounded_b_lg()
1714 .h_20()
1715 .absolute()
1716 .w_full()
1717 .bottom_0()
1718 .left_0()
1719 .bg(linear_gradient(
1720 180.,
1721 linear_color_stop(editor_bg, 1.),
1722 linear_color_stop(editor_bg.opacity(0.2), 0.),
1723 ));
1724
1725 this.child(
1726 div()
1727 .relative()
1728 .bg(editor_bg)
1729 .rounded_b_lg()
1730 .child(
1731 div()
1732 .id(("thinking-content", ix))
1733 .p_2()
1734 .h_20()
1735 .track_scroll(scroll_handle)
1736 .text_ui_sm(cx)
1737 .child(markdown.clone())
1738 .overflow_hidden(),
1739 )
1740 .child(gradient_overlay),
1741 )
1742 })
1743 .when(is_open, |this| {
1744 this.child(
1745 div()
1746 .id(("thinking-content", ix))
1747 .h_full()
1748 .p_2()
1749 .rounded_b_lg()
1750 .bg(editor_bg)
1751 .text_ui_sm(cx)
1752 .child(markdown.clone()),
1753 )
1754 }),
1755 )
1756 }
1757
1758 fn render_tool_use(
1759 &self,
1760 tool_use: ToolUse,
1761 cx: &mut Context<Self>,
1762 ) -> impl IntoElement + use<> {
1763 let is_open = self
1764 .expanded_tool_uses
1765 .get(&tool_use.id)
1766 .copied()
1767 .unwrap_or_default();
1768
1769 let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
1770
1771 let fs = self
1772 .workspace
1773 .upgrade()
1774 .map(|workspace| workspace.read(cx).app_state().fs.clone());
1775 let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
1776
1777 let status_icons = div().child(match &tool_use.status {
1778 ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
1779 let icon = Icon::new(IconName::Warning)
1780 .color(Color::Warning)
1781 .size(IconSize::Small);
1782 icon.into_any_element()
1783 }
1784 ToolUseStatus::Running => {
1785 let icon = Icon::new(IconName::ArrowCircle)
1786 .color(Color::Accent)
1787 .size(IconSize::Small);
1788 icon.with_animation(
1789 "arrow-circle",
1790 Animation::new(Duration::from_secs(2)).repeat(),
1791 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1792 )
1793 .into_any_element()
1794 }
1795 ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
1796 ToolUseStatus::Error(_) => {
1797 let icon = Icon::new(IconName::Close)
1798 .color(Color::Error)
1799 .size(IconSize::Small);
1800 icon.into_any_element()
1801 }
1802 });
1803
1804 let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
1805 let results_content_container = || v_flex().p_2().gap_0p5();
1806
1807 let results_content = v_flex()
1808 .gap_1()
1809 .child(
1810 results_content_container()
1811 .child(
1812 Label::new("Input")
1813 .size(LabelSize::XSmall)
1814 .color(Color::Muted)
1815 .buffer_font(cx),
1816 )
1817 .child(
1818 div().w_full().text_ui_sm(cx).children(
1819 rendered_tool_use
1820 .as_ref()
1821 .map(|rendered| rendered.input.clone()),
1822 ),
1823 ),
1824 )
1825 .map(|container| match tool_use.status {
1826 ToolUseStatus::Finished(_) => container.child(
1827 results_content_container()
1828 .border_t_1()
1829 .border_color(self.tool_card_border_color(cx))
1830 .child(
1831 Label::new("Result")
1832 .size(LabelSize::XSmall)
1833 .color(Color::Muted)
1834 .buffer_font(cx),
1835 )
1836 .child(
1837 div().w_full().text_ui_sm(cx).children(
1838 rendered_tool_use
1839 .as_ref()
1840 .map(|rendered| rendered.output.clone()),
1841 ),
1842 ),
1843 ),
1844 ToolUseStatus::Running => container.child(
1845 results_content_container().child(
1846 h_flex()
1847 .gap_1()
1848 .pb_1()
1849 .border_t_1()
1850 .border_color(self.tool_card_border_color(cx))
1851 .child(
1852 Icon::new(IconName::ArrowCircle)
1853 .size(IconSize::Small)
1854 .color(Color::Accent)
1855 .with_animation(
1856 "arrow-circle",
1857 Animation::new(Duration::from_secs(2)).repeat(),
1858 |icon, delta| {
1859 icon.transform(Transformation::rotate(percentage(
1860 delta,
1861 )))
1862 },
1863 ),
1864 )
1865 .child(
1866 Label::new("Running…")
1867 .size(LabelSize::XSmall)
1868 .color(Color::Muted)
1869 .buffer_font(cx),
1870 ),
1871 ),
1872 ),
1873 ToolUseStatus::Error(_) => container.child(
1874 results_content_container()
1875 .border_t_1()
1876 .border_color(self.tool_card_border_color(cx))
1877 .child(
1878 Label::new("Error")
1879 .size(LabelSize::XSmall)
1880 .color(Color::Muted)
1881 .buffer_font(cx),
1882 )
1883 .child(
1884 div().text_ui_sm(cx).children(
1885 rendered_tool_use
1886 .as_ref()
1887 .map(|rendered| rendered.output.clone()),
1888 ),
1889 ),
1890 ),
1891 ToolUseStatus::Pending => container,
1892 ToolUseStatus::NeedsConfirmation => container.child(
1893 results_content_container()
1894 .border_t_1()
1895 .border_color(self.tool_card_border_color(cx))
1896 .child(
1897 Label::new("Asking Permission")
1898 .size(LabelSize::Small)
1899 .color(Color::Muted)
1900 .buffer_font(cx),
1901 ),
1902 ),
1903 });
1904
1905 let gradient_overlay = |color: Hsla| {
1906 div()
1907 .h_full()
1908 .absolute()
1909 .w_12()
1910 .bottom_0()
1911 .map(|element| {
1912 if is_status_finished {
1913 element.right_6()
1914 } else {
1915 element.right(px(44.))
1916 }
1917 })
1918 .bg(linear_gradient(
1919 90.,
1920 linear_color_stop(color, 1.),
1921 linear_color_stop(color.opacity(0.2), 0.),
1922 ))
1923 };
1924
1925 div().map(|element| {
1926 if !tool_use.needs_confirmation {
1927 element.child(
1928 v_flex()
1929 .child(
1930 h_flex()
1931 .group("disclosure-header")
1932 .relative()
1933 .gap_1p5()
1934 .justify_between()
1935 .opacity(0.8)
1936 .hover(|style| style.opacity(1.))
1937 .when(!is_status_finished, |this| this.pr_2())
1938 .child(
1939 h_flex()
1940 .id("tool-label-container")
1941 .gap_1p5()
1942 .max_w_full()
1943 .overflow_x_scroll()
1944 .child(
1945 Icon::new(tool_use.icon)
1946 .size(IconSize::XSmall)
1947 .color(Color::Muted),
1948 )
1949 .child(
1950 h_flex().pr_8().text_ui_sm(cx).children(
1951 rendered_tool_use.map(|rendered| rendered.label)
1952 ),
1953 ),
1954 )
1955 .child(
1956 h_flex()
1957 .gap_1()
1958 .child(
1959 div().visible_on_hover("disclosure-header").child(
1960 Disclosure::new("tool-use-disclosure", is_open)
1961 .opened_icon(IconName::ChevronUp)
1962 .closed_icon(IconName::ChevronDown)
1963 .on_click(cx.listener({
1964 let tool_use_id = tool_use.id.clone();
1965 move |this, _event, _window, _cx| {
1966 let is_open = this
1967 .expanded_tool_uses
1968 .entry(tool_use_id.clone())
1969 .or_insert(false);
1970
1971 *is_open = !*is_open;
1972 }
1973 })),
1974 ),
1975 )
1976 .child(status_icons),
1977 )
1978 .child(gradient_overlay(cx.theme().colors().panel_background)),
1979 )
1980 .map(|parent| {
1981 if !is_open {
1982 return parent;
1983 }
1984
1985 parent.child(
1986 v_flex()
1987 .mt_1()
1988 .border_1()
1989 .border_color(self.tool_card_border_color(cx))
1990 .bg(cx.theme().colors().editor_background)
1991 .rounded_lg()
1992 .child(results_content),
1993 )
1994 }),
1995 )
1996 } else {
1997 v_flex()
1998 .rounded_lg()
1999 .border_1()
2000 .border_color(self.tool_card_border_color(cx))
2001 .overflow_hidden()
2002 .child(
2003 h_flex()
2004 .group("disclosure-header")
2005 .relative()
2006 .justify_between()
2007 .py_1()
2008 .map(|element| {
2009 if is_status_finished {
2010 element.pl_2().pr_0p5()
2011 } else {
2012 element.px_2()
2013 }
2014 })
2015 .bg(self.tool_card_header_bg(cx))
2016 .map(|element| {
2017 if is_open {
2018 element.border_b_1().rounded_t_md()
2019 } else if needs_confirmation {
2020 element.rounded_t_md()
2021 } else {
2022 element.rounded_md()
2023 }
2024 })
2025 .border_color(self.tool_card_border_color(cx))
2026 .child(
2027 h_flex()
2028 .id("tool-label-container")
2029 .gap_1p5()
2030 .max_w_full()
2031 .overflow_x_scroll()
2032 .child(
2033 Icon::new(tool_use.icon)
2034 .size(IconSize::XSmall)
2035 .color(Color::Muted),
2036 )
2037 .child(
2038 h_flex().pr_8().text_ui_sm(cx).children(
2039 rendered_tool_use.map(|rendered| rendered.label)
2040 ),
2041 ),
2042 )
2043 .child(
2044 h_flex()
2045 .gap_1()
2046 .child(
2047 div().visible_on_hover("disclosure-header").child(
2048 Disclosure::new("tool-use-disclosure", is_open)
2049 .opened_icon(IconName::ChevronUp)
2050 .closed_icon(IconName::ChevronDown)
2051 .on_click(cx.listener({
2052 let tool_use_id = tool_use.id.clone();
2053 move |this, _event, _window, _cx| {
2054 let is_open = this
2055 .expanded_tool_uses
2056 .entry(tool_use_id.clone())
2057 .or_insert(false);
2058
2059 *is_open = !*is_open;
2060 }
2061 })),
2062 ),
2063 )
2064 .child(status_icons),
2065 )
2066 .child(gradient_overlay(self.tool_card_header_bg(cx))),
2067 )
2068 .map(|parent| {
2069 if !is_open {
2070 return parent;
2071 }
2072
2073 parent.child(
2074 v_flex()
2075 .bg(cx.theme().colors().editor_background)
2076 .map(|element| {
2077 if needs_confirmation {
2078 element.rounded_none()
2079 } else {
2080 element.rounded_b_lg()
2081 }
2082 })
2083 .child(results_content),
2084 )
2085 })
2086 .when(needs_confirmation, |this| {
2087 this.child(
2088 h_flex()
2089 .py_1()
2090 .pl_2()
2091 .pr_1()
2092 .gap_1()
2093 .justify_between()
2094 .bg(cx.theme().colors().editor_background)
2095 .border_t_1()
2096 .border_color(self.tool_card_border_color(cx))
2097 .rounded_b_lg()
2098 .child(Label::new("Action Confirmation").color(Color::Muted).size(LabelSize::Small))
2099 .child(
2100 h_flex()
2101 .gap_0p5()
2102 .child({
2103 let tool_id = tool_use.id.clone();
2104 Button::new(
2105 "always-allow-tool-action",
2106 "Always Allow",
2107 )
2108 .label_size(LabelSize::Small)
2109 .icon(IconName::CheckDouble)
2110 .icon_position(IconPosition::Start)
2111 .icon_size(IconSize::Small)
2112 .icon_color(Color::Success)
2113 .tooltip(move |window, cx| {
2114 Tooltip::with_meta(
2115 "Never ask for permission",
2116 None,
2117 "Restore the original behavior in your Agent Panel settings",
2118 window,
2119 cx,
2120 )
2121 })
2122 .on_click(cx.listener(
2123 move |this, event, window, cx| {
2124 if let Some(fs) = fs.clone() {
2125 update_settings_file::<AssistantSettings>(
2126 fs.clone(),
2127 cx,
2128 |settings, _| {
2129 settings.set_always_allow_tool_actions(true);
2130 },
2131 );
2132 }
2133 this.handle_allow_tool(
2134 tool_id.clone(),
2135 event,
2136 window,
2137 cx,
2138 )
2139 },
2140 ))
2141 })
2142 .child(ui::Divider::vertical())
2143 .child({
2144 let tool_id = tool_use.id.clone();
2145 Button::new("allow-tool-action", "Allow")
2146 .label_size(LabelSize::Small)
2147 .icon(IconName::Check)
2148 .icon_position(IconPosition::Start)
2149 .icon_size(IconSize::Small)
2150 .icon_color(Color::Success)
2151 .on_click(cx.listener(
2152 move |this, event, window, cx| {
2153 this.handle_allow_tool(
2154 tool_id.clone(),
2155 event,
2156 window,
2157 cx,
2158 )
2159 },
2160 ))
2161 })
2162 .child({
2163 let tool_id = tool_use.id.clone();
2164 let tool_name: Arc<str> = tool_use.name.into();
2165 Button::new("deny-tool", "Deny")
2166 .label_size(LabelSize::Small)
2167 .icon(IconName::Close)
2168 .icon_position(IconPosition::Start)
2169 .icon_size(IconSize::Small)
2170 .icon_color(Color::Error)
2171 .on_click(cx.listener(
2172 move |this, event, window, cx| {
2173 this.handle_deny_tool(
2174 tool_id.clone(),
2175 tool_name.clone(),
2176 event,
2177 window,
2178 cx,
2179 )
2180 },
2181 ))
2182 }),
2183 ),
2184 )
2185 })
2186 }
2187 })
2188 }
2189
2190 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
2191 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2192 else {
2193 return div().into_any();
2194 };
2195
2196 let rules_files = system_prompt_context
2197 .worktrees
2198 .iter()
2199 .filter_map(|worktree| worktree.rules_file.as_ref())
2200 .collect::<Vec<_>>();
2201
2202 let label_text = match rules_files.as_slice() {
2203 &[] => return div().into_any(),
2204 &[rules_file] => {
2205 format!("Using {:?} file", rules_file.rel_path)
2206 }
2207 rules_files => {
2208 format!("Using {} rules files", rules_files.len())
2209 }
2210 };
2211
2212 div()
2213 .pt_1()
2214 .px_2p5()
2215 .child(
2216 h_flex()
2217 .w_full()
2218 .gap_0p5()
2219 .child(
2220 h_flex()
2221 .gap_1p5()
2222 .child(
2223 Icon::new(IconName::File)
2224 .size(IconSize::XSmall)
2225 .color(Color::Disabled),
2226 )
2227 .child(
2228 Label::new(label_text)
2229 .size(LabelSize::XSmall)
2230 .color(Color::Muted)
2231 .buffer_font(cx),
2232 ),
2233 )
2234 .child(
2235 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
2236 .shape(ui::IconButtonShape::Square)
2237 .icon_size(IconSize::XSmall)
2238 .icon_color(Color::Ignored)
2239 .on_click(cx.listener(Self::handle_open_rules))
2240 .tooltip(Tooltip::text("View Rules")),
2241 ),
2242 )
2243 .into_any()
2244 }
2245
2246 fn handle_allow_tool(
2247 &mut self,
2248 tool_use_id: LanguageModelToolUseId,
2249 _: &ClickEvent,
2250 _window: &mut Window,
2251 cx: &mut Context<Self>,
2252 ) {
2253 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
2254 .thread
2255 .read(cx)
2256 .pending_tool(&tool_use_id)
2257 .map(|tool_use| tool_use.status.clone())
2258 {
2259 self.thread.update(cx, |thread, cx| {
2260 thread.run_tool(
2261 c.tool_use_id.clone(),
2262 c.ui_text.clone(),
2263 c.input.clone(),
2264 &c.messages,
2265 c.tool.clone(),
2266 cx,
2267 );
2268 });
2269 }
2270 }
2271
2272 fn handle_deny_tool(
2273 &mut self,
2274 tool_use_id: LanguageModelToolUseId,
2275 tool_name: Arc<str>,
2276 _: &ClickEvent,
2277 _window: &mut Window,
2278 cx: &mut Context<Self>,
2279 ) {
2280 self.thread.update(cx, |thread, cx| {
2281 thread.deny_tool_use(tool_use_id, tool_name, cx);
2282 });
2283 }
2284
2285 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
2286 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
2287 else {
2288 return;
2289 };
2290
2291 let abs_paths = system_prompt_context
2292 .worktrees
2293 .iter()
2294 .flat_map(|worktree| worktree.rules_file.as_ref())
2295 .map(|rules_file| rules_file.abs_path.to_path_buf())
2296 .collect::<Vec<_>>();
2297
2298 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
2299 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
2300 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
2301 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
2302 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
2303 }) {
2304 task.detach();
2305 }
2306 }
2307
2308 fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
2309 for window in self.notifications.drain(..) {
2310 window
2311 .update(cx, |_, window, _| {
2312 window.remove_window();
2313 })
2314 .ok();
2315
2316 self.notification_subscriptions.remove(&window);
2317 }
2318 }
2319
2320 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
2321 if !self.show_scrollbar && !self.scrollbar_state.is_dragging() {
2322 return None;
2323 }
2324
2325 Some(
2326 div()
2327 .occlude()
2328 .id("active-thread-scrollbar")
2329 .on_mouse_move(cx.listener(|_, _, _, cx| {
2330 cx.notify();
2331 cx.stop_propagation()
2332 }))
2333 .on_hover(|_, _, cx| {
2334 cx.stop_propagation();
2335 })
2336 .on_any_mouse_down(|_, _, cx| {
2337 cx.stop_propagation();
2338 })
2339 .on_mouse_up(
2340 MouseButton::Left,
2341 cx.listener(|_, _, _, cx| {
2342 cx.stop_propagation();
2343 }),
2344 )
2345 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
2346 cx.notify();
2347 }))
2348 .h_full()
2349 .absolute()
2350 .right_1()
2351 .top_1()
2352 .bottom_0()
2353 .w(px(12.))
2354 .cursor_default()
2355 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
2356 )
2357 }
2358
2359 fn hide_scrollbar_later(&mut self, cx: &mut Context<Self>) {
2360 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
2361 self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| {
2362 cx.background_executor()
2363 .timer(SCROLLBAR_SHOW_INTERVAL)
2364 .await;
2365 thread
2366 .update(cx, |thread, cx| {
2367 if !thread.scrollbar_state.is_dragging() {
2368 thread.show_scrollbar = false;
2369 cx.notify();
2370 }
2371 })
2372 .log_err();
2373 }))
2374 }
2375}
2376
2377impl Render for ActiveThread {
2378 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2379 v_flex()
2380 .size_full()
2381 .relative()
2382 .on_mouse_move(cx.listener(|this, _, _, cx| {
2383 this.show_scrollbar = true;
2384 this.hide_scrollbar_later(cx);
2385 cx.notify();
2386 }))
2387 .on_scroll_wheel(cx.listener(|this, _, _, cx| {
2388 this.show_scrollbar = true;
2389 this.hide_scrollbar_later(cx);
2390 cx.notify();
2391 }))
2392 .on_mouse_up(
2393 MouseButton::Left,
2394 cx.listener(|this, _, _, cx| {
2395 this.hide_scrollbar_later(cx);
2396 }),
2397 )
2398 .child(list(self.list_state.clone()).flex_grow())
2399 .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
2400 this.child(scrollbar)
2401 })
2402 }
2403}
2404
2405pub(crate) fn open_context(
2406 id: ContextId,
2407 context_store: Entity<ContextStore>,
2408 workspace: Entity<Workspace>,
2409 window: &mut Window,
2410 cx: &mut App,
2411) {
2412 let Some(context) = context_store.read(cx).context_for_id(id) else {
2413 return;
2414 };
2415
2416 match context {
2417 AssistantContext::File(file_context) => {
2418 if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
2419 {
2420 workspace.update(cx, |workspace, cx| {
2421 workspace
2422 .open_path(project_path, None, true, window, cx)
2423 .detach_and_log_err(cx);
2424 });
2425 }
2426 }
2427 AssistantContext::Directory(directory_context) => {
2428 let path = directory_context.project_path.clone();
2429 workspace.update(cx, |workspace, cx| {
2430 workspace.project().update(cx, |project, cx| {
2431 if let Some(entry) = project.entry_for_path(&path, cx) {
2432 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2433 }
2434 })
2435 })
2436 }
2437 AssistantContext::Symbol(symbol_context) => {
2438 if let Some(project_path) = symbol_context
2439 .context_symbol
2440 .buffer
2441 .read(cx)
2442 .project_path(cx)
2443 {
2444 let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
2445 let target_position = symbol_context
2446 .context_symbol
2447 .id
2448 .range
2449 .start
2450 .to_point(&snapshot);
2451
2452 let open_task = workspace.update(cx, |workspace, cx| {
2453 workspace.open_path(project_path, None, true, window, cx)
2454 });
2455 window
2456 .spawn(cx, async move |cx| {
2457 if let Some(active_editor) = open_task
2458 .await
2459 .log_err()
2460 .and_then(|item| item.downcast::<Editor>())
2461 {
2462 active_editor
2463 .downgrade()
2464 .update_in(cx, |editor, window, cx| {
2465 editor.go_to_singleton_buffer_point(
2466 target_position,
2467 window,
2468 cx,
2469 );
2470 })
2471 .log_err();
2472 }
2473 })
2474 .detach();
2475 }
2476 }
2477 AssistantContext::FetchedUrl(fetched_url_context) => {
2478 cx.open_url(&fetched_url_context.url);
2479 }
2480 AssistantContext::Thread(thread_context) => {
2481 let thread_id = thread_context.thread.read(cx).id().clone();
2482 workspace.update(cx, |workspace, cx| {
2483 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
2484 panel.update(cx, |panel, cx| {
2485 panel
2486 .open_thread(&thread_id, window, cx)
2487 .detach_and_log_err(cx)
2488 });
2489 }
2490 })
2491 }
2492 }
2493}