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