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