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