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