1use crate::context_picker::{ContextPicker, MentionLink};
2use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
3use crate::message_editor::{extract_message_creases, insert_message_creases};
4use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
5use crate::{AgentPanel, ModelUsageContext};
6use agent::{
7 ContextStore, LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, TextThreadStore,
8 Thread, ThreadError, ThreadEvent, ThreadFeedback, ThreadStore, ThreadSummary,
9 context::{self, AgentContextHandle, RULES_ICON},
10 thread_store::RulesLoadingError,
11 tool_use::{PendingToolUseStatus, ToolUse},
12};
13use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
14use anyhow::Context as _;
15use assistant_tool::ToolUseStatus;
16use audio::{Audio, Sound};
17use collections::{HashMap, HashSet};
18use editor::actions::{MoveToEnd, MoveUp, Paste};
19use editor::scroll::Autoscroll;
20use editor::{
21 Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer, SelectionEffects,
22};
23use gpui::{
24 AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
25 ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
26 ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful,
27 StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
28 UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage,
29 pulsating_between,
30};
31use language::{Buffer, Language, LanguageRegistry};
32use language_model::{
33 LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, Role, StopReason,
34};
35use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
36use markdown::{
37 HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, PathWithRange,
38};
39use project::{ProjectEntryId, ProjectItem as _};
40use regex::Regex;
41use rope::Point;
42use settings::{Settings as _, SettingsStore, update_settings_file};
43use std::ffi::OsStr;
44use std::path::Path;
45use std::rc::Rc;
46use std::sync::{Arc, LazyLock};
47use std::time::Duration;
48use text::ToPoint;
49use theme::ThemeSettings;
50use ui::{
51 Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
52 Tooltip, prelude::*,
53};
54use util::ResultExt as _;
55use util::markdown::MarkdownCodeBlock;
56use workspace::{CollaboratorId, Workspace};
57use zed_actions::assistant::OpenRulesLibrary;
58use zed_llm_client::CompletionIntent;
59
60const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
61const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
62const RESPONSE_PADDING_X: Pixels = px(19.);
63const ASSISTANT_EDITOR_MAX_LINES: usize = 50;
64
65static LANG_PREFIX_REGEX: LazyLock<Regex> = LazyLock::new(|| {
66 Regex::new(r"```([a-zA-Z0-9]{1,5}) ([^\n]*[/\\][^\n]*\.([a-zA-Z0-9]+)(?:#[^\n]*)?)")
67 .expect("Failed to create LANG_PREFIX_REGEX")
68});
69static PATH_CODE_BLOCK_REGEX: LazyLock<Regex> = LazyLock::new(|| {
70 Regex::new(r"```(\S*[/\\]\S*\.(\w+)\S*)").expect("Failed to create PATH_CODE_BLOCK_REGEX")
71});
72
73pub struct ActiveThread {
74 context_store: Entity<ContextStore>,
75 language_registry: Arc<LanguageRegistry>,
76 thread_store: Entity<ThreadStore>,
77 text_thread_store: Entity<TextThreadStore>,
78 thread: Entity<Thread>,
79 workspace: WeakEntity<Workspace>,
80 save_thread_task: Option<Task<()>>,
81 messages: Vec<MessageId>,
82 list_state: ListState,
83 scrollbar_state: ScrollbarState,
84 show_scrollbar: bool,
85 hide_scrollbar_task: Option<Task<()>>,
86 rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
87 rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,
88 editing_message: Option<(MessageId, EditingMessageState)>,
89 expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
90 expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
91 expanded_code_blocks: HashMap<(MessageId, usize), bool>,
92 last_error: Option<ThreadError>,
93 notifications: Vec<WindowHandle<AgentNotification>>,
94 copied_code_block_ids: HashSet<(MessageId, usize)>,
95 _subscriptions: Vec<Subscription>,
96 notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
97 open_feedback_editors: HashMap<MessageId, Entity<Editor>>,
98 _load_edited_message_context_task: Option<Task<()>>,
99}
100
101struct RenderedMessage {
102 language_registry: Arc<LanguageRegistry>,
103 segments: Vec<RenderedMessageSegment>,
104}
105
106#[derive(Clone)]
107struct RenderedToolUse {
108 label: Entity<Markdown>,
109 input: Entity<Markdown>,
110 output: Entity<Markdown>,
111}
112
113impl RenderedMessage {
114 fn from_segments(
115 segments: &[MessageSegment],
116 language_registry: Arc<LanguageRegistry>,
117 cx: &mut App,
118 ) -> Self {
119 let mut this = Self {
120 language_registry,
121 segments: Vec::with_capacity(segments.len()),
122 };
123 for segment in segments {
124 this.push_segment(segment, cx);
125 }
126 this
127 }
128
129 fn append_thinking(&mut self, text: &String, cx: &mut App) {
130 if let Some(RenderedMessageSegment::Thinking {
131 content,
132 scroll_handle,
133 }) = self.segments.last_mut()
134 {
135 content.update(cx, |markdown, cx| {
136 markdown.append(text, cx);
137 });
138 scroll_handle.scroll_to_bottom();
139 } else {
140 self.segments.push(RenderedMessageSegment::Thinking {
141 content: parse_markdown(text.into(), self.language_registry.clone(), cx),
142 scroll_handle: ScrollHandle::default(),
143 });
144 }
145 }
146
147 fn append_text(&mut self, text: &String, cx: &mut App) {
148 if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {
149 markdown.update(cx, |markdown, cx| markdown.append(text, cx));
150 } else {
151 self.segments
152 .push(RenderedMessageSegment::Text(parse_markdown(
153 SharedString::from(text),
154 self.language_registry.clone(),
155 cx,
156 )));
157 }
158 }
159
160 fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {
161 match segment {
162 MessageSegment::Thinking { text, .. } => {
163 self.segments.push(RenderedMessageSegment::Thinking {
164 content: parse_markdown(text.into(), self.language_registry.clone(), cx),
165 scroll_handle: ScrollHandle::default(),
166 })
167 }
168 MessageSegment::Text(text) => {
169 self.segments
170 .push(RenderedMessageSegment::Text(parse_markdown(
171 text.into(),
172 self.language_registry.clone(),
173 cx,
174 )))
175 }
176 MessageSegment::RedactedThinking(_) => {}
177 };
178 }
179}
180
181enum RenderedMessageSegment {
182 Thinking {
183 content: Entity<Markdown>,
184 scroll_handle: ScrollHandle,
185 },
186 Text(Entity<Markdown>),
187}
188
189fn parse_markdown(
190 text: SharedString,
191 language_registry: Arc<LanguageRegistry>,
192 cx: &mut App,
193) -> Entity<Markdown> {
194 cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))
195}
196
197pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
198 let theme_settings = ThemeSettings::get_global(cx);
199 let colors = cx.theme().colors();
200 let ui_font_size = TextSize::Default.rems(cx);
201 let buffer_font_size = TextSize::Small.rems(cx);
202 let mut text_style = window.text_style();
203 let line_height = buffer_font_size * 1.75;
204
205 text_style.refine(&TextStyleRefinement {
206 font_family: Some(theme_settings.ui_font.family.clone()),
207 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
208 font_features: Some(theme_settings.ui_font.features.clone()),
209 font_size: Some(ui_font_size.into()),
210 line_height: Some(line_height.into()),
211 color: Some(cx.theme().colors().text),
212 ..Default::default()
213 });
214
215 MarkdownStyle {
216 base_text_style: text_style.clone(),
217 syntax: cx.theme().syntax().clone(),
218 selection_background_color: cx.theme().colors().element_selection_background,
219 code_block_overflow_x_scroll: true,
220 table_overflow_x_scroll: true,
221 heading_level_styles: Some(HeadingLevelStyles {
222 h1: Some(TextStyleRefinement {
223 font_size: Some(rems(1.15).into()),
224 ..Default::default()
225 }),
226 h2: Some(TextStyleRefinement {
227 font_size: Some(rems(1.1).into()),
228 ..Default::default()
229 }),
230 h3: Some(TextStyleRefinement {
231 font_size: Some(rems(1.05).into()),
232 ..Default::default()
233 }),
234 h4: Some(TextStyleRefinement {
235 font_size: Some(rems(1.).into()),
236 ..Default::default()
237 }),
238 h5: Some(TextStyleRefinement {
239 font_size: Some(rems(0.95).into()),
240 ..Default::default()
241 }),
242 h6: Some(TextStyleRefinement {
243 font_size: Some(rems(0.875).into()),
244 ..Default::default()
245 }),
246 }),
247 code_block: StyleRefinement {
248 padding: EdgesRefinement {
249 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
250 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
251 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
252 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
253 },
254 background: Some(colors.editor_background.into()),
255 text: Some(TextStyleRefinement {
256 font_family: Some(theme_settings.buffer_font.family.clone()),
257 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
258 font_features: Some(theme_settings.buffer_font.features.clone()),
259 font_size: Some(buffer_font_size.into()),
260 ..Default::default()
261 }),
262 ..Default::default()
263 },
264 inline_code: TextStyleRefinement {
265 font_family: Some(theme_settings.buffer_font.family.clone()),
266 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
267 font_features: Some(theme_settings.buffer_font.features.clone()),
268 font_size: Some(buffer_font_size.into()),
269 background_color: Some(colors.editor_foreground.opacity(0.08)),
270 ..Default::default()
271 },
272 link: TextStyleRefinement {
273 background_color: Some(colors.editor_foreground.opacity(0.025)),
274 underline: Some(UnderlineStyle {
275 color: Some(colors.text_accent.opacity(0.5)),
276 thickness: px(1.),
277 ..Default::default()
278 }),
279 ..Default::default()
280 },
281 link_callback: Some(Rc::new(move |url, cx| {
282 if MentionLink::is_valid(url) {
283 let colors = cx.theme().colors();
284 Some(TextStyleRefinement {
285 background_color: Some(colors.element_background),
286 ..Default::default()
287 })
288 } else {
289 None
290 }
291 })),
292 ..Default::default()
293 }
294}
295
296fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
297 let theme_settings = ThemeSettings::get_global(cx);
298 let colors = cx.theme().colors();
299 let ui_font_size = TextSize::Default.rems(cx);
300 let buffer_font_size = TextSize::Small.rems(cx);
301 let mut text_style = window.text_style();
302
303 text_style.refine(&TextStyleRefinement {
304 font_family: Some(theme_settings.ui_font.family.clone()),
305 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
306 font_features: Some(theme_settings.ui_font.features.clone()),
307 font_size: Some(ui_font_size.into()),
308 color: Some(cx.theme().colors().text),
309 ..Default::default()
310 });
311
312 MarkdownStyle {
313 base_text_style: text_style,
314 syntax: cx.theme().syntax().clone(),
315 selection_background_color: cx.theme().colors().element_selection_background,
316 code_block_overflow_x_scroll: false,
317 code_block: StyleRefinement {
318 margin: EdgesRefinement::default(),
319 padding: EdgesRefinement::default(),
320 background: Some(colors.editor_background.into()),
321 border_color: None,
322 border_widths: EdgesRefinement::default(),
323 text: Some(TextStyleRefinement {
324 font_family: Some(theme_settings.buffer_font.family.clone()),
325 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
326 font_features: Some(theme_settings.buffer_font.features.clone()),
327 font_size: Some(buffer_font_size.into()),
328 ..Default::default()
329 }),
330 ..Default::default()
331 },
332 inline_code: TextStyleRefinement {
333 font_family: Some(theme_settings.buffer_font.family.clone()),
334 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
335 font_features: Some(theme_settings.buffer_font.features.clone()),
336 font_size: Some(TextSize::XSmall.rems(cx).into()),
337 ..Default::default()
338 },
339 heading: StyleRefinement {
340 text: Some(TextStyleRefinement {
341 font_size: Some(ui_font_size.into()),
342 ..Default::default()
343 }),
344 ..Default::default()
345 },
346 ..Default::default()
347 }
348}
349
350fn render_markdown_code_block(
351 message_id: MessageId,
352 ix: usize,
353 kind: &CodeBlockKind,
354 parsed_markdown: &ParsedMarkdown,
355 metadata: CodeBlockMetadata,
356 active_thread: Entity<ActiveThread>,
357 workspace: WeakEntity<Workspace>,
358 _window: &Window,
359 cx: &App,
360) -> Div {
361 let label_size = rems(0.8125);
362
363 let label = match kind {
364 CodeBlockKind::Indented => None,
365 CodeBlockKind::Fenced => Some(
366 h_flex()
367 .px_1()
368 .gap_1()
369 .child(
370 Icon::new(IconName::Code)
371 .color(Color::Muted)
372 .size(IconSize::XSmall),
373 )
374 .child(div().text_size(label_size).child("Plain Text"))
375 .into_any_element(),
376 ),
377 CodeBlockKind::FencedLang(raw_language_name) => Some(render_code_language(
378 parsed_markdown.languages_by_name.get(raw_language_name),
379 raw_language_name.clone(),
380 cx,
381 )),
382 CodeBlockKind::FencedSrc(path_range) => path_range.path.file_name().map(|file_name| {
383 // We tell the model to use /dev/null for the path instead of using ```language
384 // because otherwise it consistently fails to use code citations.
385 if path_range.path.starts_with("/dev/null") {
386 let ext = path_range
387 .path
388 .extension()
389 .and_then(OsStr::to_str)
390 .map(|str| SharedString::new(str.to_string()))
391 .unwrap_or_default();
392
393 render_code_language(
394 parsed_markdown
395 .languages_by_path
396 .get(&path_range.path)
397 .or_else(|| parsed_markdown.languages_by_name.get(&ext)),
398 ext,
399 cx,
400 )
401 } else {
402 let content = if let Some(parent) = path_range.path.parent() {
403 let file_name = file_name.to_string_lossy().to_string();
404 let path = parent.to_string_lossy().to_string();
405 let path_and_file = format!("{}/{}", path, file_name);
406
407 h_flex()
408 .id(("code-block-header-label", ix))
409 .ml_1()
410 .gap_1()
411 .child(div().text_size(label_size).child(file_name))
412 .child(Label::new(path).color(Color::Muted).size(LabelSize::Small))
413 .tooltip(move |window, cx| {
414 Tooltip::with_meta(
415 "Jump to File",
416 None,
417 path_and_file.clone(),
418 window,
419 cx,
420 )
421 })
422 .into_any_element()
423 } else {
424 div()
425 .ml_1()
426 .text_size(label_size)
427 .child(path_range.path.to_string_lossy().to_string())
428 .into_any_element()
429 };
430
431 h_flex()
432 .id(("code-block-header-button", ix))
433 .w_full()
434 .max_w_full()
435 .px_1()
436 .gap_0p5()
437 .cursor_pointer()
438 .rounded_sm()
439 .hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
440 .child(
441 h_flex()
442 .gap_0p5()
443 .children(
444 file_icons::FileIcons::get_icon(&path_range.path, cx)
445 .map(Icon::from_path)
446 .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
447 )
448 .child(content)
449 .child(
450 Icon::new(IconName::ArrowUpRight)
451 .size(IconSize::XSmall)
452 .color(Color::Ignored),
453 ),
454 )
455 .on_click({
456 let path_range = path_range.clone();
457 move |_, window, cx| {
458 workspace
459 .update(cx, |workspace, cx| {
460 open_path(&path_range, window, workspace, cx)
461 })
462 .ok();
463 }
464 })
465 .into_any_element()
466 }
467 }),
468 };
469
470 let codeblock_was_copied = active_thread
471 .read(cx)
472 .copied_code_block_ids
473 .contains(&(message_id, ix));
474
475 let is_expanded = active_thread.read(cx).is_codeblock_expanded(message_id, ix);
476
477 let codeblock_header_bg = cx
478 .theme()
479 .colors()
480 .element_background
481 .blend(cx.theme().colors().editor_foreground.opacity(0.025));
482
483 let control_buttons = h_flex()
484 .visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
485 .absolute()
486 .top_0()
487 .right_0()
488 .h_full()
489 .bg(codeblock_header_bg)
490 .rounded_tr_md()
491 .px_1()
492 .gap_1()
493 .child(
494 IconButton::new(
495 ("copy-markdown-code", ix),
496 if codeblock_was_copied {
497 IconName::Check
498 } else {
499 IconName::Copy
500 },
501 )
502 .icon_color(Color::Muted)
503 .shape(ui::IconButtonShape::Square)
504 .tooltip(Tooltip::text("Copy Code"))
505 .on_click({
506 let active_thread = active_thread.clone();
507 let parsed_markdown = parsed_markdown.clone();
508 let code_block_range = metadata.content_range.clone();
509 move |_event, _window, cx| {
510 active_thread.update(cx, |this, cx| {
511 this.copied_code_block_ids.insert((message_id, ix));
512
513 let code = parsed_markdown.source()[code_block_range.clone()].to_string();
514 cx.write_to_clipboard(ClipboardItem::new_string(code));
515
516 cx.spawn(async move |this, cx| {
517 cx.background_executor().timer(Duration::from_secs(2)).await;
518
519 cx.update(|cx| {
520 this.update(cx, |this, cx| {
521 this.copied_code_block_ids.remove(&(message_id, ix));
522 cx.notify();
523 })
524 })
525 .ok();
526 })
527 .detach();
528 });
529 }
530 }),
531 )
532 .child(
533 IconButton::new(
534 ("expand-collapse-code", ix),
535 if is_expanded {
536 IconName::ChevronUp
537 } else {
538 IconName::ChevronDown
539 },
540 )
541 .icon_color(Color::Muted)
542 .shape(ui::IconButtonShape::Square)
543 .tooltip(Tooltip::text(if is_expanded {
544 "Collapse Code"
545 } else {
546 "Expand Code"
547 }))
548 .on_click({
549 let active_thread = active_thread.clone();
550 move |_event, _window, cx| {
551 active_thread.update(cx, |this, cx| {
552 this.toggle_codeblock_expanded(message_id, ix);
553 cx.notify();
554 });
555 }
556 }),
557 );
558
559 let codeblock_header = h_flex()
560 .relative()
561 .p_1()
562 .gap_1()
563 .justify_between()
564 .bg(codeblock_header_bg)
565 .map(|this| {
566 if !is_expanded {
567 this.rounded_md()
568 } else {
569 this.rounded_t_md()
570 .border_b_1()
571 .border_color(cx.theme().colors().border.opacity(0.6))
572 }
573 })
574 .children(label)
575 .child(control_buttons);
576
577 v_flex()
578 .group(CODEBLOCK_CONTAINER_GROUP)
579 .my_2()
580 .overflow_hidden()
581 .rounded_md()
582 .border_1()
583 .border_color(cx.theme().colors().border.opacity(0.6))
584 .bg(cx.theme().colors().editor_background)
585 .child(codeblock_header)
586 .when(!is_expanded, |this| this.h(rems_from_px(31.)))
587}
588
589fn open_path(
590 path_range: &PathWithRange,
591 window: &mut Window,
592 workspace: &mut Workspace,
593 cx: &mut Context<'_, Workspace>,
594) {
595 let Some(project_path) = workspace
596 .project()
597 .read(cx)
598 .find_project_path(&path_range.path, cx)
599 else {
600 return; // TODO instead of just bailing out, open that path in a buffer.
601 };
602
603 let Some(target) = path_range.range.as_ref().map(|range| {
604 Point::new(
605 // Line number is 1-based
606 range.start.line.saturating_sub(1),
607 range.start.col.unwrap_or(0),
608 )
609 }) else {
610 return;
611 };
612 let open_task = workspace.open_path(project_path, None, true, window, cx);
613 window
614 .spawn(cx, async move |cx| {
615 let item = open_task.await?;
616 if let Some(active_editor) = item.downcast::<Editor>() {
617 active_editor
618 .update_in(cx, |editor, window, cx| {
619 editor.go_to_singleton_buffer_point(target, window, cx);
620 })
621 .ok();
622 }
623 anyhow::Ok(())
624 })
625 .detach_and_log_err(cx);
626}
627
628fn render_code_language(
629 language: Option<&Arc<Language>>,
630 name_fallback: SharedString,
631 cx: &App,
632) -> AnyElement {
633 let icon_path = language.and_then(|language| {
634 language
635 .config()
636 .matcher
637 .path_suffixes
638 .iter()
639 .find_map(|extension| file_icons::FileIcons::get_icon(Path::new(extension), cx))
640 .map(Icon::from_path)
641 });
642
643 let language_label = language
644 .map(|language| language.name().into())
645 .unwrap_or(name_fallback);
646
647 let label_size = rems(0.8125);
648
649 h_flex()
650 .px_1()
651 .gap_1p5()
652 .children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)))
653 .child(div().text_size(label_size).child(language_label))
654 .into_any_element()
655}
656
657fn open_markdown_link(
658 text: SharedString,
659 workspace: WeakEntity<Workspace>,
660 window: &mut Window,
661 cx: &mut App,
662) {
663 let Some(workspace) = workspace.upgrade() else {
664 cx.open_url(&text);
665 return;
666 };
667
668 match MentionLink::try_parse(&text, &workspace, cx) {
669 Some(MentionLink::File(path, entry)) => workspace.update(cx, |workspace, cx| {
670 if entry.is_dir() {
671 workspace.project().update(cx, |_, cx| {
672 cx.emit(project::Event::RevealInProjectPanel(entry.id));
673 })
674 } else {
675 workspace
676 .open_path(path, None, true, window, cx)
677 .detach_and_log_err(cx);
678 }
679 }),
680 Some(MentionLink::Symbol(path, symbol_name)) => {
681 let open_task = workspace.update(cx, |workspace, cx| {
682 workspace.open_path(path, None, true, window, cx)
683 });
684 window
685 .spawn(cx, async move |cx| {
686 let active_editor = open_task
687 .await?
688 .downcast::<Editor>()
689 .context("Item is not an editor")?;
690 active_editor.update_in(cx, |editor, window, cx| {
691 let symbol_range = editor
692 .buffer()
693 .read(cx)
694 .snapshot(cx)
695 .outline(None)
696 .and_then(|outline| {
697 outline
698 .find_most_similar(&symbol_name)
699 .map(|(_, item)| item.range.clone())
700 })
701 .context("Could not find matching symbol")?;
702
703 editor.change_selections(
704 SelectionEffects::scroll(Autoscroll::center()),
705 window,
706 cx,
707 |s| s.select_anchor_ranges([symbol_range.start..symbol_range.start]),
708 );
709 anyhow::Ok(())
710 })
711 })
712 .detach_and_log_err(cx);
713 }
714 Some(MentionLink::Selection(path, line_range)) => {
715 let open_task = workspace.update(cx, |workspace, cx| {
716 workspace.open_path(path, None, true, window, cx)
717 });
718 window
719 .spawn(cx, async move |cx| {
720 let active_editor = open_task
721 .await?
722 .downcast::<Editor>()
723 .context("Item is not an editor")?;
724 active_editor.update_in(cx, |editor, window, cx| {
725 editor.change_selections(
726 SelectionEffects::scroll(Autoscroll::center()),
727 window,
728 cx,
729 |s| {
730 s.select_ranges([Point::new(line_range.start as u32, 0)
731 ..Point::new(line_range.start as u32, 0)])
732 },
733 );
734 anyhow::Ok(())
735 })
736 })
737 .detach_and_log_err(cx);
738 }
739 Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
740 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
741 panel.update(cx, |panel, cx| {
742 panel
743 .open_thread_by_id(&thread_id, window, cx)
744 .detach_and_log_err(cx)
745 });
746 }
747 }),
748 Some(MentionLink::TextThread(path)) => workspace.update(cx, |workspace, cx| {
749 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
750 panel.update(cx, |panel, cx| {
751 panel
752 .open_saved_prompt_editor(path, window, cx)
753 .detach_and_log_err(cx);
754 });
755 }
756 }),
757 Some(MentionLink::Fetch(url)) => cx.open_url(&url),
758 Some(MentionLink::Rule(prompt_id)) => window.dispatch_action(
759 Box::new(OpenRulesLibrary {
760 prompt_to_select: Some(prompt_id.0),
761 }),
762 cx,
763 ),
764 None => cx.open_url(&text),
765 }
766}
767
768struct EditingMessageState {
769 editor: Entity<Editor>,
770 context_strip: Entity<ContextStrip>,
771 context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
772 last_estimated_token_count: Option<u64>,
773 _subscriptions: [Subscription; 2],
774 _update_token_count_task: Option<Task<()>>,
775 is_agent_message: bool,
776 preprocessing_applied: bool,
777}
778
779impl ActiveThread {
780 pub fn new(
781 thread: Entity<Thread>,
782 thread_store: Entity<ThreadStore>,
783 text_thread_store: Entity<TextThreadStore>,
784 context_store: Entity<ContextStore>,
785 language_registry: Arc<LanguageRegistry>,
786 workspace: WeakEntity<Workspace>,
787 window: &mut Window,
788 cx: &mut Context<Self>,
789 ) -> Self {
790 let subscriptions = vec![
791 cx.observe(&thread, |_, _, cx| cx.notify()),
792 cx.subscribe_in(&thread, window, Self::handle_thread_event),
793 cx.subscribe(&thread_store, Self::handle_rules_loading_error),
794 cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
795 ];
796
797 let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
798 let this = cx.entity().downgrade();
799 move |ix, window: &mut Window, cx: &mut App| {
800 this.update(cx, |this, cx| this.render_message(ix, window, cx))
801 .unwrap()
802 }
803 });
804
805 let workspace_subscription = if let Some(workspace) = workspace.upgrade() {
806 Some(cx.observe_release(&workspace, |this, _, cx| {
807 this.dismiss_notifications(cx);
808 }))
809 } else {
810 None
811 };
812
813 let mut this = Self {
814 language_registry,
815 thread_store,
816 text_thread_store,
817 context_store,
818 thread: thread.clone(),
819 workspace,
820 save_thread_task: None,
821 messages: Vec::new(),
822 rendered_messages_by_id: HashMap::default(),
823 rendered_tool_uses: HashMap::default(),
824 expanded_tool_uses: HashMap::default(),
825 expanded_thinking_segments: HashMap::default(),
826 expanded_code_blocks: HashMap::default(),
827 list_state: list_state.clone(),
828 scrollbar_state: ScrollbarState::new(list_state),
829 show_scrollbar: false,
830 hide_scrollbar_task: None,
831 editing_message: None,
832 last_error: None,
833 copied_code_block_ids: HashSet::default(),
834 notifications: Vec::new(),
835 _subscriptions: subscriptions,
836 notification_subscriptions: HashMap::default(),
837 open_feedback_editors: HashMap::default(),
838 _load_edited_message_context_task: None,
839 };
840
841 for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
842 let rendered_message = RenderedMessage::from_segments(
843 &message.segments,
844 this.language_registry.clone(),
845 cx,
846 );
847 this.push_rendered_message(message.id, rendered_message);
848
849 for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
850 this.render_tool_use_markdown(
851 tool_use.id.clone(),
852 tool_use.ui_text.clone(),
853 &serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
854 tool_use.status.text(),
855 cx,
856 );
857 }
858 }
859
860 if let Some(subscription) = workspace_subscription {
861 this._subscriptions.push(subscription);
862 }
863
864 this
865 }
866
867 pub fn thread(&self) -> &Entity<Thread> {
868 &self.thread
869 }
870
871 pub fn is_empty(&self) -> bool {
872 self.messages.is_empty()
873 }
874
875 pub fn summary<'a>(&'a self, cx: &'a App) -> &'a ThreadSummary {
876 self.thread.read(cx).summary()
877 }
878
879 pub fn regenerate_summary(&self, cx: &mut App) {
880 self.thread.update(cx, |thread, cx| thread.summarize(cx))
881 }
882
883 pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool {
884 self.last_error.take();
885 self.thread.update(cx, |thread, cx| {
886 thread.cancel_last_completion(Some(window.window_handle()), cx)
887 })
888 }
889
890 pub fn last_error(&self) -> Option<ThreadError> {
891 self.last_error.clone()
892 }
893
894 pub fn clear_last_error(&mut self) {
895 self.last_error.take();
896 }
897
898 /// Returns the editing message id and the estimated token count in the content
899 pub fn editing_message_id(&self) -> Option<(MessageId, u64)> {
900 self.editing_message
901 .as_ref()
902 .map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
903 }
904
905 pub fn context_store(&self) -> &Entity<ContextStore> {
906 &self.context_store
907 }
908
909 pub fn thread_store(&self) -> &Entity<ThreadStore> {
910 &self.thread_store
911 }
912
913 pub fn text_thread_store(&self) -> &Entity<TextThreadStore> {
914 &self.text_thread_store
915 }
916
917 fn push_rendered_message(&mut self, id: MessageId, rendered_message: RenderedMessage) {
918 let old_len = self.messages.len();
919 self.messages.push(id);
920 self.list_state.splice(old_len..old_len, 1);
921 self.rendered_messages_by_id.insert(id, rendered_message);
922 }
923
924 fn deleted_message(&mut self, id: &MessageId) {
925 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
926 return;
927 };
928 self.messages.remove(index);
929 self.list_state.splice(index..index + 1, 0);
930 self.rendered_messages_by_id.remove(id);
931 }
932
933 pub fn edit_last_message(&mut self, role: Role, window: &mut Window, cx: &mut Context<Self>) {
934 if self.editing_message_id().is_some() {
935 return;
936 }
937
938 // smit
939 let thread = self.thread().read(cx);
940 let messages: Vec<_> = thread.messages().collect();
941
942 let Some(message) = messages.iter().rev().find(|m| m.role == role) else {
943 return;
944 };
945
946 let (id, segments, creases) = (
947 message.id,
948 message.segments.clone(),
949 message.creases.clone(),
950 );
951
952 match role {
953 Role::Assistant => {
954 self.start_editing_assistant_message(id, &segments, &creases, None, window, cx)
955 }
956 Role::User => {
957 dbg!(&segments);
958
959 if let Some(message_text) = segments.first().and_then(|segment| match segment {
960 MessageSegment::Text(message_text) => {
961 Some(Into::<Arc<str>>::into(message_text.as_str()))
962 }
963 _ => None,
964 }) {
965 dbg!(&message_text);
966
967 self.start_editing_user_message(id, message_text, &creases, None, window, cx)
968 }
969 }
970 _ => {}
971 }
972 }
973
974 fn render_tool_use_markdown(
975 &mut self,
976 tool_use_id: LanguageModelToolUseId,
977 tool_label: impl Into<SharedString>,
978 tool_input: &str,
979 tool_output: SharedString,
980 cx: &mut Context<Self>,
981 ) {
982 let rendered = self
983 .rendered_tool_uses
984 .entry(tool_use_id.clone())
985 .or_insert_with(|| RenderedToolUse {
986 label: cx.new(|cx| {
987 Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
988 }),
989 input: cx.new(|cx| {
990 Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
991 }),
992 output: cx.new(|cx| {
993 Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
994 }),
995 });
996
997 rendered.label.update(cx, |this, cx| {
998 this.replace(tool_label, cx);
999 });
1000 rendered.input.update(cx, |this, cx| {
1001 this.replace(
1002 MarkdownCodeBlock {
1003 tag: "json",
1004 text: tool_input,
1005 }
1006 .to_string(),
1007 cx,
1008 );
1009 });
1010 rendered.output.update(cx, |this, cx| {
1011 this.replace(tool_output, cx);
1012 });
1013 }
1014
1015 fn handle_thread_event(
1016 &mut self,
1017 _thread: &Entity<Thread>,
1018 event: &ThreadEvent,
1019 window: &mut Window,
1020 cx: &mut Context<Self>,
1021 ) {
1022 match event {
1023 ThreadEvent::CancelEditing => {
1024 if self.editing_message.is_some() {
1025 self.cancel_editing_message(&menu::Cancel, window, cx);
1026 }
1027 }
1028 ThreadEvent::ShowError(error) => {
1029 self.last_error = Some(error.clone());
1030 }
1031 ThreadEvent::NewRequest => {
1032 cx.notify();
1033 }
1034 ThreadEvent::CompletionCanceled => {
1035 self.thread.update(cx, |thread, cx| {
1036 thread.project().update(cx, |project, cx| {
1037 project.set_agent_location(None, cx);
1038 })
1039 });
1040 self.workspace
1041 .update(cx, |workspace, cx| {
1042 if workspace.is_being_followed(CollaboratorId::Agent) {
1043 workspace.unfollow(CollaboratorId::Agent, window, cx);
1044 }
1045 })
1046 .ok();
1047 cx.notify();
1048 }
1049 ThreadEvent::StreamedCompletion
1050 | ThreadEvent::SummaryGenerated
1051 | ThreadEvent::SummaryChanged => {
1052 self.save_thread(cx);
1053 }
1054 ThreadEvent::Stopped(reason) => {
1055 match reason {
1056 Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
1057 let used_tools = self.thread.read(cx).used_tools_since_last_user_message();
1058 self.notify_with_sound(
1059 if used_tools {
1060 "Finished running tools"
1061 } else {
1062 "New message"
1063 },
1064 IconName::ZedAssistant,
1065 window,
1066 cx,
1067 );
1068 }
1069 Ok(StopReason::ToolUse) => {
1070 // Don't notify for intermediate tool use
1071 }
1072 Ok(StopReason::Refusal) => {
1073 self.notify_with_sound(
1074 "Language model refused to respond",
1075 IconName::Warning,
1076 window,
1077 cx,
1078 );
1079 }
1080 Err(error) => {
1081 self.notify_with_sound(
1082 "Agent stopped due to an error",
1083 IconName::Warning,
1084 window,
1085 cx,
1086 );
1087
1088 let error_message = error
1089 .chain()
1090 .map(|err| err.to_string())
1091 .collect::<Vec<_>>()
1092 .join("\n");
1093 self.last_error = Some(ThreadError::Message {
1094 header: "Error".into(),
1095 message: error_message.into(),
1096 });
1097 }
1098 }
1099 }
1100 ThreadEvent::ToolConfirmationNeeded => {
1101 self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
1102 }
1103 ThreadEvent::ToolUseLimitReached => {
1104 self.notify_with_sound(
1105 "Consecutive tool use limit reached.",
1106 IconName::Warning,
1107 window,
1108 cx,
1109 );
1110 }
1111 ThreadEvent::StreamedAssistantText(message_id, text) => {
1112 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
1113 rendered_message.append_text(text, cx);
1114 }
1115 }
1116 ThreadEvent::StreamedAssistantThinking(message_id, text) => {
1117 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
1118 rendered_message.append_thinking(text, cx);
1119 }
1120 }
1121 ThreadEvent::MessageAdded(message_id) => {
1122 self.clear_last_error();
1123 if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
1124 thread.message(*message_id).map(|message| {
1125 RenderedMessage::from_segments(
1126 &message.segments,
1127 self.language_registry.clone(),
1128 cx,
1129 )
1130 })
1131 }) {
1132 self.push_rendered_message(*message_id, rendered_message);
1133 }
1134
1135 self.save_thread(cx);
1136 cx.notify();
1137 }
1138 ThreadEvent::MessageEdited(message_id) => {
1139 self.clear_last_error();
1140 if let Some(index) = self.messages.iter().position(|id| id == message_id) {
1141 if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
1142 thread.message(*message_id).map(|message| {
1143 let mut rendered_message = RenderedMessage {
1144 language_registry: self.language_registry.clone(),
1145 segments: Vec::with_capacity(message.segments.len()),
1146 };
1147 for segment in &message.segments {
1148 rendered_message.push_segment(segment, cx);
1149 }
1150 rendered_message
1151 })
1152 }) {
1153 self.list_state.splice(index..index + 1, 1);
1154 self.rendered_messages_by_id
1155 .insert(*message_id, rendered_message);
1156 self.scroll_to_bottom(cx);
1157 self.save_thread(cx);
1158 cx.notify();
1159 }
1160 }
1161 }
1162 ThreadEvent::MessageDeleted(message_id) => {
1163 self.deleted_message(message_id);
1164 self.save_thread(cx);
1165 cx.notify();
1166 }
1167 ThreadEvent::UsePendingTools { tool_uses } => {
1168 for tool_use in tool_uses {
1169 self.render_tool_use_markdown(
1170 tool_use.id.clone(),
1171 tool_use.ui_text.clone(),
1172 &serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1173 "".into(),
1174 cx,
1175 );
1176 }
1177 }
1178 ThreadEvent::StreamedToolUse {
1179 tool_use_id,
1180 ui_text,
1181 input,
1182 } => {
1183 self.render_tool_use_markdown(
1184 tool_use_id.clone(),
1185 ui_text.clone(),
1186 &serde_json::to_string_pretty(&input).unwrap_or_default(),
1187 "".into(),
1188 cx,
1189 );
1190 }
1191 ThreadEvent::ToolFinished {
1192 pending_tool_use, ..
1193 } => {
1194 if let Some(tool_use) = pending_tool_use {
1195 self.render_tool_use_markdown(
1196 tool_use.id.clone(),
1197 tool_use.ui_text.clone(),
1198 &serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1199 self.thread
1200 .read(cx)
1201 .output_for_tool(&tool_use.id)
1202 .map(|output| output.clone().into())
1203 .unwrap_or("".into()),
1204 cx,
1205 );
1206 }
1207 }
1208 ThreadEvent::CheckpointChanged => cx.notify(),
1209 ThreadEvent::ReceivedTextChunk => {}
1210 ThreadEvent::InvalidToolInput {
1211 tool_use_id,
1212 ui_text,
1213 invalid_input_json,
1214 } => {
1215 self.render_tool_use_markdown(
1216 tool_use_id.clone(),
1217 ui_text,
1218 invalid_input_json,
1219 self.thread
1220 .read(cx)
1221 .output_for_tool(tool_use_id)
1222 .map(|output| output.clone().into())
1223 .unwrap_or("".into()),
1224 cx,
1225 );
1226 }
1227 ThreadEvent::MissingToolUse {
1228 tool_use_id,
1229 ui_text,
1230 } => {
1231 self.render_tool_use_markdown(
1232 tool_use_id.clone(),
1233 ui_text,
1234 "",
1235 self.thread
1236 .read(cx)
1237 .output_for_tool(tool_use_id)
1238 .map(|output| output.clone().into())
1239 .unwrap_or("".into()),
1240 cx,
1241 );
1242 }
1243 ThreadEvent::ProfileChanged => {
1244 self.save_thread(cx);
1245 cx.notify();
1246 }
1247 }
1248 }
1249
1250 fn handle_rules_loading_error(
1251 &mut self,
1252 _thread_store: Entity<ThreadStore>,
1253 error: &RulesLoadingError,
1254 cx: &mut Context<Self>,
1255 ) {
1256 self.last_error = Some(ThreadError::Message {
1257 header: "Error loading rules file".into(),
1258 message: error.message.clone(),
1259 });
1260 cx.notify();
1261 }
1262
1263 fn play_notification_sound(&self, window: &Window, cx: &mut App) {
1264 let settings = AgentSettings::get_global(cx);
1265 if settings.play_sound_when_agent_done && !window.is_window_active() {
1266 Audio::play_sound(Sound::AgentDone, cx);
1267 }
1268 }
1269
1270 fn show_notification(
1271 &mut self,
1272 caption: impl Into<SharedString>,
1273 icon: IconName,
1274 window: &mut Window,
1275 cx: &mut Context<ActiveThread>,
1276 ) {
1277 if window.is_window_active() || !self.notifications.is_empty() {
1278 return;
1279 }
1280
1281 let title = self.thread.read(cx).summary().unwrap_or("Agent Panel");
1282
1283 match AgentSettings::get_global(cx).notify_when_agent_waiting {
1284 NotifyWhenAgentWaiting::PrimaryScreen => {
1285 if let Some(primary) = cx.primary_display() {
1286 self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
1287 }
1288 }
1289 NotifyWhenAgentWaiting::AllScreens => {
1290 let caption = caption.into();
1291 for screen in cx.displays() {
1292 self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
1293 }
1294 }
1295 NotifyWhenAgentWaiting::Never => {
1296 // Don't show anything
1297 }
1298 }
1299 }
1300
1301 fn notify_with_sound(
1302 &mut self,
1303 caption: impl Into<SharedString>,
1304 icon: IconName,
1305 window: &mut Window,
1306 cx: &mut Context<ActiveThread>,
1307 ) {
1308 self.play_notification_sound(window, cx);
1309 self.show_notification(caption, icon, window, cx);
1310 }
1311
1312 fn pop_up(
1313 &mut self,
1314 icon: IconName,
1315 caption: SharedString,
1316 title: SharedString,
1317 window: &mut Window,
1318 screen: Rc<dyn PlatformDisplay>,
1319 cx: &mut Context<'_, ActiveThread>,
1320 ) {
1321 let options = AgentNotification::window_options(screen, cx);
1322
1323 let project_name = self.workspace.upgrade().and_then(|workspace| {
1324 workspace
1325 .read(cx)
1326 .project()
1327 .read(cx)
1328 .visible_worktrees(cx)
1329 .next()
1330 .map(|worktree| worktree.read(cx).root_name().to_string())
1331 });
1332
1333 if let Some(screen_window) = cx
1334 .open_window(options, |_, cx| {
1335 cx.new(|_| {
1336 AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
1337 })
1338 })
1339 .log_err()
1340 {
1341 if let Some(pop_up) = screen_window.entity(cx).log_err() {
1342 self.notification_subscriptions
1343 .entry(screen_window)
1344 .or_insert_with(Vec::new)
1345 .push(cx.subscribe_in(&pop_up, window, {
1346 |this, _, event, window, cx| match event {
1347 AgentNotificationEvent::Accepted => {
1348 let handle = window.window_handle();
1349 cx.activate(true);
1350
1351 let workspace_handle = this.workspace.clone();
1352
1353 // If there are multiple Zed windows, activate the correct one.
1354 cx.defer(move |cx| {
1355 handle
1356 .update(cx, |_view, window, _cx| {
1357 window.activate_window();
1358
1359 if let Some(workspace) = workspace_handle.upgrade() {
1360 workspace.update(_cx, |workspace, cx| {
1361 workspace.focus_panel::<AgentPanel>(window, cx);
1362 });
1363 }
1364 })
1365 .log_err();
1366 });
1367
1368 this.dismiss_notifications(cx);
1369 }
1370 AgentNotificationEvent::Dismissed => {
1371 this.dismiss_notifications(cx);
1372 }
1373 }
1374 }));
1375
1376 self.notifications.push(screen_window);
1377
1378 // If the user manually refocuses the original window, dismiss the popup.
1379 self.notification_subscriptions
1380 .entry(screen_window)
1381 .or_insert_with(Vec::new)
1382 .push({
1383 let pop_up_weak = pop_up.downgrade();
1384
1385 cx.observe_window_activation(window, move |_, window, cx| {
1386 if window.is_window_active() {
1387 if let Some(pop_up) = pop_up_weak.upgrade() {
1388 pop_up.update(cx, |_, cx| {
1389 cx.emit(AgentNotificationEvent::Dismissed);
1390 });
1391 }
1392 }
1393 })
1394 });
1395 }
1396 }
1397 }
1398
1399 /// Spawns a task to save the active thread.
1400 ///
1401 /// Only one task to save the thread will be in flight at a time.
1402 fn save_thread(&mut self, cx: &mut Context<Self>) {
1403 let thread = self.thread.clone();
1404 self.save_thread_task = Some(cx.spawn(async move |this, cx| {
1405 let task = this
1406 .update(cx, |this, cx| {
1407 this.thread_store
1408 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
1409 })
1410 .ok();
1411
1412 if let Some(task) = task {
1413 task.await.log_err();
1414 }
1415 }));
1416 }
1417
1418 fn start_editing_user_message(
1419 &mut self,
1420 message_id: MessageId,
1421 message_text: impl Into<Arc<str>>,
1422 message_creases: &[MessageCrease],
1423 text_offset: Option<usize>,
1424 window: &mut Window,
1425 cx: &mut Context<Self>,
1426 ) {
1427 let editor = crate::message_editor::create_editor(
1428 self.workspace.clone(),
1429 self.context_store.downgrade(),
1430 self.thread_store.downgrade(),
1431 self.text_thread_store.downgrade(),
1432 EDIT_PREVIOUS_MESSAGE_MIN_LINES,
1433 None,
1434 window,
1435 cx,
1436 );
1437
1438 editor.update(cx, |editor, cx| {
1439 editor.set_text(message_text, window, cx);
1440 insert_message_creases(editor, message_creases, &self.context_store, window, cx);
1441 editor.focus_handle(cx).focus(window);
1442
1443 if let Some(offset) = text_offset {
1444 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1445 let text = buffer_snapshot.text();
1446 let clamped_offset = offset.min(text.len());
1447 let point = buffer_snapshot.offset_to_point(clamped_offset);
1448 let anchor = buffer_snapshot.anchor_before(point);
1449 editor.change_selections(
1450 SelectionEffects::scroll(Autoscroll::center()),
1451 window,
1452 cx,
1453 |s| s.select_ranges([anchor..anchor]),
1454 );
1455 } else {
1456 editor.move_to_end(&MoveToEnd, window, cx);
1457 }
1458 });
1459
1460 let buffer_edited_subscription = cx.subscribe(&editor, |this, _, event, cx| {
1461 if matches!(event, EditorEvent::BufferEdited) {
1462 this.update_editing_message_token_count(true, cx);
1463 }
1464 });
1465
1466 let context_picker_menu_handle = PopoverMenuHandle::default();
1467 let context_strip = cx.new(|cx| {
1468 ContextStrip::new(
1469 self.context_store.clone(),
1470 self.workspace.clone(),
1471 Some(self.thread_store.downgrade()),
1472 Some(self.text_thread_store.downgrade()),
1473 context_picker_menu_handle.clone(),
1474 SuggestContextKind::File,
1475 ModelUsageContext::Thread(self.thread.clone()),
1476 window,
1477 cx,
1478 )
1479 });
1480
1481 let context_strip_subscription =
1482 cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
1483
1484 self.editing_message = Some((
1485 message_id,
1486 EditingMessageState {
1487 editor: editor.clone(),
1488 context_strip,
1489 context_picker_menu_handle,
1490 last_estimated_token_count: None,
1491 _subscriptions: [buffer_edited_subscription, context_strip_subscription],
1492 _update_token_count_task: None,
1493 is_agent_message: false,
1494 preprocessing_applied: false,
1495 },
1496 ));
1497 self.update_editing_message_token_count(false, cx);
1498
1499 if let Some(message_index) = self.messages.iter().position(|id| *id == message_id) {
1500 self.list_state.scroll_to_reveal_item(message_index);
1501 }
1502
1503 cx.notify();
1504 }
1505
1506 fn start_editing_assistant_message_at_segment(
1507 &mut self,
1508 message_id: MessageId,
1509 segment_index: usize,
1510 segment_offset: usize,
1511 window: &mut Window,
1512 cx: &mut Context<Self>,
1513 ) {
1514 let thread = self.thread.read(cx);
1515 let Some(message) = thread.message(message_id) else {
1516 return;
1517 };
1518 let message_segments = message.segments.clone();
1519 let message_creases = message.creases.clone();
1520
1521 let total_offset = segment_offset
1522 + message_segments
1523 .iter()
1524 .filter(|s| !matches!(s, MessageSegment::RedactedThinking(_)))
1525 .take(segment_index)
1526 .map(|s| match s {
1527 MessageSegment::Text(text) | MessageSegment::Thinking { text, .. } => {
1528 text.len() + 2 // \n\n
1529 }
1530 _ => 0,
1531 })
1532 .sum::<usize>();
1533
1534 self.start_editing_assistant_message(
1535 message_id,
1536 &message_segments,
1537 &message_creases,
1538 Some(total_offset),
1539 window,
1540 cx,
1541 );
1542 }
1543
1544 pub fn start_editing_assistant_message(
1545 &mut self,
1546 message_id: MessageId,
1547 message_segments: &[MessageSegment],
1548 message_creases: &[MessageCrease],
1549 text_offset: Option<usize>,
1550 window: &mut Window,
1551 cx: &mut Context<Self>,
1552 ) {
1553 let message_text = message_segments
1554 .iter()
1555 .filter_map(|segment| match segment {
1556 MessageSegment::Text(text) => Some(text.clone()),
1557 MessageSegment::Thinking { text, .. } => {
1558 Some(format!("<think>\n{}\n</think>", text))
1559 }
1560 MessageSegment::RedactedThinking(_) => None,
1561 })
1562 .collect::<Vec<_>>()
1563 .join("\n\n");
1564
1565 let preprocessed_text = PATH_CODE_BLOCK_REGEX
1566 .replace_all(&message_text, "```$2 $1")
1567 .to_string();
1568 let preprocessing_applied = preprocessed_text != message_text;
1569
1570 let editor = cx.new(|cx| {
1571 let buffer =
1572 cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local("", cx)), cx));
1573 let mut e = Editor::new(
1574 editor::EditorMode::AutoHeight {
1575 min_lines: 1,
1576 max_lines: Some(ASSISTANT_EDITOR_MAX_LINES),
1577 },
1578 buffer,
1579 None,
1580 window,
1581 cx,
1582 );
1583 e.set_show_line_numbers(EditorSettings::get_global(cx).gutter.line_numbers, cx);
1584 e.set_show_indent_guides(true, cx);
1585 e.set_soft_wrap();
1586 e.set_use_modal_editing(true);
1587 e
1588 });
1589
1590 editor.update(cx, |e, cx| {
1591 e.set_text(message_text, window, cx);
1592 insert_message_creases(e, message_creases, &self.context_store, window, cx);
1593 e.focus_handle(cx).focus(window);
1594 });
1595
1596 if let Some(offset) = text_offset {
1597 let point = editor.update(cx, |editor, cx| {
1598 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1599 let text = buffer_snapshot.text();
1600 let clamped_offset = offset.min(text.len());
1601 buffer_snapshot.offset_to_point(clamped_offset)
1602 });
1603 Self::focus_editor_and_center_on_point(&editor, point, window, cx);
1604 } else {
1605 let point = editor.update(cx, |editor, cx| {
1606 editor.move_to_end(&MoveToEnd, window, cx);
1607 editor.selections.newest(cx).head()
1608 });
1609 Self::focus_editor_and_center_on_point(&editor, point, window, cx);
1610 }
1611
1612 if preprocessing_applied {
1613 editor.update(cx, |editor, cx| {
1614 let current_selection: text::Selection<usize> =
1615 editor.selections.newest(cx).clone();
1616 editor.set_text(preprocessed_text, window, cx);
1617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1618 s.select_ranges([current_selection.range()])
1619 });
1620 });
1621 }
1622 let (reg, ed) = (self.language_registry.clone(), editor.clone());
1623 cx.spawn(async move |_, cx| {
1624 if let Ok(md) = reg.language_for_name("Markdown").await {
1625 ed.update(cx, |e, cx| {
1626 e.buffer().update(cx, |buf, cx| {
1627 for b in buf.all_buffers() {
1628 b.update(cx, |b, cx| {
1629 b.set_language_registry(reg.clone());
1630 b.set_language(Some(md.clone()), cx);
1631 });
1632 }
1633 })
1634 })
1635 .ok();
1636 }
1637 })
1638 .detach();
1639
1640 let buffer_edited_subscription = cx.subscribe(&editor, |this, _, event, cx| {
1641 if matches!(event, EditorEvent::BufferEdited) {
1642 this.update_editing_message_token_count(true, cx);
1643 }
1644 });
1645
1646 let mut state =
1647 self.create_common_editing_state(message_id, editor.clone(), true, window, cx);
1648 state.preprocessing_applied = preprocessing_applied;
1649 state._subscriptions[0] = buffer_edited_subscription;
1650
1651 self.editing_message = Some((message_id, state));
1652 self.update_editing_message_token_count(false, cx);
1653 cx.notify();
1654 }
1655
1656 fn create_common_editing_state(
1657 &mut self,
1658 _message_id: MessageId,
1659 editor: Entity<Editor>,
1660 is_assistant_message: bool,
1661 window: &mut Window,
1662 cx: &mut Context<Self>,
1663 ) -> EditingMessageState {
1664 let handle = PopoverMenuHandle::default();
1665 let context_strip = cx.new(|cx| {
1666 ContextStrip::new(
1667 self.context_store.clone(),
1668 self.workspace.clone(),
1669 Some(self.thread_store.downgrade()),
1670 Some(self.text_thread_store.downgrade()),
1671 handle.clone(),
1672 SuggestContextKind::File,
1673 ModelUsageContext::Thread(self.thread.clone()),
1674 window,
1675 cx,
1676 )
1677 });
1678
1679 let context_strip_subscription =
1680 cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
1681
1682 let editor_weak = editor.downgrade();
1683 let list_state_clone = self.list_state.clone();
1684 let editor_subscription = cx.subscribe_in(
1685 &editor,
1686 window,
1687 move |this: &mut ActiveThread, _editor, event: &EditorEvent, _window, cx| {
1688 if let EditorEvent::ScrollPositionChanged { .. } = event {
1689 if let Some((_, editing_state)) = &this.editing_message {
1690 if editing_state.is_agent_message {
1691 if let Some(editor) = editor_weak.upgrade() {
1692 // Sync panel scroll to editor scroll
1693 let scroll_anchor = editor.read(cx).scroll_manager.anchor();
1694 let editor_y = scroll_anchor.offset.y;
1695 list_state_clone.scroll_to(ListOffset {
1696 item_ix: 0,
1697 offset_in_item: px(editor_y),
1698 });
1699 cx.notify();
1700 }
1701 }
1702 }
1703 }
1704 },
1705 );
1706
1707 EditingMessageState {
1708 editor,
1709 is_agent_message: is_assistant_message,
1710 context_strip,
1711 context_picker_menu_handle: handle,
1712 last_estimated_token_count: None,
1713 _subscriptions: [editor_subscription, context_strip_subscription],
1714 _update_token_count_task: None,
1715 preprocessing_applied: false,
1716 }
1717 }
1718
1719 fn handle_context_strip_event(
1720 &mut self,
1721 _context_strip: &Entity<ContextStrip>,
1722 event: &ContextStripEvent,
1723 window: &mut Window,
1724 cx: &mut Context<Self>,
1725 ) {
1726 if let Some((_, state)) = self.editing_message.as_ref() {
1727 match event {
1728 ContextStripEvent::PickerDismissed
1729 | ContextStripEvent::BlurredEmpty
1730 | ContextStripEvent::BlurredDown => {
1731 let editor_focus_handle = state.editor.focus_handle(cx);
1732 window.focus(&editor_focus_handle);
1733 }
1734 ContextStripEvent::BlurredUp => {}
1735 }
1736 }
1737 }
1738
1739 fn focus_editor_and_center_on_point(
1740 editor: &Entity<Editor>,
1741 point: Point,
1742 window: &mut Window,
1743 cx: &mut Context<Self>,
1744 ) {
1745 editor.update(cx, |editor, cx| {
1746 let snapshot = editor.buffer().read(cx).snapshot(cx);
1747 let anchor = snapshot.anchor_before(point);
1748 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1749 s.select_ranges([anchor..anchor]);
1750 });
1751 });
1752
1753 // Autoscroll after text is set
1754 let editor_handle = editor.downgrade();
1755 cx.on_next_frame(window, move |_, window, cx| {
1756 if window.is_window_active() {
1757 if let Some(editor) = editor_handle.upgrade() {
1758 editor.update(cx, |editor, cx| {
1759 let snapshot = editor.buffer().read(cx).snapshot(cx);
1760 let anchor = snapshot.anchor_before(point);
1761 editor.change_selections(
1762 SelectionEffects::scroll(Autoscroll::center()),
1763 window,
1764 cx,
1765 |s| {
1766 s.select_ranges([anchor..anchor]);
1767 },
1768 );
1769 });
1770 }
1771 }
1772 });
1773 }
1774
1775 fn update_editing_message_token_count(&mut self, debounce: bool, cx: &mut Context<Self>) {
1776 let Some((message_id, state)) = self.editing_message.as_mut() else {
1777 return;
1778 };
1779
1780 cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
1781 state._update_token_count_task.take();
1782
1783 let Some(configured_model) = self.thread.read(cx).configured_model() else {
1784 state.last_estimated_token_count.take();
1785 return;
1786 };
1787
1788 let editor = state.editor.clone();
1789 let thread = self.thread.clone();
1790 let message_id = *message_id;
1791
1792 state._update_token_count_task = Some(cx.spawn(async move |this, cx| {
1793 if debounce {
1794 cx.background_executor()
1795 .timer(Duration::from_millis(200))
1796 .await;
1797 }
1798
1799 let token_count = if let Some(task) = cx
1800 .update(|cx| {
1801 let Some(message) = thread.read(cx).message(message_id) else {
1802 log::error!("Message that was being edited no longer exists");
1803 return None;
1804 };
1805 let message_text = editor.read(cx).text(cx);
1806
1807 if message_text.is_empty() && message.loaded_context.is_empty() {
1808 return None;
1809 }
1810
1811 let mut request_message = LanguageModelRequestMessage {
1812 role: language_model::Role::User,
1813 content: Vec::new(),
1814 cache: false,
1815 };
1816
1817 message
1818 .loaded_context
1819 .add_to_request_message(&mut request_message);
1820
1821 if !message_text.is_empty() {
1822 request_message
1823 .content
1824 .push(MessageContent::Text(message_text));
1825 }
1826
1827 let request = language_model::LanguageModelRequest {
1828 thread_id: None,
1829 prompt_id: None,
1830 intent: None,
1831 mode: None,
1832 messages: vec![request_message],
1833 tools: vec![],
1834 tool_choice: None,
1835 stop: vec![],
1836 temperature: AgentSettings::temperature_for_model(
1837 &configured_model.model,
1838 cx,
1839 ),
1840 thinking_allowed: true,
1841 };
1842
1843 Some(configured_model.model.count_tokens(request, cx))
1844 })
1845 .ok()
1846 .flatten()
1847 {
1848 task.await.log_err()
1849 } else {
1850 Some(0)
1851 };
1852
1853 if let Some(token_count) = token_count {
1854 this.update(cx, |this, cx| {
1855 let Some((_message_id, state)) = this.editing_message.as_mut() else {
1856 return;
1857 };
1858
1859 state.last_estimated_token_count = Some(token_count);
1860 cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
1861 })
1862 .ok();
1863 };
1864 }));
1865 }
1866
1867 fn toggle_context_picker(
1868 &mut self,
1869 _: &crate::ToggleContextPicker,
1870 window: &mut Window,
1871 cx: &mut Context<Self>,
1872 ) {
1873 if let Some((_, state)) = self.editing_message.as_mut() {
1874 let handle = state.context_picker_menu_handle.clone();
1875 window.defer(cx, move |window, cx| {
1876 handle.toggle(window, cx);
1877 });
1878 }
1879 }
1880
1881 fn remove_all_context(
1882 &mut self,
1883 _: &crate::RemoveAllContext,
1884 _window: &mut Window,
1885 cx: &mut Context<Self>,
1886 ) {
1887 self.context_store.update(cx, |store, cx| store.clear(cx));
1888 cx.notify();
1889 }
1890
1891 fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
1892 if let Some((_, state)) = self.editing_message.as_mut() {
1893 if state.context_picker_menu_handle.is_deployed() {
1894 cx.propagate();
1895 } else {
1896 state.context_strip.focus_handle(cx).focus(window);
1897 }
1898 }
1899 }
1900
1901 fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
1902 attach_pasted_images_as_context(&self.context_store, cx);
1903 }
1904
1905 fn cancel_editing_message(
1906 &mut self,
1907 _: &menu::Cancel,
1908 window: &mut Window,
1909 cx: &mut Context<Self>,
1910 ) {
1911 self.editing_message.take();
1912 cx.notify();
1913
1914 if let Some(workspace) = self.workspace.upgrade() {
1915 workspace.update(cx, |workspace, cx| {
1916 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
1917 panel.focus_handle(cx).focus(window);
1918 }
1919 });
1920 }
1921 }
1922
1923 fn confirm_editing_message(
1924 &mut self,
1925 _: &menu::Confirm,
1926 window: &mut Window,
1927 cx: &mut Context<Self>,
1928 ) {
1929 let Some((message_id, state)) = self.editing_message.take() else {
1930 return;
1931 };
1932
1933 let Some(model) = self
1934 .thread
1935 .update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
1936 else {
1937 return;
1938 };
1939
1940 if model.provider.must_accept_terms(cx) {
1941 cx.notify();
1942 return;
1943 }
1944
1945 let edited_text = state.editor.read(cx).text(cx);
1946
1947 let restored_text = if state.preprocessing_applied {
1948 LANG_PREFIX_REGEX
1949 .replace_all(&edited_text, "```$2")
1950 .to_string()
1951 } else {
1952 edited_text
1953 };
1954
1955 let creases = state.editor.update(cx, extract_message_creases);
1956
1957 let new_context = self
1958 .context_store
1959 .read(cx)
1960 .new_context_for_thread(self.thread.read(cx), Some(message_id));
1961
1962 let project = self.thread.read(cx).project().clone();
1963 let prompt_store = self.thread_store.read(cx).prompt_store().clone();
1964
1965 let git_store = project.read(cx).git_store().clone();
1966 let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
1967
1968 let load_context_task = context::load_context(new_context, &project, &prompt_store, cx);
1969 self._load_edited_message_context_task =
1970 Some(cx.spawn_in(window, async move |this, cx| {
1971 let (context, checkpoint) =
1972 futures::future::join(load_context_task, checkpoint).await;
1973 this.update_in(cx, |this, window, cx| {
1974 let original_role = this
1975 .thread
1976 .read(cx)
1977 .message(message_id)
1978 .map(|m| m.role)
1979 .unwrap_or(Role::User);
1980
1981 this.thread.update(cx, |thread, cx| {
1982 let segments = vec![MessageSegment::Text(restored_text)];
1983
1984 thread.edit_message(
1985 message_id,
1986 original_role,
1987 segments,
1988 creases,
1989 Some(context.loaded_context),
1990 checkpoint.ok(),
1991 cx,
1992 );
1993 if original_role == Role::User {
1994 let messages_to_delete = this.messages_after(message_id).to_vec();
1995 for msg_id in messages_to_delete {
1996 thread.delete_message(msg_id, cx);
1997 }
1998 thread.advance_prompt_id();
1999 thread.cancel_last_completion(Some(window.window_handle()), cx);
2000 thread.send_to_model(
2001 model.model,
2002 CompletionIntent::UserPrompt,
2003 Some(window.window_handle()),
2004 cx,
2005 );
2006 }
2007 });
2008 this._load_edited_message_context_task = None;
2009 cx.notify();
2010 })
2011 .log_err();
2012 }));
2013
2014 if let Some(workspace) = self.workspace.upgrade() {
2015 workspace.update(cx, |workspace, cx| {
2016 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
2017 panel.focus_handle(cx).focus(window);
2018 }
2019 });
2020 }
2021 }
2022
2023 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
2024 self.messages
2025 .iter()
2026 .position(|id| *id == message_id)
2027 .map(|index| &self.messages[index + 1..])
2028 .unwrap_or(&[])
2029 }
2030
2031 fn save_editing_message(
2032 &mut self,
2033 _: &menu::SaveEdit,
2034 window: &mut Window,
2035 cx: &mut Context<Self>,
2036 ) {
2037 let Some((message_id, state)) = self.editing_message.take() else {
2038 return;
2039 };
2040
2041 let edited_text = state.editor.read(cx).text(cx);
2042
2043 let new_context = self
2044 .context_store
2045 .read(cx)
2046 .new_context_for_thread(self.thread.read(cx), Some(message_id));
2047
2048 let project = self.thread.read(cx).project().clone();
2049 let prompt_store = self.thread_store.read(cx).prompt_store().clone();
2050
2051 let git_store = project.read(cx).git_store().clone();
2052 let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
2053
2054 let creases = state.editor.update(cx, extract_message_creases);
2055
2056 let load_context_task = context::load_context(new_context, &project, &prompt_store, cx);
2057 self._load_edited_message_context_task =
2058 Some(cx.spawn_in(window, async move |this, cx| {
2059 let (context, checkpoint) =
2060 futures::future::join(load_context_task, checkpoint).await;
2061 this.update_in(cx, |this, _window, cx| {
2062 let original_role = this
2063 .thread
2064 .read(cx)
2065 .message(message_id)
2066 .map(|m| m.role)
2067 .unwrap_or(Role::User);
2068
2069 this.thread.update(cx, |thread, cx| {
2070 thread.edit_message(
2071 message_id,
2072 original_role,
2073 vec![MessageSegment::Text(edited_text)],
2074 creases,
2075 Some(context.loaded_context),
2076 checkpoint.ok(),
2077 cx,
2078 );
2079 });
2080 this._load_edited_message_context_task = None;
2081 cx.notify();
2082 })
2083 .log_err();
2084 }));
2085 }
2086
2087 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
2088 self.cancel_editing_message(&menu::Cancel, window, cx);
2089 }
2090
2091 fn handle_save_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
2092 self.save_editing_message(&menu::SaveEdit, window, cx);
2093 }
2094
2095 fn handle_regenerate_click(
2096 &mut self,
2097 _: &ClickEvent,
2098 window: &mut Window,
2099 cx: &mut Context<Self>,
2100 ) {
2101 self.confirm_editing_message(&menu::Confirm, window, cx);
2102 }
2103
2104 fn handle_feedback_click(
2105 &mut self,
2106 message_id: MessageId,
2107 feedback: ThreadFeedback,
2108 window: &mut Window,
2109 cx: &mut Context<Self>,
2110 ) {
2111 let report = self.thread.update(cx, |thread, cx| {
2112 thread.report_message_feedback(message_id, feedback, cx)
2113 });
2114
2115 cx.spawn(async move |this, cx| {
2116 report.await?;
2117 this.update(cx, |_this, cx| cx.notify())
2118 })
2119 .detach_and_log_err(cx);
2120
2121 match feedback {
2122 ThreadFeedback::Positive => {
2123 self.open_feedback_editors.remove(&message_id);
2124 }
2125 ThreadFeedback::Negative => {
2126 self.handle_show_feedback_comments(message_id, window, cx);
2127 }
2128 }
2129 }
2130
2131 fn handle_show_feedback_comments(
2132 &mut self,
2133 message_id: MessageId,
2134 window: &mut Window,
2135 cx: &mut Context<Self>,
2136 ) {
2137 let buffer = cx.new(|cx| {
2138 let empty_string = String::new();
2139 MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
2140 });
2141
2142 let editor = cx.new(|cx| {
2143 let mut editor = Editor::new(
2144 editor::EditorMode::AutoHeight {
2145 min_lines: 1,
2146 max_lines: Some(4),
2147 },
2148 buffer,
2149 None,
2150 window,
2151 cx,
2152 );
2153 editor.set_placeholder_text(
2154 "What went wrong? Share your feedback so we can improve.",
2155 cx,
2156 );
2157 editor
2158 });
2159
2160 editor.read(cx).focus_handle(cx).focus(window);
2161 self.open_feedback_editors.insert(message_id, editor);
2162 cx.notify();
2163 }
2164
2165 fn submit_feedback_message(&mut self, message_id: MessageId, cx: &mut Context<Self>) {
2166 let Some(editor) = self.open_feedback_editors.get(&message_id) else {
2167 return;
2168 };
2169
2170 let report_task = self.thread.update(cx, |thread, cx| {
2171 thread.report_message_feedback(message_id, ThreadFeedback::Negative, cx)
2172 });
2173
2174 let comments = editor.read(cx).text(cx);
2175 if !comments.is_empty() {
2176 let thread_id = self.thread.read(cx).id().clone();
2177 let comments_value = String::from(comments.as_str());
2178
2179 let message_content = self
2180 .thread
2181 .read(cx)
2182 .message(message_id)
2183 .map(|msg| msg.to_string())
2184 .unwrap_or_default();
2185
2186 telemetry::event!(
2187 "Assistant Thread Feedback Comments",
2188 thread_id,
2189 message_id = message_id.as_usize(),
2190 message_content,
2191 comments = comments_value
2192 );
2193
2194 self.open_feedback_editors.remove(&message_id);
2195
2196 cx.spawn(async move |this, cx| {
2197 report_task.await?;
2198 this.update(cx, |_this, cx| cx.notify())
2199 })
2200 .detach_and_log_err(cx);
2201 }
2202 }
2203
2204 fn render_edit_message_editor(
2205 &self,
2206 state: &EditingMessageState,
2207 _window: &mut Window,
2208 cx: &Context<Self>,
2209 ) -> impl IntoElement {
2210 let settings = ThemeSettings::get_global(cx);
2211 let editor_settings = editor::EditorSettings::get_global(cx);
2212
2213 let font_size = TextSize::Small
2214 .rems(cx)
2215 .to_pixels(settings.agent_font_size(cx));
2216 let line_height = font_size * 1.75;
2217 let colors = cx.theme().colors();
2218
2219 let text_style = TextStyle {
2220 color: colors.text,
2221 font_family: settings.buffer_font.family.clone(),
2222 font_fallbacks: settings.buffer_font.fallbacks.clone(),
2223 font_features: settings.buffer_font.features.clone(),
2224 font_size: font_size.into(),
2225 line_height: line_height.into(),
2226 ..Default::default()
2227 };
2228
2229 let editor_style = EditorStyle {
2230 background: colors.editor_background,
2231 local_player: cx.theme().players().local(),
2232 text: text_style,
2233 syntax: cx.theme().syntax().clone(),
2234 ..Default::default()
2235 };
2236
2237 let editor_element = div()
2238 .pt(px(-3.))
2239 .when(
2240 state.is_agent_message && !editor_settings.gutter.line_numbers,
2241 |d| d.px_neg_0p5(),
2242 )
2243 .w_full()
2244 .child(EditorElement::new(&state.editor, editor_style))
2245 .into_any_element();
2246
2247 v_flex()
2248 .key_context("EditMessageEditor")
2249 .on_action(cx.listener(Self::toggle_context_picker))
2250 .on_action(cx.listener(Self::remove_all_context))
2251 .on_action(cx.listener(Self::move_up))
2252 .capture_action(cx.listener(Self::paste))
2253 .w_full()
2254 .gap_1()
2255 .when(state.is_agent_message, |container| {
2256 container
2257 .capture_action(cx.listener(Self::cancel_editing_message))
2258 .capture_action(cx.listener(Self::save_editing_message))
2259 })
2260 .when(!state.is_agent_message, |container| {
2261 container
2262 .on_action(cx.listener(Self::cancel_editing_message))
2263 .on_action(cx.listener(Self::confirm_editing_message))
2264 .on_action(cx.listener(Self::save_editing_message))
2265 .child(state.context_strip.clone())
2266 })
2267 .child(editor_element)
2268
2269 // v_flex()
2270 // .key_context("EditMessageEditor")
2271 // .on_action(cx.listener(Self::toggle_context_picker))
2272 // .on_action(cx.listener(Self::remove_all_context))
2273 // .on_action(cx.listener(Self::move_up))
2274 // .on_action(cx.listener(Self::cancel_editing_message))
2275 // .on_action(cx.listener(Self::confirm_editing_message))
2276 // .capture_action(cx.listener(Self::paste))
2277 // .min_h_6()
2278 // .w_full()
2279 // .flex_grow()
2280 // .gap_2()
2281 // .child(state.context_strip.clone())
2282 // .child(div().pt(px(-3.)).px_neg_0p5().child(EditorElement::new(
2283 // &state.editor,
2284 // EditorStyle {
2285 // background: colors.editor_background,
2286 // local_player: cx.theme().players().local(),
2287 // text: text_style,
2288 // syntax: cx.theme().syntax().clone(),
2289 // ..Default::default()
2290 // },
2291 // )))
2292 }
2293
2294 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
2295 let message_id = self.messages[ix];
2296 let workspace = self.workspace.clone();
2297 let thread = self.thread.read(cx);
2298
2299 let is_first_message = ix == 0;
2300 let is_last_message = ix == self.messages.len() - 1;
2301
2302 let Some(message) = thread.message(message_id) else {
2303 return Empty.into_any();
2304 };
2305
2306 let is_generating = thread.is_generating();
2307 let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
2308
2309 let loading_dots = (is_generating && is_last_message).then(|| {
2310 h_flex()
2311 .h_8()
2312 .my_3()
2313 .mx_5()
2314 .when(is_generating_stale || message.is_hidden, |this| {
2315 this.child(LoadingLabel::new("").size(LabelSize::Small))
2316 })
2317 });
2318
2319 if message.is_hidden {
2320 return div().children(loading_dots).into_any();
2321 }
2322
2323 let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
2324 return Empty.into_any();
2325 };
2326
2327 // Get all the data we need from thread before we start using it in closures
2328 let checkpoint = thread.checkpoint_for_message(message_id);
2329 let configured_model = thread.configured_model().map(|m| m.model);
2330 let added_context = thread
2331 .context_for_message(message_id)
2332 .map(|context| AddedContext::new_attached(context, configured_model.as_ref(), cx))
2333 .collect::<Vec<_>>();
2334
2335 let tool_uses = thread.tool_uses_for_message(message_id, cx);
2336 let has_tool_uses = !tool_uses.is_empty();
2337
2338 let editing_message_state = self
2339 .editing_message
2340 .as_ref()
2341 .filter(|(id, _)| *id == message_id)
2342 .map(|(_, state)| state);
2343
2344 let (editor_bg_color, panel_bg) = {
2345 let colors = cx.theme().colors();
2346 (colors.editor_background, colors.panel_background)
2347 };
2348
2349 let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText)
2350 .icon_size(IconSize::XSmall)
2351 .icon_color(Color::Ignored)
2352 .tooltip(Tooltip::text("Open Thread as Markdown"))
2353 .on_click({
2354 let thread = self.thread.clone();
2355 let workspace = self.workspace.clone();
2356 move |_, window, cx| {
2357 if let Some(workspace) = workspace.upgrade() {
2358 open_active_thread_as_markdown(thread.clone(), workspace, window, cx)
2359 .detach_and_log_err(cx);
2360 }
2361 }
2362 });
2363
2364 let scroll_to_top = IconButton::new(("scroll_to_top", ix), IconName::ArrowUpAlt)
2365 .icon_size(IconSize::XSmall)
2366 .icon_color(Color::Ignored)
2367 .tooltip(Tooltip::text("Scroll To Top"))
2368 .on_click(cx.listener(move |this, _, _, cx| {
2369 this.scroll_to_top(cx);
2370 }));
2371
2372 let show_feedback = thread.is_turn_end(ix);
2373 let feedback_container = h_flex()
2374 .group("feedback_container")
2375 .mt_1()
2376 .py_2()
2377 .px(RESPONSE_PADDING_X)
2378 .mr_1()
2379 .opacity(0.4)
2380 .hover(|style| style.opacity(1.))
2381 .gap_1p5()
2382 .flex_wrap()
2383 .justify_end();
2384 let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
2385 Some(feedback) => feedback_container
2386 .child(
2387 div().visible_on_hover("feedback_container").child(
2388 Label::new(match feedback {
2389 ThreadFeedback::Positive => "Thanks for your feedback!",
2390 ThreadFeedback::Negative => {
2391 "We appreciate your feedback and will use it to improve."
2392 }
2393 })
2394 .color(Color::Muted)
2395 .size(LabelSize::XSmall)
2396 .truncate())
2397 )
2398 .child(
2399 h_flex()
2400 .child(
2401 IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
2402 .icon_size(IconSize::XSmall)
2403 .icon_color(match feedback {
2404 ThreadFeedback::Positive => Color::Accent,
2405 ThreadFeedback::Negative => Color::Ignored,
2406 })
2407 .tooltip(Tooltip::text("Helpful Response"))
2408 .on_click(cx.listener(move |this, _, window, cx| {
2409 this.handle_feedback_click(
2410 message_id,
2411 ThreadFeedback::Positive,
2412 window,
2413 cx,
2414 );
2415 })),
2416 )
2417 .child(
2418 IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
2419 .icon_size(IconSize::XSmall)
2420 .icon_color(match feedback {
2421 ThreadFeedback::Positive => Color::Ignored,
2422 ThreadFeedback::Negative => Color::Accent,
2423 })
2424 .tooltip(Tooltip::text("Not Helpful"))
2425 .on_click(cx.listener(move |this, _, window, cx| {
2426 this.handle_feedback_click(
2427 message_id,
2428 ThreadFeedback::Negative,
2429 window,
2430 cx,
2431 );
2432 })),
2433 )
2434 .child(open_as_markdown),
2435 )
2436 .into_any_element(),
2437 None if AgentSettings::get_global(cx).enable_feedback =>
2438 feedback_container
2439 .child(
2440 div().visible_on_hover("feedback_container").child(
2441 Label::new(
2442 "Rating the thread sends all of your current conversation to the Zed team.",
2443 )
2444 .color(Color::Muted)
2445 .size(LabelSize::XSmall)
2446 .truncate())
2447 )
2448 .child(
2449 h_flex()
2450 .child(
2451 IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
2452 .icon_size(IconSize::XSmall)
2453 .icon_color(Color::Ignored)
2454 .tooltip(Tooltip::text("Helpful Response"))
2455 .on_click(cx.listener(move |this, _, window, cx| {
2456 this.handle_feedback_click(
2457 message_id,
2458 ThreadFeedback::Positive,
2459 window,
2460 cx,
2461 );
2462 })),
2463 )
2464 .child(
2465 IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
2466 .icon_size(IconSize::XSmall)
2467 .icon_color(Color::Ignored)
2468 .tooltip(Tooltip::text("Not Helpful"))
2469 .on_click(cx.listener(move |this, _, window, cx| {
2470 this.handle_feedback_click(
2471 message_id,
2472 ThreadFeedback::Negative,
2473 window,
2474 cx,
2475 );
2476 })),
2477 )
2478 .child(open_as_markdown)
2479 .child(scroll_to_top),
2480 )
2481 .into_any_element(),
2482 None => feedback_container
2483 .child(h_flex()
2484 .child(open_as_markdown))
2485 .child(scroll_to_top)
2486 .into_any_element(),
2487 };
2488
2489 let message_is_empty = message.should_display_content();
2490 let has_content = !message_is_empty || !added_context.is_empty();
2491
2492 let message_content = has_content.then(|| {
2493 if let Some(state) = editing_message_state.as_ref() {
2494 self.render_edit_message_editor(state, window, cx)
2495 .into_any_element()
2496 } else {
2497 let base_content_element = v_flex()
2498 .w_full()
2499 .gap_1()
2500 .when(!added_context.is_empty(), |parent| {
2501 parent.child(h_flex().flex_wrap().gap_1().children(
2502 added_context.into_iter().map(|added_context| {
2503 let context = added_context.handle.clone();
2504 ContextPill::added(added_context, false, false, None).on_click(
2505 Rc::new(cx.listener({
2506 let workspace = workspace.clone();
2507 move |_, _, window, cx| {
2508 if let Some(workspace) = workspace.upgrade() {
2509 open_context(&context, workspace, window, cx);
2510 cx.notify();
2511 }
2512 }
2513 })),
2514 )
2515 }),
2516 ))
2517 })
2518 .when(!message_is_empty, |parent| {
2519 parent.child(
2520 div().pt_0p5().min_h_6().child(
2521 self.render_message_content(
2522 message_id,
2523 rendered_message,
2524 has_tool_uses,
2525 workspace.clone(),
2526 window,
2527 cx,
2528 )
2529 .into_any_element(),
2530 ),
2531 )
2532 });
2533
2534 if message.role == Role::Assistant && editing_message_state.is_none() {
2535 base_content_element
2536 .id(("assistant_content_wrapper", ix))
2537 .into_any_element()
2538 } else {
2539 base_content_element.into_any_element()
2540 }
2541 }
2542 });
2543
2544 let styled_message = if message.ui_only {
2545 self.render_ui_notification(message_content, ix, cx)
2546 } else {
2547 match message.role {
2548 Role::User => {
2549 let colors = cx.theme().colors();
2550 v_flex()
2551 .id(("message-container", ix))
2552 .pt_2()
2553 .pl_2()
2554 .pr_2p5()
2555 .pb_4()
2556 .child(
2557 v_flex()
2558 .id(("user-message", ix))
2559 .bg(editor_bg_color)
2560 .rounded_lg()
2561 .shadow_md()
2562 .border_1()
2563 .border_color(colors.border)
2564 .hover(|hover| hover.border_color(colors.text_accent.opacity(0.5)))
2565 .child(
2566 v_flex()
2567 .p_2p5()
2568 .gap_1()
2569 .children(message_content)
2570 .when_some(editing_message_state, |this, state| {
2571 let focus_handle = state.editor.focus_handle(cx).clone();
2572
2573 this.child(
2574 h_flex()
2575 .w_full()
2576 .gap_1()
2577 .justify_between()
2578 .flex_wrap()
2579 .child(
2580 h_flex()
2581 .gap_1p5()
2582 .child(
2583 div()
2584 .opacity(0.8)
2585 .child(
2586 Icon::new(IconName::Warning)
2587 .size(IconSize::Indicator)
2588 .color(Color::Warning)
2589 ),
2590 )
2591 .child(
2592 Label::new("Editing will restart the thread from this point.")
2593 .color(Color::Muted)
2594 .size(LabelSize::XSmall),
2595 ),
2596 )
2597 .child(
2598 h_flex()
2599 .gap_0p5()
2600 .child(
2601 IconButton::new(
2602 "cancel-edit-message",
2603 IconName::Close,
2604 )
2605 .shape(ui::IconButtonShape::Square)
2606 .icon_color(Color::Error)
2607 .icon_size(IconSize::Small)
2608 .tooltip({
2609 let focus_handle = focus_handle.clone();
2610 move |window, cx| {
2611 Tooltip::for_action_in(
2612 "Cancel Edit",
2613 &menu::Cancel,
2614 &focus_handle,
2615 window,
2616 cx,
2617 )
2618 }
2619 })
2620 .on_click(cx.listener(Self::handle_cancel_click)),
2621 )
2622 .child(
2623 IconButton::new(
2624 "save-edit-message",
2625 IconName::Save,
2626 )
2627 .disabled(state.editor.read(cx).is_empty(cx))
2628 .shape(ui::IconButtonShape::Square)
2629 .icon_color(Color::Muted)
2630 .icon_size(IconSize::Small)
2631 // .tooltip({
2632 // let focus_handle = focus_handle.clone();
2633 // move |window, cx| {
2634 // Tooltip::for_action_in(
2635 // "Only Save",
2636 // &menu::SaveEdit,
2637 // &focus_handle,
2638 // window,
2639 // cx,
2640 // )
2641 // }
2642 // })
2643 .on_click(cx.listener(Self::handle_save_click)),
2644 )
2645 .child(
2646 IconButton::new(
2647 "confirm-edit-message",
2648 IconName::Return,
2649 )
2650 .disabled(state.editor.read(cx).is_empty(cx))
2651 .shape(ui::IconButtonShape::Square)
2652 .icon_color(Color::Muted)
2653 .icon_size(IconSize::Small)
2654 .tooltip({
2655 let focus_handle = focus_handle.clone();
2656 move |window, cx| {
2657 Tooltip::for_action_in(
2658 "Regenerate",
2659 &menu::Confirm,
2660 &focus_handle,
2661 window,
2662 cx,
2663 )
2664 }
2665 })
2666 .on_click(
2667 cx.listener(Self::handle_regenerate_click),
2668 ),
2669 ),
2670 )
2671 )
2672 }),
2673 )
2674 .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
2675 // .on_click(cx.listener({
2676 // let message_creases = message.creases.clone();
2677 // move |this, _, window, cx| {
2678 // if let Some(message_text) =
2679 // this.thread.read(cx).message(message_id).and_then(|message| {
2680 // message.segments.first().and_then(|segment| {
2681 // match segment {
2682 // MessageSegment::Text(message_text) => {
2683 // Some(Into::<Arc<str>>::into(message_text.as_str()))
2684 // }
2685 // _ => {
2686 // None
2687 // }
2688 // }
2689 // })
2690 // })
2691 // {
2692 // this.start_editing_message(
2693 // message_id,
2694 // message_text,
2695 // &message_creases,
2696 // window,
2697 // cx,
2698 // );
2699 // }
2700 // }
2701 // })),
2702 )
2703 }
2704 Role::Assistant => v_flex()
2705 .id(("message-container", ix))
2706 .px(RESPONSE_PADDING_X)
2707 .gap_2()
2708 .children(message_content)
2709 .when(has_tool_uses, |parent| {
2710 parent.children(tool_uses.into_iter().map(|tool_use| {
2711 self.render_tool_use(tool_use, window, workspace.clone(), cx)
2712 }))
2713 })
2714 .when_some(editing_message_state, |this, state| {
2715 let focus_handle = state.editor.focus_handle(cx);
2716 this.child(
2717 h_flex()
2718 .justify_end()
2719 .gap_0p5()
2720 .child(
2721 IconButton::new("cancel-edit-message", IconName::Close)
2722 .shape(ui::IconButtonShape::Square)
2723 .icon_color(Color::Error)
2724 .icon_size(IconSize::Small)
2725 .tooltip({
2726 let focus_handle = focus_handle.clone();
2727 move |window, cx| {
2728 Tooltip::for_action_in(
2729 "Cancel Edit",
2730 &menu::Cancel,
2731 &focus_handle,
2732 window,
2733 cx,
2734 )
2735 }
2736 })
2737 .on_click(cx.listener(Self::handle_cancel_click)),
2738 )
2739 .child(
2740 IconButton::new("save-edit-message", IconName::Save)
2741 .disabled(state.editor.read(cx).is_empty(cx))
2742 .shape(ui::IconButtonShape::Square)
2743 .icon_color(Color::Muted)
2744 .icon_size(IconSize::Small)
2745 .tooltip({
2746 let focus_handle = focus_handle.clone();
2747 move |window, cx| {
2748 Tooltip::for_action_in(
2749 "Save Edit",
2750 &menu::SaveEdit,
2751 &focus_handle,
2752 window,
2753 cx,
2754 )
2755 }
2756 })
2757 .on_click(cx.listener(Self::handle_save_click)),
2758 ),
2759 )
2760 }),
2761 Role::System => {
2762 let colors = cx.theme().colors();
2763 div().id(("message-container", ix)).py_1().px_2().child(
2764 v_flex()
2765 .bg(colors.editor_background)
2766 .rounded_sm()
2767 .child(div().p_4().children(message_content))
2768 .when_some(editing_message_state, |this, state| {
2769 let focus_handle = state.editor.focus_handle(cx);
2770 this.child(
2771 h_flex()
2772 .justify_end()
2773 .gap_0p5()
2774 .child(
2775 IconButton::new("cancel-edit-message", IconName::Close)
2776 .shape(ui::IconButtonShape::Square)
2777 .icon_color(Color::Error)
2778 .icon_size(IconSize::Small)
2779 .tooltip({
2780 let focus_handle = focus_handle.clone();
2781 move |window, cx| {
2782 Tooltip::for_action_in(
2783 "Cancel Edit",
2784 &menu::Cancel,
2785 &focus_handle,
2786 window,
2787 cx,
2788 )
2789 }
2790 })
2791 .on_click(cx.listener(Self::handle_cancel_click)),
2792 )
2793 .child(
2794 IconButton::new("save-edit-message", IconName::Save)
2795 .disabled(state.editor.read(cx).is_empty(cx))
2796 .shape(ui::IconButtonShape::Square)
2797 .icon_color(Color::Accent)
2798 .icon_size(IconSize::Small)
2799 .tooltip({
2800 let focus_handle = focus_handle.clone();
2801 move |window, cx| {
2802 Tooltip::for_action_in(
2803 "Save Edit",
2804 &menu::SaveEdit,
2805 &focus_handle,
2806 window,
2807 cx,
2808 )
2809 }
2810 })
2811 .on_click(cx.listener(Self::handle_save_click)),
2812 ),
2813 )
2814 }),
2815 )
2816 }
2817 }
2818 };
2819
2820 let after_editing_message = self
2821 .editing_message
2822 .as_ref()
2823 .map_or(false, |(editing_message_id, _)| {
2824 message_id > *editing_message_id
2825 });
2826
2827 let backdrop = div()
2828 .id(("backdrop", ix))
2829 .size_full()
2830 .absolute()
2831 .inset_0()
2832 .bg(panel_bg)
2833 .opacity(0.8)
2834 .block_mouse_except_scroll()
2835 .on_click(cx.listener(Self::handle_cancel_click));
2836
2837 v_flex()
2838 .w_full()
2839 .map(|parent| {
2840 if let Some(checkpoint) = checkpoint.filter(|_| !is_generating) {
2841 let mut is_pending = false;
2842 let mut error = None;
2843 if let Some(last_restore_checkpoint) =
2844 self.thread.read(cx).last_restore_checkpoint()
2845 {
2846 if last_restore_checkpoint.message_id() == message_id {
2847 match last_restore_checkpoint {
2848 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
2849 LastRestoreCheckpoint::Error { error: err, .. } => {
2850 error = Some(err.clone());
2851 }
2852 }
2853 }
2854 }
2855
2856 let restore_checkpoint_button =
2857 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
2858 .icon(if error.is_some() {
2859 IconName::XCircle
2860 } else {
2861 IconName::Undo
2862 })
2863 .icon_size(IconSize::XSmall)
2864 .icon_position(IconPosition::Start)
2865 .icon_color(if error.is_some() {
2866 Some(Color::Error)
2867 } else {
2868 None
2869 })
2870 .label_size(LabelSize::XSmall)
2871 .disabled(is_pending)
2872 .on_click(cx.listener(move |this, _, _window, cx| {
2873 this.thread.update(cx, |thread, cx| {
2874 thread
2875 .restore_checkpoint(checkpoint.clone(), cx)
2876 .detach_and_log_err(cx);
2877 });
2878 }));
2879
2880 let restore_checkpoint_button = if is_pending {
2881 restore_checkpoint_button
2882 .with_animation(
2883 ("pulsating-restore-checkpoint-button", ix),
2884 Animation::new(Duration::from_secs(2))
2885 .repeat()
2886 .with_easing(pulsating_between(0.6, 1.)),
2887 |label, delta| label.alpha(delta),
2888 )
2889 .into_any_element()
2890 } else if let Some(error) = error {
2891 restore_checkpoint_button
2892 .tooltip(Tooltip::text(error.to_string()))
2893 .into_any_element()
2894 } else {
2895 restore_checkpoint_button.into_any_element()
2896 };
2897
2898 parent.child(
2899 h_flex()
2900 .pt_2p5()
2901 .px_2p5()
2902 .w_full()
2903 .gap_1()
2904 .child(ui::Divider::horizontal())
2905 .child(restore_checkpoint_button)
2906 .child(ui::Divider::horizontal()),
2907 )
2908 } else {
2909 parent
2910 }
2911 })
2912 .when(is_first_message, |parent| {
2913 parent.child(self.render_rules_item(cx))
2914 })
2915 .child(styled_message)
2916 .children(loading_dots)
2917 .when(show_feedback, move |parent| {
2918 parent.child(feedback_items).when_some(
2919 self.open_feedback_editors.get(&message_id),
2920 move |parent, feedback_editor| {
2921 let focus_handle = feedback_editor.focus_handle(cx);
2922 parent.child(
2923 v_flex()
2924 .key_context("AgentFeedbackMessageEditor")
2925 .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
2926 this.open_feedback_editors.remove(&message_id);
2927 cx.notify();
2928 }))
2929 .on_action(cx.listener(move |this, _: &menu::Confirm, _, cx| {
2930 this.submit_feedback_message(message_id, cx);
2931 cx.notify();
2932 }))
2933 .on_action(cx.listener(Self::confirm_editing_message))
2934 .mb_2()
2935 .mx_4()
2936 .p_2()
2937 .rounded_md()
2938 .border_1()
2939 .border_color(cx.theme().colors().border)
2940 .bg(cx.theme().colors().editor_background)
2941 .child(feedback_editor.clone())
2942 .child(
2943 h_flex()
2944 .gap_1()
2945 .justify_end()
2946 .child(
2947 Button::new("dismiss-feedback-message", "Cancel")
2948 .label_size(LabelSize::Small)
2949 .key_binding(
2950 KeyBinding::for_action_in(
2951 &menu::Cancel,
2952 &focus_handle,
2953 window,
2954 cx,
2955 )
2956 .map(|kb| kb.size(rems_from_px(10.))),
2957 )
2958 .on_click(cx.listener(
2959 move |this, _, _window, cx| {
2960 this.open_feedback_editors
2961 .remove(&message_id);
2962 cx.notify();
2963 },
2964 )),
2965 )
2966 .child(
2967 Button::new(
2968 "submit-feedback-message",
2969 "Share Feedback",
2970 )
2971 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
2972 .label_size(LabelSize::Small)
2973 .key_binding(
2974 KeyBinding::for_action_in(
2975 &menu::Confirm,
2976 &focus_handle,
2977 window,
2978 cx,
2979 )
2980 .map(|kb| kb.size(rems_from_px(10.))),
2981 )
2982 .on_click(
2983 cx.listener(move |this, _, _window, cx| {
2984 this.submit_feedback_message(message_id, cx);
2985 cx.notify()
2986 }),
2987 ),
2988 ),
2989 ),
2990 )
2991 },
2992 )
2993 })
2994 .when(after_editing_message, |parent| {
2995 // Backdrop to dim out the whole thread below the editing user message
2996 parent.relative().child(backdrop)
2997 })
2998 .into_any()
2999 }
3000
3001 fn render_message_content(
3002 &self,
3003 message_id: MessageId,
3004 rendered_message: &RenderedMessage,
3005 has_tool_uses: bool,
3006 workspace: WeakEntity<Workspace>,
3007 window: &Window,
3008 cx: &Context<Self>,
3009 ) -> impl IntoElement {
3010 let is_last_message = self.messages.last() == Some(&message_id);
3011 let is_generating = self.thread.read(cx).is_generating();
3012 let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses {
3013 rendered_message
3014 .segments
3015 .iter()
3016 .enumerate()
3017 .next_back()
3018 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
3019 .map(|(index, _)| index)
3020 } else {
3021 None
3022 };
3023
3024 let message_role = self
3025 .thread
3026 .read(cx)
3027 .message(message_id)
3028 .map(|m| m.role)
3029 .unwrap_or(Role::User);
3030
3031 let is_assistant_message = message_role == Role::Assistant;
3032 let is_user_message = message_role == Role::User;
3033
3034 v_flex()
3035 .text_ui(cx)
3036 .gap_2()
3037 .when(is_user_message, |this| this.text_xs())
3038 .children(
3039 rendered_message.segments.iter().enumerate().map(
3040 |(index, segment)| match segment {
3041 RenderedMessageSegment::Thinking {
3042 content,
3043 scroll_handle,
3044 } => self
3045 .render_message_thinking_segment(
3046 message_id,
3047 index,
3048 content.clone(),
3049 &scroll_handle,
3050 Some(index) == pending_thinking_segment_index,
3051 window,
3052 cx,
3053 )
3054 .into_any_element(),
3055 RenderedMessageSegment::Text(markdown) => {
3056 let markdown_element = MarkdownElement::new(
3057 markdown.clone(),
3058 if is_user_message {
3059 let mut style = default_markdown_style(window, cx);
3060 let mut text_style = window.text_style();
3061 let theme_settings = ThemeSettings::get_global(cx);
3062
3063 let buffer_font = theme_settings.buffer_font.family.clone();
3064 let buffer_font_size = TextSize::Small.rems(cx);
3065
3066 text_style.refine(&TextStyleRefinement {
3067 font_family: Some(buffer_font),
3068 font_size: Some(buffer_font_size.into()),
3069 ..Default::default()
3070 });
3071
3072 style.base_text_style = text_style;
3073 style
3074 } else {
3075 default_markdown_style(window, cx)
3076 },
3077 );
3078
3079 let markdown_element = if is_assistant_message {
3080 markdown_element.code_block_renderer(
3081 markdown::CodeBlockRenderer::Custom {
3082 render: Arc::new({
3083 let workspace = workspace.clone();
3084 let active_thread = cx.entity();
3085 move |kind,
3086 parsed_markdown,
3087 range,
3088 metadata,
3089 window,
3090 cx| {
3091 render_markdown_code_block(
3092 message_id,
3093 range.start,
3094 kind,
3095 parsed_markdown,
3096 metadata,
3097 active_thread.clone(),
3098 workspace.clone(),
3099 window,
3100 cx,
3101 )
3102 }
3103 }),
3104 transform: Some(Arc::new({
3105 let active_thread = cx.entity();
3106
3107 move |element, range, _, _, cx| {
3108 let is_expanded = active_thread
3109 .read(cx)
3110 .is_codeblock_expanded(message_id, range.start);
3111
3112 if is_expanded {
3113 return element;
3114 }
3115
3116 element
3117 }
3118 })),
3119 },
3120 )
3121 } else {
3122 markdown_element.code_block_renderer(
3123 markdown::CodeBlockRenderer::Default {
3124 copy_button: false,
3125 copy_button_on_hover: false,
3126 border: true,
3127 },
3128 )
3129 };
3130
3131 let markdown_element = markdown_element.on_url_click({
3132 let workspace = self.workspace.clone();
3133 move |text, window, cx| {
3134 open_markdown_link(text, workspace.clone(), window, cx);
3135 }
3136 });
3137
3138 if is_assistant_message {
3139 let active_thread = cx.entity();
3140 let markdown_element =
3141 markdown_element.on_click(move |text_offset, window, cx| {
3142 if window.modifiers().secondary() {
3143 active_thread.update(cx, |this, cx| {
3144 this.start_editing_assistant_message_at_segment(
3145 message_id,
3146 index,
3147 text_offset,
3148 window,
3149 cx,
3150 );
3151 });
3152 }
3153 });
3154
3155 div()
3156 .id(("assistant-message-text", message_id.as_usize()))
3157 .child(markdown_element)
3158 .into_any_element()
3159 } else {
3160 let active_thread = cx.entity();
3161 let markdown_element =
3162 markdown_element.on_click(move |text_offset, window, cx| {
3163 let message_data = active_thread
3164 .read(cx)
3165 .thread
3166 .read(cx)
3167 .message(message_id)
3168 .map(|m| (m.segments.clone(), m.creases.clone()));
3169
3170 if let Some((segments, creases)) = message_data {
3171 if let Some(message_text) =
3172 segments.first().and_then(|segment| match segment {
3173 MessageSegment::Text(message_text) => {
3174 Some(Into::<Arc<str>>::into(
3175 message_text.as_str(),
3176 ))
3177 }
3178 _ => None,
3179 })
3180 {
3181 active_thread.update(cx, |this, inner_cx| {
3182 this.start_editing_user_message(
3183 message_id,
3184 message_text,
3185 &creases,
3186 Some(text_offset),
3187 window,
3188 inner_cx,
3189 );
3190 });
3191 }
3192 }
3193 });
3194 div().child(markdown_element).into_any_element()
3195 }
3196 }
3197 },
3198 ),
3199 )
3200 }
3201
3202 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
3203 cx.theme().colors().border.opacity(0.5)
3204 }
3205
3206 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
3207 cx.theme()
3208 .colors()
3209 .element_background
3210 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
3211 }
3212
3213 fn render_ui_notification(
3214 &self,
3215 message_content: impl IntoIterator<Item = impl IntoElement>,
3216 ix: usize,
3217 cx: &mut Context<Self>,
3218 ) -> Stateful<Div> {
3219 let message = div()
3220 .flex_1()
3221 .min_w_0()
3222 .text_size(TextSize::XSmall.rems(cx))
3223 .text_color(cx.theme().colors().text_muted)
3224 .children(message_content);
3225
3226 div()
3227 .id(("message-container", ix))
3228 .py_1()
3229 .px_2p5()
3230 .child(Banner::new().severity(ui::Severity::Warning).child(message))
3231 }
3232
3233 fn render_message_thinking_segment(
3234 &self,
3235 message_id: MessageId,
3236 ix: usize,
3237 markdown: Entity<Markdown>,
3238 scroll_handle: &ScrollHandle,
3239 pending: bool,
3240 window: &Window,
3241 cx: &Context<Self>,
3242 ) -> impl IntoElement {
3243 let is_open = self
3244 .expanded_thinking_segments
3245 .get(&(message_id, ix))
3246 .copied()
3247 .unwrap_or_default();
3248
3249 let editor_bg = cx.theme().colors().panel_background;
3250
3251 div().map(|this| {
3252 if pending {
3253 this.v_flex()
3254 .mt_neg_2()
3255 .mb_1p5()
3256 .child(
3257 h_flex()
3258 .group("disclosure-header")
3259 .justify_between()
3260 .child(
3261 h_flex()
3262 .gap_1p5()
3263 .child(
3264 Icon::new(IconName::ToolBulb)
3265 .size(IconSize::Small)
3266 .color(Color::Muted),
3267 )
3268 .child(LoadingLabel::new("Thinking").size(LabelSize::Small)),
3269 )
3270 .child(
3271 h_flex()
3272 .gap_1()
3273 .child(
3274 div().visible_on_hover("disclosure-header").child(
3275 Disclosure::new("thinking-disclosure", is_open)
3276 .opened_icon(IconName::ChevronUp)
3277 .closed_icon(IconName::ChevronDown)
3278 .on_click(cx.listener({
3279 move |this, _event, _window, _cx| {
3280 let is_open = this
3281 .expanded_thinking_segments
3282 .entry((message_id, ix))
3283 .or_insert(false);
3284
3285 *is_open = !*is_open;
3286 }
3287 })),
3288 ),
3289 )
3290 .child({
3291 Icon::new(IconName::ArrowCircle)
3292 .color(Color::Accent)
3293 .size(IconSize::Small)
3294 .with_animation(
3295 "arrow-circle",
3296 Animation::new(Duration::from_secs(2)).repeat(),
3297 |icon, delta| {
3298 icon.transform(Transformation::rotate(
3299 percentage(delta),
3300 ))
3301 },
3302 )
3303 }),
3304 ),
3305 )
3306 .when(!is_open, |this| {
3307 let gradient_overlay = div()
3308 .rounded_b_lg()
3309 .h_full()
3310 .absolute()
3311 .w_full()
3312 .bottom_0()
3313 .left_0()
3314 .bg(linear_gradient(
3315 180.,
3316 linear_color_stop(editor_bg, 1.),
3317 linear_color_stop(editor_bg.opacity(0.2), 0.),
3318 ));
3319
3320 this.child(
3321 div()
3322 .relative()
3323 .bg(editor_bg)
3324 .rounded_b_lg()
3325 .mt_2()
3326 .pl_4()
3327 .child(
3328 div()
3329 .id(("thinking-content", ix))
3330 .max_h_20()
3331 .track_scroll(scroll_handle)
3332 .text_ui_sm(cx)
3333 .overflow_hidden()
3334 .child(
3335 MarkdownElement::new(
3336 markdown.clone(),
3337 default_markdown_style(window, cx),
3338 )
3339 .on_url_click({
3340 let workspace = self.workspace.clone();
3341 move |text, window, cx| {
3342 open_markdown_link(
3343 text,
3344 workspace.clone(),
3345 window,
3346 cx,
3347 );
3348 }
3349 }),
3350 ),
3351 )
3352 .child(gradient_overlay),
3353 )
3354 })
3355 .when(is_open, |this| {
3356 this.child(
3357 div()
3358 .id(("thinking-content", ix))
3359 .h_full()
3360 .bg(editor_bg)
3361 .text_ui_sm(cx)
3362 .child(
3363 MarkdownElement::new(
3364 markdown.clone(),
3365 default_markdown_style(window, cx),
3366 )
3367 .on_url_click({
3368 let workspace = self.workspace.clone();
3369 move |text, window, cx| {
3370 open_markdown_link(text, workspace.clone(), window, cx);
3371 }
3372 }),
3373 ),
3374 )
3375 })
3376 } else {
3377 this.v_flex()
3378 .mt_neg_2()
3379 .child(
3380 h_flex()
3381 .group("disclosure-header")
3382 .pr_1()
3383 .justify_between()
3384 .opacity(0.8)
3385 .hover(|style| style.opacity(1.))
3386 .child(
3387 h_flex()
3388 .gap_1p5()
3389 .child(
3390 Icon::new(IconName::LightBulb)
3391 .size(IconSize::XSmall)
3392 .color(Color::Muted),
3393 )
3394 .child(Label::new("Thought Process").size(LabelSize::Small)),
3395 )
3396 .child(
3397 div().visible_on_hover("disclosure-header").child(
3398 Disclosure::new("thinking-disclosure", is_open)
3399 .opened_icon(IconName::ChevronUp)
3400 .closed_icon(IconName::ChevronDown)
3401 .on_click(cx.listener({
3402 move |this, _event, _window, _cx| {
3403 let is_open = this
3404 .expanded_thinking_segments
3405 .entry((message_id, ix))
3406 .or_insert(false);
3407
3408 *is_open = !*is_open;
3409 }
3410 })),
3411 ),
3412 ),
3413 )
3414 .child(
3415 div()
3416 .id(("thinking-content", ix))
3417 .relative()
3418 .mt_1p5()
3419 .ml_1p5()
3420 .pl_2p5()
3421 .border_l_1()
3422 .border_color(cx.theme().colors().border_variant)
3423 .text_ui_sm(cx)
3424 .when(is_open, |this| {
3425 this.child(
3426 MarkdownElement::new(
3427 markdown.clone(),
3428 default_markdown_style(window, cx),
3429 )
3430 .on_url_click({
3431 let workspace = self.workspace.clone();
3432 move |text, window, cx| {
3433 open_markdown_link(text, workspace.clone(), window, cx);
3434 }
3435 }),
3436 )
3437 }),
3438 )
3439 }
3440 })
3441 }
3442
3443 fn render_tool_use(
3444 &self,
3445 tool_use: ToolUse,
3446 window: &mut Window,
3447 workspace: WeakEntity<Workspace>,
3448 cx: &mut Context<Self>,
3449 ) -> impl IntoElement + use<> {
3450 if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
3451 return card.render(&tool_use.status, window, workspace, cx);
3452 }
3453
3454 let is_open = self
3455 .expanded_tool_uses
3456 .get(&tool_use.id)
3457 .copied()
3458 .unwrap_or_default();
3459
3460 let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
3461
3462 let fs = self
3463 .workspace
3464 .upgrade()
3465 .map(|workspace| workspace.read(cx).app_state().fs.clone());
3466 let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
3467 let needs_confirmation_tools = tool_use.needs_confirmation;
3468
3469 let status_icons = div().child(match &tool_use.status {
3470 ToolUseStatus::NeedsConfirmation => {
3471 let icon = Icon::new(IconName::Warning)
3472 .color(Color::Warning)
3473 .size(IconSize::Small);
3474 icon.into_any_element()
3475 }
3476 ToolUseStatus::Pending
3477 | ToolUseStatus::InputStillStreaming
3478 | ToolUseStatus::Running => {
3479 let icon = Icon::new(IconName::ArrowCircle)
3480 .color(Color::Accent)
3481 .size(IconSize::Small);
3482 icon.with_animation(
3483 "arrow-circle",
3484 Animation::new(Duration::from_secs(2)).repeat(),
3485 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
3486 )
3487 .into_any_element()
3488 }
3489 ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
3490 ToolUseStatus::Error(_) => {
3491 let icon = Icon::new(IconName::Close)
3492 .color(Color::Error)
3493 .size(IconSize::Small);
3494 icon.into_any_element()
3495 }
3496 });
3497
3498 let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
3499 let results_content_container = || v_flex().p_2().gap_0p5();
3500
3501 let results_content = v_flex()
3502 .gap_1()
3503 .child(
3504 results_content_container()
3505 .child(
3506 Label::new("Input")
3507 .size(LabelSize::XSmall)
3508 .color(Color::Muted)
3509 .buffer_font(cx),
3510 )
3511 .child(
3512 div()
3513 .w_full()
3514 .text_ui_sm(cx)
3515 .children(rendered_tool_use.as_ref().map(|rendered| {
3516 MarkdownElement::new(
3517 rendered.input.clone(),
3518 tool_use_markdown_style(window, cx),
3519 )
3520 .code_block_renderer(markdown::CodeBlockRenderer::Default {
3521 copy_button: false,
3522 copy_button_on_hover: false,
3523 border: false,
3524 })
3525 .on_url_click({
3526 let workspace = self.workspace.clone();
3527 move |text, window, cx| {
3528 open_markdown_link(text, workspace.clone(), window, cx);
3529 }
3530 })
3531 })),
3532 ),
3533 )
3534 .map(|container| match tool_use.status {
3535 ToolUseStatus::Finished(_) => container.child(
3536 results_content_container()
3537 .border_t_1()
3538 .border_color(self.tool_card_border_color(cx))
3539 .child(
3540 Label::new("Result")
3541 .size(LabelSize::XSmall)
3542 .color(Color::Muted)
3543 .buffer_font(cx),
3544 )
3545 .child(div().w_full().text_ui_sm(cx).children(
3546 rendered_tool_use.as_ref().map(|rendered| {
3547 MarkdownElement::new(
3548 rendered.output.clone(),
3549 tool_use_markdown_style(window, cx),
3550 )
3551 .code_block_renderer(markdown::CodeBlockRenderer::Default {
3552 copy_button: false,
3553 copy_button_on_hover: false,
3554 border: false,
3555 })
3556 .on_url_click({
3557 let workspace = self.workspace.clone();
3558 move |text, window, cx| {
3559 open_markdown_link(text, workspace.clone(), window, cx);
3560 }
3561 })
3562 .into_any_element()
3563 }),
3564 )),
3565 ),
3566 ToolUseStatus::InputStillStreaming | ToolUseStatus::Running => container.child(
3567 results_content_container()
3568 .border_t_1()
3569 .border_color(self.tool_card_border_color(cx))
3570 .child(
3571 h_flex()
3572 .gap_1()
3573 .child(
3574 Icon::new(IconName::ArrowCircle)
3575 .size(IconSize::Small)
3576 .color(Color::Accent)
3577 .with_animation(
3578 "arrow-circle",
3579 Animation::new(Duration::from_secs(2)).repeat(),
3580 |icon, delta| {
3581 icon.transform(Transformation::rotate(percentage(
3582 delta,
3583 )))
3584 },
3585 ),
3586 )
3587 .child(
3588 Label::new("Running…")
3589 .size(LabelSize::XSmall)
3590 .color(Color::Muted)
3591 .buffer_font(cx),
3592 ),
3593 ),
3594 ),
3595 ToolUseStatus::Error(_) => container.child(
3596 results_content_container()
3597 .border_t_1()
3598 .border_color(self.tool_card_border_color(cx))
3599 .child(
3600 Label::new("Error")
3601 .size(LabelSize::XSmall)
3602 .color(Color::Muted)
3603 .buffer_font(cx),
3604 )
3605 .child(
3606 div()
3607 .text_ui_sm(cx)
3608 .children(rendered_tool_use.as_ref().map(|rendered| {
3609 MarkdownElement::new(
3610 rendered.output.clone(),
3611 tool_use_markdown_style(window, cx),
3612 )
3613 .on_url_click({
3614 let workspace = self.workspace.clone();
3615 move |text, window, cx| {
3616 open_markdown_link(text, workspace.clone(), window, cx);
3617 }
3618 })
3619 .into_any_element()
3620 })),
3621 ),
3622 ),
3623 ToolUseStatus::Pending => container,
3624 ToolUseStatus::NeedsConfirmation => container.child(
3625 results_content_container()
3626 .border_t_1()
3627 .border_color(self.tool_card_border_color(cx))
3628 .child(
3629 Label::new("Asking Permission")
3630 .size(LabelSize::Small)
3631 .color(Color::Muted)
3632 .buffer_font(cx),
3633 ),
3634 ),
3635 });
3636
3637 let gradient_overlay = |color: Hsla| {
3638 div()
3639 .h_full()
3640 .absolute()
3641 .w_12()
3642 .bottom_0()
3643 .map(|element| {
3644 if is_status_finished {
3645 element.right_6()
3646 } else {
3647 element.right(px(44.))
3648 }
3649 })
3650 .bg(linear_gradient(
3651 90.,
3652 linear_color_stop(color, 1.),
3653 linear_color_stop(color.opacity(0.2), 0.),
3654 ))
3655 };
3656
3657 v_flex().gap_1().mb_2().map(|element| {
3658 if !needs_confirmation_tools {
3659 element.child(
3660 v_flex()
3661 .child(
3662 h_flex()
3663 .group("disclosure-header")
3664 .relative()
3665 .gap_1p5()
3666 .justify_between()
3667 .opacity(0.8)
3668 .hover(|style| style.opacity(1.))
3669 .when(!is_status_finished, |this| this.pr_2())
3670 .child(
3671 h_flex()
3672 .id("tool-label-container")
3673 .gap_1p5()
3674 .max_w_full()
3675 .overflow_x_scroll()
3676 .child(
3677 Icon::new(tool_use.icon)
3678 .size(IconSize::Small)
3679 .color(Color::Muted),
3680 )
3681 .child(
3682 h_flex().pr_8().text_size(rems(0.8125)).children(
3683 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| {
3684 open_markdown_link(text, workspace.clone(), window, cx);
3685 }}))
3686 ),
3687 ),
3688 )
3689 .child(
3690 h_flex()
3691 .gap_1()
3692 .child(
3693 div().visible_on_hover("disclosure-header").child(
3694 Disclosure::new("tool-use-disclosure", is_open)
3695 .opened_icon(IconName::ChevronUp)
3696 .closed_icon(IconName::ChevronDown)
3697 .on_click(cx.listener({
3698 let tool_use_id = tool_use.id.clone();
3699 move |this, _event, _window, _cx| {
3700 let is_open = this
3701 .expanded_tool_uses
3702 .entry(tool_use_id.clone())
3703 .or_insert(false);
3704
3705 *is_open = !*is_open;
3706 }
3707 })),
3708 ),
3709 )
3710 .child(status_icons),
3711 )
3712 .child(gradient_overlay(cx.theme().colors().panel_background)),
3713 )
3714 .map(|parent| {
3715 if !is_open {
3716 return parent;
3717 }
3718
3719 parent.child(
3720 v_flex()
3721 .mt_1()
3722 .border_1()
3723 .border_color(self.tool_card_border_color(cx))
3724 .bg(cx.theme().colors().editor_background)
3725 .rounded_lg()
3726 .child(results_content),
3727 )
3728 }),
3729 )
3730 } else {
3731 v_flex()
3732 .mb_2()
3733 .rounded_lg()
3734 .border_1()
3735 .border_color(self.tool_card_border_color(cx))
3736 .overflow_hidden()
3737 .child(
3738 h_flex()
3739 .group("disclosure-header")
3740 .relative()
3741 .justify_between()
3742 .py_1()
3743 .map(|element| {
3744 if is_status_finished {
3745 element.pl_2().pr_0p5()
3746 } else {
3747 element.px_2()
3748 }
3749 })
3750 .bg(self.tool_card_header_bg(cx))
3751 .map(|element| {
3752 if is_open {
3753 element.border_b_1().rounded_t_md()
3754 } else if needs_confirmation {
3755 element.rounded_t_md()
3756 } else {
3757 element.rounded_md()
3758 }
3759 })
3760 .border_color(self.tool_card_border_color(cx))
3761 .child(
3762 h_flex()
3763 .id("tool-label-container")
3764 .gap_1p5()
3765 .max_w_full()
3766 .overflow_x_scroll()
3767 .child(
3768 Icon::new(tool_use.icon)
3769 .size(IconSize::XSmall)
3770 .color(Color::Muted),
3771 )
3772 .child(
3773 h_flex().pr_8().text_ui_sm(cx).children(
3774 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| {
3775 open_markdown_link(text, workspace.clone(), window, cx);
3776 }}))
3777 ),
3778 ),
3779 )
3780 .child(
3781 h_flex()
3782 .gap_1()
3783 .child(
3784 div().visible_on_hover("disclosure-header").child(
3785 Disclosure::new("tool-use-disclosure", is_open)
3786 .opened_icon(IconName::ChevronUp)
3787 .closed_icon(IconName::ChevronDown)
3788 .on_click(cx.listener({
3789 let tool_use_id = tool_use.id.clone();
3790 move |this, _event, _window, _cx| {
3791 let is_open = this
3792 .expanded_tool_uses
3793 .entry(tool_use_id.clone())
3794 .or_insert(false);
3795
3796 *is_open = !*is_open;
3797 }
3798 })),
3799 ),
3800 )
3801 .child(status_icons),
3802 )
3803 .child(gradient_overlay(self.tool_card_header_bg(cx))),
3804 )
3805 .map(|parent| {
3806 if !is_open {
3807 return parent;
3808 }
3809
3810 parent.child(
3811 v_flex()
3812 .bg(cx.theme().colors().editor_background)
3813 .map(|element| {
3814 if needs_confirmation {
3815 element.rounded_none()
3816 } else {
3817 element.rounded_b_lg()
3818 }
3819 })
3820 .child(results_content),
3821 )
3822 })
3823 .when(needs_confirmation, |this| {
3824 this.child(
3825 h_flex()
3826 .py_1()
3827 .pl_2()
3828 .pr_1()
3829 .gap_1()
3830 .justify_between()
3831 .flex_wrap()
3832 .bg(cx.theme().colors().editor_background)
3833 .border_t_1()
3834 .border_color(self.tool_card_border_color(cx))
3835 .rounded_b_lg()
3836 .child(
3837 div()
3838 .min_w(rems_from_px(145.))
3839 .child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)
3840 )
3841 )
3842 .child(
3843 h_flex()
3844 .gap_0p5()
3845 .child({
3846 let tool_id = tool_use.id.clone();
3847 Button::new(
3848 "always-allow-tool-action",
3849 "Always Allow",
3850 )
3851 .label_size(LabelSize::Small)
3852 .icon(IconName::CheckDouble)
3853 .icon_position(IconPosition::Start)
3854 .icon_size(IconSize::Small)
3855 .icon_color(Color::Success)
3856 .tooltip(move |window, cx| {
3857 Tooltip::with_meta(
3858 "Never ask for permission",
3859 None,
3860 "Restore the original behavior in your Agent Panel settings",
3861 window,
3862 cx,
3863 )
3864 })
3865 .on_click(cx.listener(
3866 move |this, event, window, cx| {
3867 if let Some(fs) = fs.clone() {
3868 update_settings_file::<AgentSettings>(
3869 fs.clone(),
3870 cx,
3871 |settings, _| {
3872 settings.set_always_allow_tool_actions(true);
3873 },
3874 );
3875 }
3876 this.handle_allow_tool(
3877 tool_id.clone(),
3878 event,
3879 window,
3880 cx,
3881 )
3882 },
3883 ))
3884 })
3885 .child({
3886 let tool_id = tool_use.id.clone();
3887 Button::new("allow-tool-action", "Allow")
3888 .label_size(LabelSize::Small)
3889 .icon(IconName::Check)
3890 .icon_position(IconPosition::Start)
3891 .icon_size(IconSize::Small)
3892 .icon_color(Color::Success)
3893 .on_click(cx.listener(
3894 move |this, event, window, cx| {
3895 this.handle_allow_tool(
3896 tool_id.clone(),
3897 event,
3898 window,
3899 cx,
3900 )
3901 },
3902 ))
3903 })
3904 .child({
3905 let tool_id = tool_use.id.clone();
3906 let tool_name: Arc<str> = tool_use.name.into();
3907 Button::new("deny-tool", "Deny")
3908 .label_size(LabelSize::Small)
3909 .icon(IconName::Close)
3910 .icon_position(IconPosition::Start)
3911 .icon_size(IconSize::Small)
3912 .icon_color(Color::Error)
3913 .on_click(cx.listener(
3914 move |this, event, window, cx| {
3915 this.handle_deny_tool(
3916 tool_id.clone(),
3917 tool_name.clone(),
3918 event,
3919 window,
3920 cx,
3921 )
3922 },
3923 ))
3924 }),
3925 ),
3926 )
3927 })
3928 }
3929 }).into_any_element()
3930 }
3931
3932 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
3933 let project_context = self.thread.read(cx).project_context();
3934 let project_context = project_context.borrow();
3935 let Some(project_context) = project_context.as_ref() else {
3936 return div().into_any();
3937 };
3938
3939 let user_rules_text = if project_context.user_rules.is_empty() {
3940 None
3941 } else if project_context.user_rules.len() == 1 {
3942 let user_rules = &project_context.user_rules[0];
3943
3944 match user_rules.title.as_ref() {
3945 Some(title) => Some(format!("Using \"{title}\" user rule")),
3946 None => Some("Using user rule".into()),
3947 }
3948 } else {
3949 Some(format!(
3950 "Using {} user rules",
3951 project_context.user_rules.len()
3952 ))
3953 };
3954
3955 let first_user_rules_id = project_context
3956 .user_rules
3957 .first()
3958 .map(|user_rules| user_rules.uuid.0);
3959
3960 let rules_files = project_context
3961 .worktrees
3962 .iter()
3963 .filter_map(|worktree| worktree.rules_file.as_ref())
3964 .collect::<Vec<_>>();
3965
3966 let rules_file_text = match rules_files.as_slice() {
3967 &[] => None,
3968 &[rules_file] => Some(format!(
3969 "Using project {:?} file",
3970 rules_file.path_in_worktree
3971 )),
3972 rules_files => Some(format!("Using {} project rules files", rules_files.len())),
3973 };
3974
3975 if user_rules_text.is_none() && rules_file_text.is_none() {
3976 return div().into_any();
3977 }
3978
3979 v_flex()
3980 .pt_2()
3981 .px_2p5()
3982 .gap_1()
3983 .when_some(user_rules_text, |parent, user_rules_text| {
3984 parent.child(
3985 h_flex()
3986 .w_full()
3987 .child(
3988 Icon::new(RULES_ICON)
3989 .size(IconSize::XSmall)
3990 .color(Color::Disabled),
3991 )
3992 .child(
3993 Label::new(user_rules_text)
3994 .size(LabelSize::XSmall)
3995 .color(Color::Muted)
3996 .truncate()
3997 .buffer_font(cx)
3998 .ml_1p5()
3999 .mr_0p5(),
4000 )
4001 .child(
4002 IconButton::new("open-prompt-library", IconName::ArrowUpRightAlt)
4003 .shape(ui::IconButtonShape::Square)
4004 .icon_size(IconSize::XSmall)
4005 .icon_color(Color::Ignored)
4006 // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary` keybinding
4007 .tooltip(Tooltip::text("View User Rules"))
4008 .on_click(move |_event, window, cx| {
4009 window.dispatch_action(
4010 Box::new(OpenRulesLibrary {
4011 prompt_to_select: first_user_rules_id,
4012 }),
4013 cx,
4014 )
4015 }),
4016 ),
4017 )
4018 })
4019 .when_some(rules_file_text, |parent, rules_file_text| {
4020 parent.child(
4021 h_flex()
4022 .w_full()
4023 .child(
4024 Icon::new(IconName::File)
4025 .size(IconSize::XSmall)
4026 .color(Color::Disabled),
4027 )
4028 .child(
4029 Label::new(rules_file_text)
4030 .size(LabelSize::XSmall)
4031 .color(Color::Muted)
4032 .buffer_font(cx)
4033 .ml_1p5()
4034 .mr_0p5(),
4035 )
4036 .child(
4037 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
4038 .shape(ui::IconButtonShape::Square)
4039 .icon_size(IconSize::XSmall)
4040 .icon_color(Color::Ignored)
4041 .on_click(cx.listener(Self::handle_open_rules))
4042 .tooltip(Tooltip::text("View Rules")),
4043 ),
4044 )
4045 })
4046 .into_any()
4047 }
4048
4049 fn handle_allow_tool(
4050 &mut self,
4051 tool_use_id: LanguageModelToolUseId,
4052 _: &ClickEvent,
4053 window: &mut Window,
4054 cx: &mut Context<Self>,
4055 ) {
4056 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
4057 .thread
4058 .read(cx)
4059 .pending_tool(&tool_use_id)
4060 .map(|tool_use| tool_use.status.clone())
4061 {
4062 self.thread.update(cx, |thread, cx| {
4063 if let Some(configured) = thread.get_or_init_configured_model(cx) {
4064 thread.run_tool(
4065 c.tool_use_id.clone(),
4066 c.ui_text.clone(),
4067 c.input.clone(),
4068 c.request.clone(),
4069 c.tool.clone(),
4070 configured.model,
4071 Some(window.window_handle()),
4072 cx,
4073 );
4074 }
4075 });
4076 }
4077 }
4078
4079 fn handle_deny_tool(
4080 &mut self,
4081 tool_use_id: LanguageModelToolUseId,
4082 tool_name: Arc<str>,
4083 _: &ClickEvent,
4084 window: &mut Window,
4085 cx: &mut Context<Self>,
4086 ) {
4087 let window_handle = window.window_handle();
4088 self.thread.update(cx, |thread, cx| {
4089 thread.deny_tool_use(tool_use_id, tool_name, Some(window_handle), cx);
4090 });
4091 }
4092
4093 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
4094 let project_context = self.thread.read(cx).project_context();
4095 let project_context = project_context.borrow();
4096 let Some(project_context) = project_context.as_ref() else {
4097 return;
4098 };
4099
4100 let project_entry_ids = project_context
4101 .worktrees
4102 .iter()
4103 .flat_map(|worktree| worktree.rules_file.as_ref())
4104 .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id))
4105 .collect::<Vec<_>>();
4106
4107 self.workspace
4108 .update(cx, move |workspace, cx| {
4109 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
4110 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
4111 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
4112 let project = workspace.project().read(cx);
4113 let project_paths = project_entry_ids
4114 .into_iter()
4115 .flat_map(|entry_id| project.path_for_entry(entry_id, cx))
4116 .collect::<Vec<_>>();
4117 for project_path in project_paths {
4118 workspace
4119 .open_path(project_path, None, true, window, cx)
4120 .detach_and_log_err(cx);
4121 }
4122 })
4123 .ok();
4124 }
4125
4126 fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
4127 for window in self.notifications.drain(..) {
4128 window
4129 .update(cx, |_, window, _| {
4130 window.remove_window();
4131 })
4132 .ok();
4133
4134 self.notification_subscriptions.remove(&window);
4135 }
4136 }
4137
4138 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4139 if let Some((_, state)) = &self.editing_message {
4140 if state.is_agent_message {
4141 return None;
4142 }
4143 }
4144
4145 if !self.show_scrollbar && !self.scrollbar_state.is_dragging() {
4146 return None;
4147 }
4148
4149 Some(
4150 div()
4151 .occlude()
4152 .id("active-thread-scrollbar")
4153 .on_mouse_move(cx.listener(|_, _, _, cx| {
4154 cx.notify();
4155 cx.stop_propagation()
4156 }))
4157 .on_hover(|_, _, cx| {
4158 cx.stop_propagation();
4159 })
4160 .on_any_mouse_down(|_, _, cx| {
4161 cx.stop_propagation();
4162 })
4163 .on_mouse_up(
4164 MouseButton::Left,
4165 cx.listener(|_, _, _, cx| {
4166 cx.stop_propagation();
4167 }),
4168 )
4169 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4170 cx.notify();
4171 }))
4172 .h_full()
4173 .absolute()
4174 .right_1()
4175 .top_1()
4176 .bottom_0()
4177 .w(px(12.))
4178 .cursor_default()
4179 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
4180 )
4181 }
4182
4183 fn hide_scrollbar_later(&mut self, cx: &mut Context<Self>) {
4184 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4185 self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| {
4186 cx.background_executor()
4187 .timer(SCROLLBAR_SHOW_INTERVAL)
4188 .await;
4189 thread
4190 .update(cx, |thread, cx| {
4191 if !thread.scrollbar_state.is_dragging() {
4192 thread.show_scrollbar = false;
4193 cx.notify();
4194 }
4195 })
4196 .log_err();
4197 }))
4198 }
4199
4200 pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool {
4201 self.expanded_code_blocks
4202 .get(&(message_id, ix))
4203 .copied()
4204 .unwrap_or(true)
4205 }
4206
4207 pub fn toggle_codeblock_expanded(&mut self, message_id: MessageId, ix: usize) {
4208 let is_expanded = self
4209 .expanded_code_blocks
4210 .entry((message_id, ix))
4211 .or_insert(true);
4212 *is_expanded = !*is_expanded;
4213 }
4214
4215 pub fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
4216 self.list_state.scroll_to(ListOffset::default());
4217 cx.notify();
4218 }
4219
4220 pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
4221 self.list_state.reset(self.messages.len());
4222 cx.notify();
4223 }
4224}
4225
4226pub enum ActiveThreadEvent {
4227 EditingMessageTokenCountChanged,
4228}
4229
4230impl EventEmitter<ActiveThreadEvent> for ActiveThread {}
4231
4232impl Render for ActiveThread {
4233 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4234 v_flex()
4235 .size_full()
4236 .relative()
4237 .bg(cx.theme().colors().panel_background)
4238 .on_mouse_move(cx.listener(|this, _, _, cx| {
4239 this.show_scrollbar = true;
4240 this.hide_scrollbar_later(cx);
4241 cx.notify();
4242 }))
4243 .on_scroll_wheel(cx.listener(|this, _, _, cx| {
4244 this.show_scrollbar = true;
4245 this.hide_scrollbar_later(cx);
4246 cx.notify();
4247 }))
4248 .on_mouse_up(
4249 MouseButton::Left,
4250 cx.listener(|this, _, _, cx| {
4251 this.hide_scrollbar_later(cx);
4252 }),
4253 )
4254 .child(list(self.list_state.clone()).flex_grow())
4255 .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
4256 this.child(scrollbar)
4257 })
4258 }
4259}
4260
4261pub(crate) fn open_active_thread_as_markdown(
4262 thread: Entity<Thread>,
4263 workspace: Entity<Workspace>,
4264 window: &mut Window,
4265 cx: &mut App,
4266) -> Task<anyhow::Result<()>> {
4267 let markdown_language_task = workspace
4268 .read(cx)
4269 .app_state()
4270 .languages
4271 .language_for_name("Markdown");
4272
4273 window.spawn(cx, async move |cx| {
4274 let markdown_language = markdown_language_task.await?;
4275
4276 workspace.update_in(cx, |workspace, window, cx| {
4277 let thread = thread.read(cx);
4278 let markdown = thread.to_markdown(cx)?;
4279 let thread_summary = thread.summary().or_default().to_string();
4280
4281 let project = workspace.project().clone();
4282
4283 if !project.read(cx).is_local() {
4284 anyhow::bail!("failed to open active thread as markdown in remote project");
4285 }
4286
4287 let buffer = project.update(cx, |project, cx| {
4288 project.create_local_buffer(&markdown, Some(markdown_language), cx)
4289 });
4290 let buffer =
4291 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone()));
4292
4293 workspace.add_item_to_active_pane(
4294 Box::new(cx.new(|cx| {
4295 let mut editor =
4296 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
4297 editor.set_breadcrumb_header(thread_summary);
4298 editor
4299 })),
4300 None,
4301 true,
4302 window,
4303 cx,
4304 );
4305
4306 anyhow::Ok(())
4307 })??;
4308 anyhow::Ok(())
4309 })
4310}
4311
4312pub(crate) fn open_context(
4313 context: &AgentContextHandle,
4314 workspace: Entity<Workspace>,
4315 window: &mut Window,
4316 cx: &mut App,
4317) {
4318 match context {
4319 AgentContextHandle::File(file_context) => {
4320 if let Some(project_path) = file_context.project_path(cx) {
4321 workspace.update(cx, |workspace, cx| {
4322 workspace
4323 .open_path(project_path, None, true, window, cx)
4324 .detach_and_log_err(cx);
4325 });
4326 }
4327 }
4328
4329 AgentContextHandle::Directory(directory_context) => {
4330 let entry_id = directory_context.entry_id;
4331 workspace.update(cx, |workspace, cx| {
4332 workspace.project().update(cx, |_project, cx| {
4333 cx.emit(project::Event::RevealInProjectPanel(entry_id));
4334 })
4335 })
4336 }
4337
4338 AgentContextHandle::Symbol(symbol_context) => {
4339 let buffer = symbol_context.buffer.read(cx);
4340 if let Some(project_path) = buffer.project_path(cx) {
4341 let snapshot = buffer.snapshot();
4342 let target_position = symbol_context.range.start.to_point(&snapshot);
4343 open_editor_at_position(project_path, target_position, &workspace, window, cx)
4344 .detach();
4345 }
4346 }
4347
4348 AgentContextHandle::Selection(selection_context) => {
4349 let buffer = selection_context.buffer.read(cx);
4350 if let Some(project_path) = buffer.project_path(cx) {
4351 let snapshot = buffer.snapshot();
4352 let target_position = selection_context.range.start.to_point(&snapshot);
4353
4354 open_editor_at_position(project_path, target_position, &workspace, window, cx)
4355 .detach();
4356 }
4357 }
4358
4359 AgentContextHandle::FetchedUrl(fetched_url_context) => {
4360 cx.open_url(&fetched_url_context.url);
4361 }
4362
4363 AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
4364 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
4365 let thread = thread_context.thread.clone();
4366 window.defer(cx, move |window, cx| {
4367 panel.update(cx, |panel, cx| {
4368 panel.open_thread(thread, window, cx);
4369 });
4370 });
4371 }
4372 }),
4373
4374 AgentContextHandle::TextThread(text_thread_context) => {
4375 workspace.update(cx, |workspace, cx| {
4376 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
4377 let context = text_thread_context.context.clone();
4378 window.defer(cx, move |window, cx| {
4379 panel.update(cx, |panel, cx| {
4380 panel.open_prompt_editor(context, window, cx)
4381 });
4382 });
4383 }
4384 })
4385 }
4386
4387 AgentContextHandle::Rules(rules_context) => window.dispatch_action(
4388 Box::new(OpenRulesLibrary {
4389 prompt_to_select: Some(rules_context.prompt_id.0),
4390 }),
4391 cx,
4392 ),
4393
4394 AgentContextHandle::Image(_) => {}
4395 }
4396}
4397
4398pub(crate) fn attach_pasted_images_as_context(
4399 context_store: &Entity<ContextStore>,
4400 cx: &mut App,
4401) -> bool {
4402 let images = cx
4403 .read_from_clipboard()
4404 .map(|item| {
4405 item.into_entries()
4406 .filter_map(|entry| {
4407 if let ClipboardEntry::Image(image) = entry {
4408 Some(image)
4409 } else {
4410 None
4411 }
4412 })
4413 .collect::<Vec<_>>()
4414 })
4415 .unwrap_or_default();
4416
4417 if images.is_empty() {
4418 return false;
4419 }
4420 cx.stop_propagation();
4421
4422 context_store.update(cx, |store, cx| {
4423 for image in images {
4424 store.add_image_instance(Arc::new(image), cx);
4425 }
4426 });
4427 true
4428}
4429
4430fn open_editor_at_position(
4431 project_path: project::ProjectPath,
4432 target_position: Point,
4433 workspace: &Entity<Workspace>,
4434 window: &mut Window,
4435 cx: &mut App,
4436) -> Task<()> {
4437 let open_task = workspace.update(cx, |workspace, cx| {
4438 workspace.open_path(project_path, None, true, window, cx)
4439 });
4440 window.spawn(cx, async move |cx| {
4441 if let Some(active_editor) = open_task
4442 .await
4443 .log_err()
4444 .and_then(|item| item.downcast::<Editor>())
4445 {
4446 active_editor
4447 .downgrade()
4448 .update_in(cx, |editor, window, cx| {
4449 editor.go_to_singleton_buffer_point(target_position, window, cx);
4450 })
4451 .log_err();
4452 }
4453 })
4454}
4455
4456#[cfg(test)]
4457mod tests {
4458 use super::*;
4459 use agent::{MessageSegment, context::ContextLoadResult, thread_store};
4460 use assistant_tool::{ToolRegistry, ToolWorkingSet};
4461 use editor::EditorSettings;
4462 use fs::FakeFs;
4463 use gpui::{AppContext, TestAppContext, VisualTestContext};
4464 use language_model::{
4465 ConfiguredModel, LanguageModel, LanguageModelRegistry,
4466 fake_provider::{FakeLanguageModel, FakeLanguageModelProvider},
4467 };
4468 use project::Project;
4469 use prompt_store::PromptBuilder;
4470 use serde_json::json;
4471 use settings::SettingsStore;
4472 use util::path;
4473 use workspace::CollaboratorId;
4474
4475 #[gpui::test]
4476 async fn test_agent_is_unfollowed_after_cancelling_completion(cx: &mut TestAppContext) {
4477 init_test_settings(cx);
4478
4479 let project = create_test_project(
4480 cx,
4481 json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
4482 )
4483 .await;
4484
4485 let (cx, _active_thread, workspace, thread, model) =
4486 setup_test_environment(cx, project.clone()).await;
4487
4488 // Insert user message without any context (empty context vector)
4489 thread.update(cx, |thread, cx| {
4490 thread.insert_user_message(
4491 "What is the best way to learn Rust?",
4492 ContextLoadResult::default(),
4493 None,
4494 vec![],
4495 cx,
4496 );
4497 });
4498
4499 // Stream response to user message
4500 thread.update(cx, |thread, cx| {
4501 let intent = CompletionIntent::UserPrompt;
4502 let request = thread.to_completion_request(model.clone(), intent, cx);
4503 thread.stream_completion(request, model, intent, cx.active_window(), cx)
4504 });
4505 // Follow the agent
4506 cx.update(|window, cx| {
4507 workspace.update(cx, |workspace, cx| {
4508 workspace.follow(CollaboratorId::Agent, window, cx);
4509 })
4510 });
4511 assert!(cx.read(|cx| workspace.read(cx).is_being_followed(CollaboratorId::Agent)));
4512
4513 // Cancel the current completion
4514 thread.update(cx, |thread, cx| {
4515 thread.cancel_last_completion(cx.active_window(), cx)
4516 });
4517
4518 cx.executor().run_until_parked();
4519
4520 // No longer following the agent
4521 assert!(!cx.read(|cx| workspace.read(cx).is_being_followed(CollaboratorId::Agent)));
4522 }
4523
4524 #[gpui::test]
4525 async fn test_reinserting_creases_for_edited_message(cx: &mut TestAppContext) {
4526 init_test_settings(cx);
4527
4528 let project = create_test_project(cx, json!({})).await;
4529
4530 let (cx, active_thread, _, thread, model) =
4531 setup_test_environment(cx, project.clone()).await;
4532 cx.update(|_, cx| {
4533 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
4534 registry.set_default_model(
4535 Some(ConfiguredModel {
4536 provider: Arc::new(FakeLanguageModelProvider::default()),
4537 model,
4538 }),
4539 cx,
4540 );
4541 });
4542 });
4543
4544 let creases = vec![MessageCrease {
4545 range: 14..22,
4546 icon_path: "icon".into(),
4547 label: "foo.txt".into(),
4548 context: None,
4549 }];
4550
4551 let message = thread.update(cx, |thread, cx| {
4552 let message_id = thread.insert_user_message(
4553 "Tell me about @foo.txt",
4554 ContextLoadResult::default(),
4555 None,
4556 creases,
4557 cx,
4558 );
4559 thread.message(message_id).cloned().unwrap()
4560 });
4561
4562 active_thread.update_in(cx, |active_thread, window, cx| {
4563 if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
4564 active_thread.start_editing_user_message(
4565 message.id,
4566 message_text,
4567 message.creases.as_slice(),
4568 None,
4569 window,
4570 cx,
4571 );
4572 }
4573 let editor = active_thread
4574 .editing_message
4575 .as_ref()
4576 .unwrap()
4577 .1
4578 .editor
4579 .clone();
4580 editor.update(cx, |editor, cx| editor.edit([(0..13, "modified")], cx));
4581 active_thread.confirm_editing_message(&Default::default(), window, cx);
4582 });
4583 cx.run_until_parked();
4584
4585 let message = thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
4586 active_thread.update_in(cx, |active_thread, window, cx| {
4587 if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
4588 active_thread.start_editing_user_message(
4589 message.id,
4590 message_text,
4591 message.creases.as_slice(),
4592 None,
4593 window,
4594 cx,
4595 );
4596 }
4597 let editor = active_thread
4598 .editing_message
4599 .as_ref()
4600 .unwrap()
4601 .1
4602 .editor
4603 .clone();
4604 let text = editor.update(cx, |editor, cx| editor.text(cx));
4605 assert_eq!(text, "modified @foo.txt");
4606 });
4607 }
4608
4609 #[gpui::test]
4610 async fn test_editing_message_cancels_previous_completion(cx: &mut TestAppContext) {
4611 init_test_settings(cx);
4612
4613 let project = create_test_project(cx, json!({})).await;
4614
4615 let (cx, active_thread, _, thread, model) =
4616 setup_test_environment(cx, project.clone()).await;
4617
4618 cx.update(|_, cx| {
4619 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
4620 registry.set_default_model(
4621 Some(ConfiguredModel {
4622 provider: Arc::new(FakeLanguageModelProvider::default()),
4623 model: model.clone(),
4624 }),
4625 cx,
4626 );
4627 });
4628 });
4629
4630 // Track thread events to verify cancellation
4631 let cancellation_events = Arc::new(std::sync::Mutex::new(Vec::new()));
4632 let new_request_events = Arc::new(std::sync::Mutex::new(Vec::new()));
4633
4634 let _subscription = cx.update(|_, cx| {
4635 let cancellation_events = cancellation_events.clone();
4636 let new_request_events = new_request_events.clone();
4637 cx.subscribe(
4638 &thread,
4639 move |_thread, event: &ThreadEvent, _cx| match event {
4640 ThreadEvent::CompletionCanceled => {
4641 cancellation_events.lock().unwrap().push(());
4642 }
4643 ThreadEvent::NewRequest => {
4644 new_request_events.lock().unwrap().push(());
4645 }
4646 _ => {}
4647 },
4648 )
4649 });
4650
4651 // Insert a user message and start streaming a response
4652 let message = thread.update(cx, |thread, cx| {
4653 let message_id = thread.insert_user_message(
4654 "Hello, how are you?",
4655 ContextLoadResult::default(),
4656 None,
4657 vec![],
4658 cx,
4659 );
4660 thread.advance_prompt_id();
4661 thread.send_to_model(
4662 model.clone(),
4663 CompletionIntent::UserPrompt,
4664 cx.active_window(),
4665 cx,
4666 );
4667 thread.message(message_id).cloned().unwrap()
4668 });
4669
4670 cx.run_until_parked();
4671
4672 // Verify that a completion is in progress
4673 assert!(cx.read(|cx| thread.read(cx).is_generating()));
4674 assert_eq!(new_request_events.lock().unwrap().len(), 1);
4675
4676 // Edit the message while the completion is still running
4677 active_thread.update_in(cx, |active_thread, window, cx| {
4678 if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
4679 active_thread.start_editing_user_message(
4680 message.id,
4681 message_text,
4682 message.creases.as_slice(),
4683 None,
4684 window,
4685 cx,
4686 );
4687 }
4688 let editor = active_thread
4689 .editing_message
4690 .as_ref()
4691 .unwrap()
4692 .1
4693 .editor
4694 .clone();
4695 editor.update(cx, |editor, cx| {
4696 editor.set_text("What is the weather like?", window, cx);
4697 });
4698 active_thread.confirm_editing_message(&Default::default(), window, cx);
4699 });
4700
4701 cx.run_until_parked();
4702
4703 // Verify that the previous completion was cancelled
4704 assert_eq!(cancellation_events.lock().unwrap().len(), 1);
4705
4706 // Verify that a new request was started after cancellation
4707 assert_eq!(new_request_events.lock().unwrap().len(), 2);
4708
4709 // Verify that the edited message contains the new text
4710 let edited_message =
4711 thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
4712 match &edited_message.segments[0] {
4713 MessageSegment::Text(text) => {
4714 assert_eq!(text, "What is the weather like?");
4715 }
4716 _ => panic!("Expected text segment"),
4717 }
4718 }
4719
4720 fn init_test_settings(cx: &mut TestAppContext) {
4721 cx.update(|cx| {
4722 let settings_store = SettingsStore::test(cx);
4723 cx.set_global(settings_store);
4724 language::init(cx);
4725 Project::init_settings(cx);
4726 AgentSettings::register(cx);
4727 prompt_store::init(cx);
4728 thread_store::init(cx);
4729 workspace::init_settings(cx);
4730 language_model::init_settings(cx);
4731 ThemeSettings::register(cx);
4732 EditorSettings::register(cx);
4733 ToolRegistry::default_global(cx);
4734 });
4735 }
4736
4737 // Helper to create a test project with test files
4738 async fn create_test_project(
4739 cx: &mut TestAppContext,
4740 files: serde_json::Value,
4741 ) -> Entity<Project> {
4742 let fs = FakeFs::new(cx.executor());
4743 fs.insert_tree(path!("/test"), files).await;
4744 Project::test(fs, [path!("/test").as_ref()], cx).await
4745 }
4746
4747 async fn setup_test_environment(
4748 cx: &mut TestAppContext,
4749 project: Entity<Project>,
4750 ) -> (
4751 &mut VisualTestContext,
4752 Entity<ActiveThread>,
4753 Entity<Workspace>,
4754 Entity<Thread>,
4755 Arc<dyn LanguageModel>,
4756 ) {
4757 let (workspace, cx) =
4758 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
4759
4760 let thread_store = cx
4761 .update(|_, cx| {
4762 ThreadStore::load(
4763 project.clone(),
4764 cx.new(|_| ToolWorkingSet::default()),
4765 None,
4766 Arc::new(PromptBuilder::new(None).unwrap()),
4767 cx,
4768 )
4769 })
4770 .await
4771 .unwrap();
4772
4773 let text_thread_store = cx
4774 .update(|_, cx| {
4775 TextThreadStore::new(
4776 project.clone(),
4777 Arc::new(PromptBuilder::new(None).unwrap()),
4778 Default::default(),
4779 cx,
4780 )
4781 })
4782 .await
4783 .unwrap();
4784
4785 let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
4786 let context_store =
4787 cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
4788
4789 let model = FakeLanguageModel::default();
4790 let model: Arc<dyn LanguageModel> = Arc::new(model);
4791
4792 let language_registry = LanguageRegistry::new(cx.executor());
4793 let language_registry = Arc::new(language_registry);
4794
4795 let active_thread = cx.update(|window, cx| {
4796 cx.new(|cx| {
4797 let mut active_thread = ActiveThread::new(
4798 thread.clone(),
4799 thread_store.clone(),
4800 text_thread_store,
4801 context_store.clone(),
4802 language_registry.clone(),
4803 workspace.downgrade(),
4804 window,
4805 cx,
4806 );
4807 #[cfg(test)]
4808 {
4809 active_thread.skip_save_when_testing = true;
4810 }
4811 active_thread
4812 })
4813 });
4814
4815 (cx, active_thread, workspace, thread, model)
4816 }
4817}