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