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