1use crate::thread::{
2 LastRestoreCheckpoint, MessageId, RequestKind, Thread, ThreadError, ThreadEvent, ThreadFeedback,
3};
4use crate::thread_store::ThreadStore;
5use crate::tool_use::{ToolUse, ToolUseStatus};
6use crate::ui::ContextPill;
7use collections::HashMap;
8use editor::{Editor, MultiBuffer};
9use gpui::{
10 list, percentage, pulsating_between, AbsoluteLength, Animation, AnimationExt, AnyElement, App,
11 ClickEvent, DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment,
12 ListOffset, ListState, StyleRefinement, Subscription, Task, TextStyleRefinement,
13 Transformation, UnderlineStyle, WeakEntity,
14};
15use language::{Buffer, LanguageRegistry};
16use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
17use markdown::{Markdown, MarkdownStyle};
18use scripting_tool::{ScriptingTool, ScriptingToolInput};
19use settings::Settings as _;
20use std::sync::Arc;
21use std::time::Duration;
22use theme::ThemeSettings;
23use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Tooltip};
24use util::ResultExt as _;
25use workspace::{OpenOptions, Workspace};
26
27use crate::context_store::{refresh_context_store_text, ContextStore};
28
29pub struct ActiveThread {
30 language_registry: Arc<LanguageRegistry>,
31 thread_store: Entity<ThreadStore>,
32 thread: Entity<Thread>,
33 context_store: Entity<ContextStore>,
34 workspace: WeakEntity<Workspace>,
35 save_thread_task: Option<Task<()>>,
36 messages: Vec<MessageId>,
37 list_state: ListState,
38 rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
39 rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
40 rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
41 editing_message: Option<(MessageId, EditMessageState)>,
42 expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
43 last_error: Option<ThreadError>,
44 _subscriptions: Vec<Subscription>,
45}
46
47struct EditMessageState {
48 editor: Entity<Editor>,
49}
50
51impl ActiveThread {
52 pub fn new(
53 thread: Entity<Thread>,
54 thread_store: Entity<ThreadStore>,
55 language_registry: Arc<LanguageRegistry>,
56 context_store: Entity<ContextStore>,
57 workspace: WeakEntity<Workspace>,
58 window: &mut Window,
59 cx: &mut Context<Self>,
60 ) -> Self {
61 let subscriptions = vec![
62 cx.observe(&thread, |_, _, cx| cx.notify()),
63 cx.subscribe_in(&thread, window, Self::handle_thread_event),
64 ];
65
66 let mut this = Self {
67 language_registry,
68 thread_store,
69 thread: thread.clone(),
70 context_store,
71 workspace,
72 save_thread_task: None,
73 messages: Vec::new(),
74 rendered_messages_by_id: HashMap::default(),
75 rendered_scripting_tool_uses: HashMap::default(),
76 rendered_tool_use_labels: HashMap::default(),
77 expanded_tool_uses: HashMap::default(),
78 list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
79 let this = cx.entity().downgrade();
80 move |ix, window: &mut Window, cx: &mut App| {
81 this.update(cx, |this, cx| this.render_message(ix, window, cx))
82 .unwrap()
83 }
84 }),
85 editing_message: None,
86 last_error: None,
87 _subscriptions: subscriptions,
88 };
89
90 for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
91 this.push_message(&message.id, message.text.clone(), window, cx);
92
93 for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
94 this.render_tool_use_label_markdown(
95 tool_use.id.clone(),
96 tool_use.ui_text.clone(),
97 window,
98 cx,
99 );
100 }
101
102 for tool_use in thread
103 .read(cx)
104 .scripting_tool_uses_for_message(message.id, cx)
105 {
106 this.render_tool_use_label_markdown(
107 tool_use.id.clone(),
108 tool_use.ui_text.clone(),
109 window,
110 cx,
111 );
112
113 this.render_scripting_tool_use_markdown(
114 tool_use.id.clone(),
115 tool_use.ui_text.as_ref(),
116 tool_use.input.clone(),
117 window,
118 cx,
119 );
120 }
121 }
122
123 this
124 }
125
126 pub fn thread(&self) -> &Entity<Thread> {
127 &self.thread
128 }
129
130 pub fn is_empty(&self) -> bool {
131 self.messages.is_empty()
132 }
133
134 pub fn summary(&self, cx: &App) -> Option<SharedString> {
135 self.thread.read(cx).summary()
136 }
137
138 pub fn summary_or_default(&self, cx: &App) -> SharedString {
139 self.thread.read(cx).summary_or_default()
140 }
141
142 pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
143 self.last_error.take();
144 self.thread
145 .update(cx, |thread, cx| thread.cancel_last_completion(cx))
146 }
147
148 pub fn last_error(&self) -> Option<ThreadError> {
149 self.last_error.clone()
150 }
151
152 pub fn clear_last_error(&mut self) {
153 self.last_error.take();
154 }
155
156 fn push_message(
157 &mut self,
158 id: &MessageId,
159 text: String,
160 window: &mut Window,
161 cx: &mut Context<Self>,
162 ) {
163 let old_len = self.messages.len();
164 self.messages.push(*id);
165 self.list_state.splice(old_len..old_len, 1);
166
167 let markdown = self.render_markdown(text.into(), window, cx);
168 self.rendered_messages_by_id.insert(*id, markdown);
169 self.list_state.scroll_to(ListOffset {
170 item_ix: old_len,
171 offset_in_item: Pixels(0.0),
172 });
173 }
174
175 fn edited_message(
176 &mut self,
177 id: &MessageId,
178 text: String,
179 window: &mut Window,
180 cx: &mut Context<Self>,
181 ) {
182 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
183 return;
184 };
185 self.list_state.splice(index..index + 1, 1);
186 let markdown = self.render_markdown(text.into(), window, cx);
187 self.rendered_messages_by_id.insert(*id, markdown);
188 }
189
190 fn deleted_message(&mut self, id: &MessageId) {
191 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
192 return;
193 };
194 self.messages.remove(index);
195 self.list_state.splice(index..index + 1, 0);
196 self.rendered_messages_by_id.remove(id);
197 }
198
199 fn render_markdown(
200 &self,
201 text: SharedString,
202 window: &Window,
203 cx: &mut Context<Self>,
204 ) -> Entity<Markdown> {
205 let theme_settings = ThemeSettings::get_global(cx);
206 let colors = cx.theme().colors();
207 let ui_font_size = TextSize::Default.rems(cx);
208 let buffer_font_size = TextSize::Small.rems(cx);
209 let mut text_style = window.text_style();
210
211 text_style.refine(&TextStyleRefinement {
212 font_family: Some(theme_settings.ui_font.family.clone()),
213 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
214 font_features: Some(theme_settings.ui_font.features.clone()),
215 font_size: Some(ui_font_size.into()),
216 color: Some(cx.theme().colors().text),
217 ..Default::default()
218 });
219
220 let markdown_style = MarkdownStyle {
221 base_text_style: text_style,
222 syntax: cx.theme().syntax().clone(),
223 selection_background_color: cx.theme().players().local().selection,
224 code_block_overflow_x_scroll: true,
225 table_overflow_x_scroll: true,
226 code_block: StyleRefinement {
227 margin: EdgesRefinement {
228 top: Some(Length::Definite(rems(0.).into())),
229 left: Some(Length::Definite(rems(0.).into())),
230 right: Some(Length::Definite(rems(0.).into())),
231 bottom: Some(Length::Definite(rems(0.5).into())),
232 },
233 padding: EdgesRefinement {
234 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
235 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
236 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
237 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
238 },
239 background: Some(colors.editor_background.into()),
240 border_color: Some(colors.border_variant),
241 border_widths: EdgesRefinement {
242 top: Some(AbsoluteLength::Pixels(Pixels(1.))),
243 left: Some(AbsoluteLength::Pixels(Pixels(1.))),
244 right: Some(AbsoluteLength::Pixels(Pixels(1.))),
245 bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
246 },
247 text: Some(TextStyleRefinement {
248 font_family: Some(theme_settings.buffer_font.family.clone()),
249 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
250 font_features: Some(theme_settings.buffer_font.features.clone()),
251 font_size: Some(buffer_font_size.into()),
252 ..Default::default()
253 }),
254 ..Default::default()
255 },
256 inline_code: TextStyleRefinement {
257 font_family: Some(theme_settings.buffer_font.family.clone()),
258 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
259 font_features: Some(theme_settings.buffer_font.features.clone()),
260 font_size: Some(buffer_font_size.into()),
261 background_color: Some(colors.editor_foreground.opacity(0.1)),
262 ..Default::default()
263 },
264 link: TextStyleRefinement {
265 background_color: Some(colors.editor_foreground.opacity(0.025)),
266 underline: Some(UnderlineStyle {
267 color: Some(colors.text_accent.opacity(0.5)),
268 thickness: px(1.),
269 ..Default::default()
270 }),
271 ..Default::default()
272 },
273 ..Default::default()
274 };
275
276 cx.new(|cx| {
277 Markdown::new(
278 text,
279 markdown_style,
280 Some(self.language_registry.clone()),
281 None,
282 cx,
283 )
284 })
285 }
286
287 /// Renders the input of a scripting tool use to Markdown.
288 ///
289 /// Does nothing if the tool use does not correspond to the scripting tool.
290 fn render_scripting_tool_use_markdown(
291 &mut self,
292 tool_use_id: LanguageModelToolUseId,
293 tool_name: &str,
294 tool_input: serde_json::Value,
295 window: &mut Window,
296 cx: &mut Context<Self>,
297 ) {
298 if tool_name != ScriptingTool::NAME {
299 return;
300 }
301
302 let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
303 .map(|input| input.lua_script)
304 .unwrap_or_default();
305
306 let lua_script =
307 self.render_markdown(format!("```lua\n{lua_script}\n```").into(), window, cx);
308
309 self.rendered_scripting_tool_uses
310 .insert(tool_use_id, lua_script);
311 }
312
313 fn render_tool_use_label_markdown(
314 &mut self,
315 tool_use_id: LanguageModelToolUseId,
316 tool_label: impl Into<SharedString>,
317 window: &mut Window,
318 cx: &mut Context<Self>,
319 ) {
320 self.rendered_tool_use_labels.insert(
321 tool_use_id,
322 self.render_markdown(tool_label.into(), window, cx),
323 );
324 }
325
326 fn handle_thread_event(
327 &mut self,
328 _thread: &Entity<Thread>,
329 event: &ThreadEvent,
330 window: &mut Window,
331 cx: &mut Context<Self>,
332 ) {
333 match event {
334 ThreadEvent::ShowError(error) => {
335 self.last_error = Some(error.clone());
336 }
337 ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
338 self.save_thread(cx);
339 }
340 ThreadEvent::DoneStreaming => {}
341 ThreadEvent::StreamedAssistantText(message_id, text) => {
342 if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
343 markdown.update(cx, |markdown, cx| {
344 markdown.append(text, cx);
345 });
346 }
347 }
348 ThreadEvent::MessageAdded(message_id) => {
349 if let Some(message_text) = self
350 .thread
351 .read(cx)
352 .message(*message_id)
353 .map(|message| message.text.clone())
354 {
355 self.push_message(message_id, message_text, window, cx);
356 }
357
358 self.save_thread(cx);
359 cx.notify();
360 }
361 ThreadEvent::MessageEdited(message_id) => {
362 if let Some(message_text) = self
363 .thread
364 .read(cx)
365 .message(*message_id)
366 .map(|message| message.text.clone())
367 {
368 self.edited_message(message_id, message_text, window, cx);
369 }
370
371 self.save_thread(cx);
372 cx.notify();
373 }
374 ThreadEvent::MessageDeleted(message_id) => {
375 self.deleted_message(message_id);
376 self.save_thread(cx);
377 cx.notify();
378 }
379 ThreadEvent::UsePendingTools => {
380 let tool_uses = self
381 .thread
382 .update(cx, |thread, cx| thread.use_pending_tools(cx));
383
384 for tool_use in tool_uses {
385 self.render_tool_use_label_markdown(
386 tool_use.id,
387 tool_use.ui_text.clone(),
388 window,
389 cx,
390 );
391 }
392 }
393 ThreadEvent::ToolFinished {
394 pending_tool_use,
395 canceled,
396 ..
397 } => {
398 let canceled = *canceled;
399 if let Some(tool_use) = pending_tool_use {
400 self.render_tool_use_label_markdown(
401 tool_use.id.clone(),
402 SharedString::from(tool_use.ui_text.clone()),
403 window,
404 cx,
405 );
406 self.render_scripting_tool_use_markdown(
407 tool_use.id.clone(),
408 tool_use.name.as_ref(),
409 tool_use.input.clone(),
410 window,
411 cx,
412 );
413 }
414
415 if self.thread.read(cx).all_tools_finished() {
416 let pending_refresh_buffers = self.thread.update(cx, |thread, cx| {
417 thread.action_log().update(cx, |action_log, _cx| {
418 action_log.take_stale_buffers_in_context()
419 })
420 });
421
422 let context_update_task = if !pending_refresh_buffers.is_empty() {
423 let refresh_task = refresh_context_store_text(
424 self.context_store.clone(),
425 &pending_refresh_buffers,
426 cx,
427 );
428
429 cx.spawn(async move |this, cx| {
430 let updated_context_ids = refresh_task.await;
431
432 this.update(cx, |this, cx| {
433 this.context_store.read_with(cx, |context_store, cx| {
434 context_store
435 .context()
436 .iter()
437 .filter(|context| {
438 updated_context_ids.contains(&context.id())
439 })
440 .flat_map(|context| context.snapshot(cx))
441 .collect()
442 })
443 })
444 })
445 } else {
446 Task::ready(anyhow::Ok(Vec::new()))
447 };
448
449 let model_registry = LanguageModelRegistry::read_global(cx);
450 if let Some(model) = model_registry.active_model() {
451 cx.spawn(async move |this, cx| {
452 let updated_context = context_update_task.await?;
453
454 this.update(cx, |this, cx| {
455 this.thread.update(cx, |thread, cx| {
456 thread.attach_tool_results(updated_context, cx);
457 if !canceled {
458 thread.send_to_model(model, RequestKind::Chat, cx);
459 }
460 });
461 })
462 })
463 .detach();
464 }
465 }
466 }
467 ThreadEvent::CheckpointChanged => cx.notify(),
468 }
469 }
470
471 /// Spawns a task to save the active thread.
472 ///
473 /// Only one task to save the thread will be in flight at a time.
474 fn save_thread(&mut self, cx: &mut Context<Self>) {
475 let thread = self.thread.clone();
476 self.save_thread_task = Some(cx.spawn(async move |this, cx| {
477 let task = this
478 .update(cx, |this, cx| {
479 this.thread_store
480 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
481 })
482 .ok();
483
484 if let Some(task) = task {
485 task.await.log_err();
486 }
487 }));
488 }
489
490 fn start_editing_message(
491 &mut self,
492 message_id: MessageId,
493 message_text: String,
494 window: &mut Window,
495 cx: &mut Context<Self>,
496 ) {
497 let buffer = cx.new(|cx| {
498 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
499 });
500 let editor = cx.new(|cx| {
501 let mut editor = Editor::new(
502 editor::EditorMode::AutoHeight { max_lines: 8 },
503 buffer,
504 None,
505 window,
506 cx,
507 );
508 editor.focus_handle(cx).focus(window);
509 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
510 editor
511 });
512 self.editing_message = Some((
513 message_id,
514 EditMessageState {
515 editor: editor.clone(),
516 },
517 ));
518 cx.notify();
519 }
520
521 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
522 self.editing_message.take();
523 cx.notify();
524 }
525
526 fn confirm_editing_message(
527 &mut self,
528 _: &menu::Confirm,
529 _: &mut Window,
530 cx: &mut Context<Self>,
531 ) {
532 let Some((message_id, state)) = self.editing_message.take() else {
533 return;
534 };
535 let edited_text = state.editor.read(cx).text(cx);
536 self.thread.update(cx, |thread, cx| {
537 thread.edit_message(message_id, Role::User, edited_text, cx);
538 for message_id in self.messages_after(message_id) {
539 thread.delete_message(*message_id, cx);
540 }
541 });
542
543 let provider = LanguageModelRegistry::read_global(cx).active_provider();
544 if provider
545 .as_ref()
546 .map_or(false, |provider| provider.must_accept_terms(cx))
547 {
548 cx.notify();
549 return;
550 }
551 let model_registry = LanguageModelRegistry::read_global(cx);
552 let Some(model) = model_registry.active_model() else {
553 return;
554 };
555
556 self.thread.update(cx, |thread, cx| {
557 thread.send_to_model(model, RequestKind::Chat, cx)
558 });
559 cx.notify();
560 }
561
562 fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
563 self.messages
564 .iter()
565 .rev()
566 .find(|message_id| {
567 self.thread
568 .read(cx)
569 .message(**message_id)
570 .map_or(false, |message| message.role == Role::User)
571 })
572 .cloned()
573 }
574
575 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
576 self.messages
577 .iter()
578 .position(|id| *id == message_id)
579 .map(|index| &self.messages[index + 1..])
580 .unwrap_or(&[])
581 }
582
583 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
584 self.cancel_editing_message(&menu::Cancel, window, cx);
585 }
586
587 fn handle_regenerate_click(
588 &mut self,
589 _: &ClickEvent,
590 window: &mut Window,
591 cx: &mut Context<Self>,
592 ) {
593 self.confirm_editing_message(&menu::Confirm, window, cx);
594 }
595
596 fn handle_feedback_click(
597 &mut self,
598 feedback: ThreadFeedback,
599 _window: &mut Window,
600 cx: &mut Context<Self>,
601 ) {
602 let report = self
603 .thread
604 .update(cx, |thread, cx| thread.report_feedback(feedback, cx));
605
606 let this = cx.entity().downgrade();
607 cx.spawn(async move |_, cx| {
608 report.await?;
609 this.update(cx, |_this, cx| cx.notify())
610 })
611 .detach_and_log_err(cx);
612 }
613
614 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
615 let message_id = self.messages[ix];
616 let Some(message) = self.thread.read(cx).message(message_id) else {
617 return Empty.into_any();
618 };
619
620 let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
621 return Empty.into_any();
622 };
623
624 let thread = self.thread.read(cx);
625 // Get all the data we need from thread before we start using it in closures
626 let checkpoint = thread.checkpoint_for_message(message_id);
627 let context = thread.context_for_message(message_id);
628 let tool_uses = thread.tool_uses_for_message(message_id, cx);
629 let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id, cx);
630
631 // Don't render user messages that are just there for returning tool results.
632 if message.role == Role::User
633 && (thread.message_has_tool_results(message_id)
634 || thread.message_has_scripting_tool_results(message_id))
635 {
636 return Empty.into_any();
637 }
638
639 let allow_editing_message =
640 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
641
642 let edit_message_editor = self
643 .editing_message
644 .as_ref()
645 .filter(|(id, _)| *id == message_id)
646 .map(|(_, state)| state.editor.clone());
647
648 let first_message = ix == 0;
649 let is_last_message = ix == self.messages.len() - 1;
650
651 let colors = cx.theme().colors();
652 let active_color = colors.element_active;
653 let editor_bg_color = colors.editor_background;
654 let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
655
656 let feedback_container = h_flex().pb_4().px_4().gap_1().justify_between();
657 let feedback_items = match self.thread.read(cx).feedback() {
658 Some(feedback) => feedback_container
659 .child(
660 Label::new(match feedback {
661 ThreadFeedback::Positive => "Thanks for your feedback!",
662 ThreadFeedback::Negative => {
663 "We appreciate your feedback and will use it to improve."
664 }
665 })
666 .color(Color::Muted)
667 .size(LabelSize::XSmall),
668 )
669 .child(
670 h_flex()
671 .gap_1()
672 .child(
673 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
674 .icon_size(IconSize::XSmall)
675 .icon_color(match feedback {
676 ThreadFeedback::Positive => Color::Accent,
677 ThreadFeedback::Negative => Color::Ignored,
678 })
679 .shape(ui::IconButtonShape::Square)
680 .tooltip(Tooltip::text("Helpful Response"))
681 .on_click(cx.listener(move |this, _, window, cx| {
682 this.handle_feedback_click(
683 ThreadFeedback::Positive,
684 window,
685 cx,
686 );
687 })),
688 )
689 .child(
690 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
691 .icon_size(IconSize::XSmall)
692 .icon_color(match feedback {
693 ThreadFeedback::Positive => Color::Ignored,
694 ThreadFeedback::Negative => Color::Accent,
695 })
696 .shape(ui::IconButtonShape::Square)
697 .tooltip(Tooltip::text("Not Helpful"))
698 .on_click(cx.listener(move |this, _, window, cx| {
699 this.handle_feedback_click(
700 ThreadFeedback::Negative,
701 window,
702 cx,
703 );
704 })),
705 ),
706 )
707 .into_any_element(),
708 None => feedback_container
709 .child(
710 Label::new(
711 "Rating the thread sends all of your current conversation to the Zed team.",
712 )
713 .color(Color::Muted)
714 .size(LabelSize::XSmall),
715 )
716 .child(
717 h_flex()
718 .gap_1()
719 .child(
720 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
721 .icon_size(IconSize::XSmall)
722 .icon_color(Color::Ignored)
723 .shape(ui::IconButtonShape::Square)
724 .tooltip(Tooltip::text("Helpful Response"))
725 .on_click(cx.listener(move |this, _, window, cx| {
726 this.handle_feedback_click(
727 ThreadFeedback::Positive,
728 window,
729 cx,
730 );
731 })),
732 )
733 .child(
734 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
735 .icon_size(IconSize::XSmall)
736 .icon_color(Color::Ignored)
737 .shape(ui::IconButtonShape::Square)
738 .tooltip(Tooltip::text("Not Helpful"))
739 .on_click(cx.listener(move |this, _, window, cx| {
740 this.handle_feedback_click(
741 ThreadFeedback::Negative,
742 window,
743 cx,
744 );
745 })),
746 ),
747 )
748 .into_any_element(),
749 };
750
751 let message_content = v_flex()
752 .gap_1p5()
753 .child(
754 if let Some(edit_message_editor) = edit_message_editor.clone() {
755 div()
756 .key_context("EditMessageEditor")
757 .on_action(cx.listener(Self::cancel_editing_message))
758 .on_action(cx.listener(Self::confirm_editing_message))
759 .min_h_6()
760 .child(edit_message_editor)
761 } else {
762 div().min_h_6().text_ui(cx).child(markdown.clone())
763 },
764 )
765 .when_some(context, |parent, context| {
766 if !context.is_empty() {
767 parent.child(
768 h_flex().flex_wrap().gap_1().children(
769 context
770 .into_iter()
771 .map(|context| ContextPill::added(context, false, false, None)),
772 ),
773 )
774 } else {
775 parent
776 }
777 });
778
779 let styled_message = match message.role {
780 Role::User => v_flex()
781 .id(("message-container", ix))
782 .py_2()
783 .pl_2()
784 .pr_2p5()
785 .child(
786 v_flex()
787 .bg(colors.editor_background)
788 .rounded_lg()
789 .border_1()
790 .border_color(colors.border)
791 .shadow_md()
792 .child(
793 h_flex()
794 .py_1()
795 .pl_2()
796 .pr_1()
797 .bg(bg_user_message_header)
798 .border_b_1()
799 .border_color(colors.border)
800 .justify_between()
801 .rounded_t_md()
802 .child(
803 h_flex()
804 .gap_1p5()
805 .child(
806 Icon::new(IconName::PersonCircle)
807 .size(IconSize::XSmall)
808 .color(Color::Muted),
809 )
810 .child(
811 Label::new("You")
812 .size(LabelSize::Small)
813 .color(Color::Muted),
814 ),
815 )
816 .child(
817 h_flex()
818 // DL: To double-check whether we want to fully remove
819 // the editing feature from meassages. Checkpoint sort of
820 // solve the same problem.
821 .invisible()
822 .gap_1()
823 .when_some(
824 edit_message_editor.clone(),
825 |this, edit_message_editor| {
826 let focus_handle =
827 edit_message_editor.focus_handle(cx);
828 this.child(
829 Button::new("cancel-edit-message", "Cancel")
830 .label_size(LabelSize::Small)
831 .key_binding(
832 KeyBinding::for_action_in(
833 &menu::Cancel,
834 &focus_handle,
835 window,
836 cx,
837 )
838 .map(|kb| kb.size(rems_from_px(12.))),
839 )
840 .on_click(
841 cx.listener(Self::handle_cancel_click),
842 ),
843 )
844 .child(
845 Button::new(
846 "confirm-edit-message",
847 "Regenerate",
848 )
849 .label_size(LabelSize::Small)
850 .key_binding(
851 KeyBinding::for_action_in(
852 &menu::Confirm,
853 &focus_handle,
854 window,
855 cx,
856 )
857 .map(|kb| kb.size(rems_from_px(12.))),
858 )
859 .on_click(
860 cx.listener(Self::handle_regenerate_click),
861 ),
862 )
863 },
864 )
865 .when(
866 edit_message_editor.is_none() && allow_editing_message,
867 |this| {
868 this.child(
869 Button::new("edit-message", "Edit")
870 .label_size(LabelSize::Small)
871 .on_click(cx.listener({
872 let message_text = message.text.clone();
873 move |this, _, window, cx| {
874 this.start_editing_message(
875 message_id,
876 message_text.clone(),
877 window,
878 cx,
879 );
880 }
881 })),
882 )
883 },
884 ),
885 ),
886 )
887 .child(div().p_2().child(message_content)),
888 ),
889 Role::Assistant => v_flex()
890 .id(("message-container", ix))
891 .child(v_flex().py_2().px_4().child(message_content))
892 .when(
893 !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
894 |parent| {
895 parent.child(
896 v_flex()
897 .children(
898 tool_uses
899 .into_iter()
900 .map(|tool_use| self.render_tool_use(tool_use, cx)),
901 )
902 .children(scripting_tool_uses.into_iter().map(|tool_use| {
903 self.render_scripting_tool_use(tool_use, window, cx)
904 })),
905 )
906 },
907 ),
908 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
909 v_flex()
910 .bg(colors.editor_background)
911 .rounded_sm()
912 .child(div().p_4().child(message_content)),
913 ),
914 };
915
916 v_flex()
917 .w_full()
918 .when(first_message, |parent| {
919 parent.child(self.render_rules_item(cx))
920 })
921 .when(!first_message && checkpoint.is_some(), |parent| {
922 let checkpoint = checkpoint.clone().unwrap();
923 let mut is_pending = false;
924 let mut error = None;
925 if let Some(last_restore_checkpoint) =
926 self.thread.read(cx).last_restore_checkpoint()
927 {
928 if last_restore_checkpoint.message_id() == message_id {
929 match last_restore_checkpoint {
930 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
931 LastRestoreCheckpoint::Error { error: err, .. } => {
932 error = Some(err.clone());
933 }
934 }
935 }
936 }
937
938 let restore_checkpoint_button =
939 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
940 .icon(if error.is_some() {
941 IconName::XCircle
942 } else {
943 IconName::Undo
944 })
945 .icon_size(IconSize::XSmall)
946 .icon_position(IconPosition::Start)
947 .icon_color(if error.is_some() {
948 Some(Color::Error)
949 } else {
950 None
951 })
952 .label_size(LabelSize::XSmall)
953 .disabled(is_pending)
954 .on_click(cx.listener(move |this, _, _window, cx| {
955 this.thread.update(cx, |thread, cx| {
956 thread
957 .restore_checkpoint(checkpoint.clone(), cx)
958 .detach_and_log_err(cx);
959 });
960 }));
961
962 let restore_checkpoint_button = if is_pending {
963 restore_checkpoint_button
964 .with_animation(
965 ("pulsating-restore-checkpoint-button", ix),
966 Animation::new(Duration::from_secs(2))
967 .repeat()
968 .with_easing(pulsating_between(0.6, 1.)),
969 |label, delta| label.alpha(delta),
970 )
971 .into_any_element()
972 } else if let Some(error) = error {
973 restore_checkpoint_button
974 .tooltip(Tooltip::text(error.to_string()))
975 .into_any_element()
976 } else {
977 restore_checkpoint_button.into_any_element()
978 };
979
980 parent.child(
981 h_flex()
982 .px_2p5()
983 .w_full()
984 .gap_1()
985 .child(ui::Divider::horizontal())
986 .child(restore_checkpoint_button)
987 .child(ui::Divider::horizontal()),
988 )
989 })
990 .child(styled_message)
991 .when(
992 is_last_message && !self.thread.read(cx).is_generating(),
993 |parent| parent.child(feedback_items),
994 )
995 .into_any()
996 }
997
998 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
999 let is_open = self
1000 .expanded_tool_uses
1001 .get(&tool_use.id)
1002 .copied()
1003 .unwrap_or_default();
1004
1005 let lighter_border = cx.theme().colors().border.opacity(0.5);
1006
1007 let tool_icon = match tool_use.name.as_ref() {
1008 "bash" => IconName::Terminal,
1009 "delete-path" => IconName::Trash,
1010 "diagnostics" => IconName::Warning,
1011 "edit-files" => IconName::Pencil,
1012 "fetch" => IconName::Globe,
1013 "list-directory" => IconName::Folder,
1014 "now" => IconName::Info,
1015 "path-search" => IconName::SearchCode,
1016 "read-file" => IconName::Eye,
1017 "regex-search" => IconName::Regex,
1018 "thinking" => IconName::Brain,
1019 _ => IconName::Terminal,
1020 };
1021
1022 div().px_4().child(
1023 v_flex()
1024 .rounded_lg()
1025 .border_1()
1026 .border_color(lighter_border)
1027 .overflow_hidden()
1028 .child(
1029 h_flex()
1030 .group("disclosure-header")
1031 .justify_between()
1032 .py_1()
1033 .px_2()
1034 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
1035 .map(|element| {
1036 if is_open {
1037 element.border_b_1().rounded_t_md()
1038 } else {
1039 element.rounded_md()
1040 }
1041 })
1042 .border_color(lighter_border)
1043 .child(
1044 h_flex()
1045 .gap_1p5()
1046 .child(
1047 Icon::new(tool_icon)
1048 .size(IconSize::XSmall)
1049 .color(Color::Muted),
1050 )
1051 .child(
1052 div()
1053 .text_ui_sm(cx)
1054 .children(
1055 self.rendered_tool_use_labels
1056 .get(&tool_use.id)
1057 .cloned(),
1058 )
1059 .truncate(),
1060 ),
1061 )
1062 .child(
1063 h_flex()
1064 .gap_1()
1065 .child(
1066 div().visible_on_hover("disclosure-header").child(
1067 Disclosure::new("tool-use-disclosure", is_open)
1068 .opened_icon(IconName::ChevronUp)
1069 .closed_icon(IconName::ChevronDown)
1070 .on_click(cx.listener({
1071 let tool_use_id = tool_use.id.clone();
1072 move |this, _event, _window, _cx| {
1073 let is_open = this
1074 .expanded_tool_uses
1075 .entry(tool_use_id.clone())
1076 .or_insert(false);
1077
1078 *is_open = !*is_open;
1079 }
1080 })),
1081 ),
1082 )
1083 .child({
1084 let (icon_name, color, animated) = match &tool_use.status {
1085 ToolUseStatus::Pending => {
1086 (IconName::Warning, Color::Warning, false)
1087 }
1088 ToolUseStatus::Running => {
1089 (IconName::ArrowCircle, Color::Accent, true)
1090 }
1091 ToolUseStatus::Finished(_) => {
1092 (IconName::Check, Color::Success, false)
1093 }
1094 ToolUseStatus::Error(_) => {
1095 (IconName::Close, Color::Error, false)
1096 }
1097 };
1098
1099 let icon =
1100 Icon::new(icon_name).color(color).size(IconSize::Small);
1101
1102 if animated {
1103 icon.with_animation(
1104 "arrow-circle",
1105 Animation::new(Duration::from_secs(2)).repeat(),
1106 |icon, delta| {
1107 icon.transform(Transformation::rotate(percentage(
1108 delta,
1109 )))
1110 },
1111 )
1112 .into_any_element()
1113 } else {
1114 icon.into_any_element()
1115 }
1116 }),
1117 ),
1118 )
1119 .map(|parent| {
1120 if !is_open {
1121 return parent;
1122 }
1123
1124 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1125
1126 parent.child(
1127 v_flex()
1128 .gap_1()
1129 .bg(cx.theme().colors().editor_background)
1130 .rounded_b_lg()
1131 .child(
1132 content_container()
1133 .border_b_1()
1134 .border_color(lighter_border)
1135 .child(
1136 Label::new("Input")
1137 .size(LabelSize::XSmall)
1138 .color(Color::Muted)
1139 .buffer_font(cx),
1140 )
1141 .child(
1142 Label::new(
1143 serde_json::to_string_pretty(&tool_use.input)
1144 .unwrap_or_default(),
1145 )
1146 .size(LabelSize::Small)
1147 .buffer_font(cx),
1148 ),
1149 )
1150 .map(|container| match tool_use.status {
1151 ToolUseStatus::Finished(output) => container.child(
1152 content_container()
1153 .child(
1154 Label::new("Result")
1155 .size(LabelSize::XSmall)
1156 .color(Color::Muted)
1157 .buffer_font(cx),
1158 )
1159 .child(
1160 Label::new(output)
1161 .size(LabelSize::Small)
1162 .buffer_font(cx),
1163 ),
1164 ),
1165 ToolUseStatus::Running => container.child(
1166 content_container().child(
1167 h_flex()
1168 .gap_1()
1169 .pb_1()
1170 .child(
1171 Icon::new(IconName::ArrowCircle)
1172 .size(IconSize::Small)
1173 .color(Color::Accent)
1174 .with_animation(
1175 "arrow-circle",
1176 Animation::new(Duration::from_secs(2))
1177 .repeat(),
1178 |icon, delta| {
1179 icon.transform(Transformation::rotate(
1180 percentage(delta),
1181 ))
1182 },
1183 ),
1184 )
1185 .child(
1186 Label::new("Running…")
1187 .size(LabelSize::XSmall)
1188 .color(Color::Muted)
1189 .buffer_font(cx),
1190 ),
1191 ),
1192 ),
1193 ToolUseStatus::Error(err) => container.child(
1194 content_container()
1195 .child(
1196 Label::new("Error")
1197 .size(LabelSize::XSmall)
1198 .color(Color::Muted)
1199 .buffer_font(cx),
1200 )
1201 .child(
1202 Label::new(err).size(LabelSize::Small).buffer_font(cx),
1203 ),
1204 ),
1205 ToolUseStatus::Pending => container,
1206 }),
1207 )
1208 }),
1209 )
1210 }
1211
1212 fn render_scripting_tool_use(
1213 &self,
1214 tool_use: ToolUse,
1215 window: &Window,
1216 cx: &mut Context<Self>,
1217 ) -> impl IntoElement {
1218 let is_open = self
1219 .expanded_tool_uses
1220 .get(&tool_use.id)
1221 .copied()
1222 .unwrap_or_default();
1223
1224 div().px_2p5().child(
1225 v_flex()
1226 .gap_1()
1227 .rounded_lg()
1228 .border_1()
1229 .border_color(cx.theme().colors().border)
1230 .child(
1231 h_flex()
1232 .justify_between()
1233 .py_0p5()
1234 .pl_1()
1235 .pr_2()
1236 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
1237 .map(|element| {
1238 if is_open {
1239 element.border_b_1().rounded_t_md()
1240 } else {
1241 element.rounded_md()
1242 }
1243 })
1244 .border_color(cx.theme().colors().border)
1245 .child(
1246 h_flex()
1247 .gap_1()
1248 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
1249 cx.listener({
1250 let tool_use_id = tool_use.id.clone();
1251 move |this, _event, _window, _cx| {
1252 let is_open = this
1253 .expanded_tool_uses
1254 .entry(tool_use_id.clone())
1255 .or_insert(false);
1256
1257 *is_open = !*is_open;
1258 }
1259 }),
1260 ))
1261 .child(div().text_ui_sm(cx).child(self.render_markdown(
1262 tool_use.ui_text.clone(),
1263 window,
1264 cx,
1265 )))
1266 .truncate(),
1267 )
1268 .child(
1269 Label::new(match tool_use.status {
1270 ToolUseStatus::Pending => "Pending",
1271 ToolUseStatus::Running => "Running",
1272 ToolUseStatus::Finished(_) => "Finished",
1273 ToolUseStatus::Error(_) => "Error",
1274 })
1275 .size(LabelSize::XSmall)
1276 .buffer_font(cx),
1277 ),
1278 )
1279 .map(|parent| {
1280 if !is_open {
1281 return parent;
1282 }
1283
1284 let lua_script_markdown =
1285 self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
1286
1287 parent.child(
1288 v_flex()
1289 .child(
1290 v_flex()
1291 .gap_0p5()
1292 .py_1()
1293 .px_2p5()
1294 .border_b_1()
1295 .border_color(cx.theme().colors().border)
1296 .child(Label::new("Input:"))
1297 .map(|parent| {
1298 if let Some(markdown) = lua_script_markdown {
1299 parent.child(markdown)
1300 } else {
1301 parent.child(Label::new(
1302 "Failed to render script input to Markdown",
1303 ))
1304 }
1305 }),
1306 )
1307 .map(|parent| match tool_use.status {
1308 ToolUseStatus::Finished(output) => parent.child(
1309 v_flex()
1310 .gap_0p5()
1311 .py_1()
1312 .px_2p5()
1313 .child(Label::new("Result:"))
1314 .child(Label::new(output)),
1315 ),
1316 ToolUseStatus::Error(err) => parent.child(
1317 v_flex()
1318 .gap_0p5()
1319 .py_1()
1320 .px_2p5()
1321 .child(Label::new("Error:"))
1322 .child(Label::new(err)),
1323 ),
1324 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
1325 }),
1326 )
1327 }),
1328 )
1329 }
1330
1331 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1332 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1333 else {
1334 return div().into_any();
1335 };
1336
1337 let rules_files = system_prompt_context
1338 .worktrees
1339 .iter()
1340 .filter_map(|worktree| worktree.rules_file.as_ref())
1341 .collect::<Vec<_>>();
1342
1343 let label_text = match rules_files.as_slice() {
1344 &[] => return div().into_any(),
1345 &[rules_file] => {
1346 format!("Using {:?} file", rules_file.rel_path)
1347 }
1348 rules_files => {
1349 format!("Using {} rules files", rules_files.len())
1350 }
1351 };
1352
1353 div()
1354 .pt_1()
1355 .px_2p5()
1356 .child(
1357 h_flex()
1358 .w_full()
1359 .gap_0p5()
1360 .child(
1361 h_flex()
1362 .gap_1p5()
1363 .child(
1364 Icon::new(IconName::File)
1365 .size(IconSize::XSmall)
1366 .color(Color::Disabled),
1367 )
1368 .child(
1369 Label::new(label_text)
1370 .size(LabelSize::XSmall)
1371 .color(Color::Muted)
1372 .buffer_font(cx),
1373 ),
1374 )
1375 .child(
1376 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1377 .shape(ui::IconButtonShape::Square)
1378 .icon_size(IconSize::XSmall)
1379 .icon_color(Color::Ignored)
1380 .on_click(cx.listener(Self::handle_open_rules))
1381 .tooltip(Tooltip::text("View Rules")),
1382 ),
1383 )
1384 .into_any()
1385 }
1386
1387 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1388 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1389 else {
1390 return;
1391 };
1392
1393 let abs_paths = system_prompt_context
1394 .worktrees
1395 .iter()
1396 .flat_map(|worktree| worktree.rules_file.as_ref())
1397 .map(|rules_file| rules_file.abs_path.to_path_buf())
1398 .collect::<Vec<_>>();
1399
1400 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1401 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1402 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1403 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1404 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1405 }) {
1406 task.detach();
1407 }
1408 }
1409}
1410
1411impl Render for ActiveThread {
1412 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1413 v_flex()
1414 .size_full()
1415 .child(list(self.list_state.clone()).flex_grow())
1416 }
1417}