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