1use crate::thread::{
2 LastRestoreCheckpoint, MessageId, RequestKind, Thread, ThreadError, ThreadEvent,
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, 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 render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
597 let message_id = self.messages[ix];
598 let Some(message) = self.thread.read(cx).message(message_id) else {
599 return Empty.into_any();
600 };
601
602 let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
603 return Empty.into_any();
604 };
605
606 let thread = self.thread.read(cx);
607 // Get all the data we need from thread before we start using it in closures
608 let checkpoint = thread.checkpoint_for_message(message_id);
609 let context = thread.context_for_message(message_id);
610 let tool_uses = thread.tool_uses_for_message(message_id, cx);
611 let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id, cx);
612
613 // Don't render user messages that are just there for returning tool results.
614 if message.role == Role::User
615 && (thread.message_has_tool_results(message_id)
616 || thread.message_has_scripting_tool_results(message_id))
617 {
618 return Empty.into_any();
619 }
620
621 let allow_editing_message =
622 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
623
624 let edit_message_editor = self
625 .editing_message
626 .as_ref()
627 .filter(|(id, _)| *id == message_id)
628 .map(|(_, state)| state.editor.clone());
629
630 let colors = cx.theme().colors();
631
632 let message_content = v_flex()
633 .child(
634 if let Some(edit_message_editor) = edit_message_editor.clone() {
635 div()
636 .key_context("EditMessageEditor")
637 .on_action(cx.listener(Self::cancel_editing_message))
638 .on_action(cx.listener(Self::confirm_editing_message))
639 .p_2p5()
640 .child(edit_message_editor)
641 } else {
642 div().text_ui(cx).child(markdown.clone())
643 },
644 )
645 .when_some(context, |parent, context| {
646 if !context.is_empty() {
647 parent.child(
648 h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
649 context
650 .into_iter()
651 .map(|context| ContextPill::added(context, false, false, None)),
652 ),
653 )
654 } else {
655 parent
656 }
657 });
658
659 let styled_message = match message.role {
660 Role::User => v_flex()
661 .id(("message-container", ix))
662 .pt_2()
663 .pl_2()
664 .pr_2p5()
665 .child(
666 v_flex()
667 .bg(colors.editor_background)
668 .rounded_lg()
669 .border_1()
670 .border_color(colors.border)
671 .shadow_md()
672 .child(
673 h_flex()
674 .py_1()
675 .pl_2()
676 .pr_1()
677 .bg(colors.editor_foreground.opacity(0.05))
678 .border_b_1()
679 .border_color(colors.border)
680 .justify_between()
681 .rounded_t(px(6.))
682 .child(
683 h_flex()
684 .gap_1p5()
685 .child(
686 Icon::new(IconName::PersonCircle)
687 .size(IconSize::XSmall)
688 .color(Color::Muted),
689 )
690 .child(
691 Label::new("You")
692 .size(LabelSize::Small)
693 .color(Color::Muted),
694 ),
695 )
696 .when_some(
697 edit_message_editor.clone(),
698 |this, edit_message_editor| {
699 let focus_handle = edit_message_editor.focus_handle(cx);
700 this.child(
701 h_flex()
702 .gap_1()
703 .child(
704 Button::new("cancel-edit-message", "Cancel")
705 .label_size(LabelSize::Small)
706 .key_binding(
707 KeyBinding::for_action_in(
708 &menu::Cancel,
709 &focus_handle,
710 window,
711 cx,
712 )
713 .map(|kb| kb.size(rems_from_px(12.))),
714 )
715 .on_click(
716 cx.listener(Self::handle_cancel_click),
717 ),
718 )
719 .child(
720 Button::new(
721 "confirm-edit-message",
722 "Regenerate",
723 )
724 .label_size(LabelSize::Small)
725 .key_binding(
726 KeyBinding::for_action_in(
727 &menu::Confirm,
728 &focus_handle,
729 window,
730 cx,
731 )
732 .map(|kb| kb.size(rems_from_px(12.))),
733 )
734 .on_click(
735 cx.listener(Self::handle_regenerate_click),
736 ),
737 ),
738 )
739 },
740 )
741 .when(
742 edit_message_editor.is_none() && allow_editing_message,
743 |this| {
744 this.child(
745 Button::new("edit-message", "Edit")
746 .label_size(LabelSize::Small)
747 .on_click(cx.listener({
748 let message_text = message.text.clone();
749 move |this, _, window, cx| {
750 this.start_editing_message(
751 message_id,
752 message_text.clone(),
753 window,
754 cx,
755 );
756 }
757 })),
758 )
759 },
760 ),
761 )
762 .child(div().p_2().child(message_content)),
763 ),
764 Role::Assistant => v_flex()
765 .id(("message-container", ix))
766 .child(div().py_3().px_4().child(message_content))
767 .when(
768 !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
769 |parent| {
770 parent.child(
771 v_flex()
772 .children(
773 tool_uses
774 .into_iter()
775 .map(|tool_use| self.render_tool_use(tool_use, cx)),
776 )
777 .children(scripting_tool_uses.into_iter().map(|tool_use| {
778 self.render_scripting_tool_use(tool_use, window, cx)
779 })),
780 )
781 },
782 ),
783 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
784 v_flex()
785 .bg(colors.editor_background)
786 .rounded_sm()
787 .child(div().p_4().child(message_content)),
788 ),
789 };
790
791 v_flex()
792 .when(ix == 0, |parent| parent.child(self.render_rules_item(cx)))
793 .when_some(checkpoint, |parent, checkpoint| {
794 let mut is_pending = false;
795 let mut error = None;
796 if let Some(last_restore_checkpoint) =
797 self.thread.read(cx).last_restore_checkpoint()
798 {
799 if last_restore_checkpoint.message_id() == message_id {
800 match last_restore_checkpoint {
801 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
802 LastRestoreCheckpoint::Error { error: err, .. } => {
803 error = Some(err.clone());
804 }
805 }
806 }
807 }
808
809 let restore_checkpoint_button =
810 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
811 .icon(if error.is_some() {
812 IconName::XCircle
813 } else {
814 IconName::Undo
815 })
816 .size(ButtonSize::Compact)
817 .disabled(is_pending)
818 .icon_color(if error.is_some() {
819 Some(Color::Error)
820 } else {
821 None
822 })
823 .on_click(cx.listener(move |this, _, _window, cx| {
824 this.thread.update(cx, |thread, cx| {
825 thread
826 .restore_checkpoint(checkpoint.clone(), cx)
827 .detach_and_log_err(cx);
828 });
829 }));
830
831 let restore_checkpoint_button = if is_pending {
832 restore_checkpoint_button
833 .with_animation(
834 ("pulsating-restore-checkpoint-button", ix),
835 Animation::new(Duration::from_secs(2))
836 .repeat()
837 .with_easing(pulsating_between(0.6, 1.)),
838 |label, delta| label.alpha(delta),
839 )
840 .into_any_element()
841 } else if let Some(error) = error {
842 restore_checkpoint_button
843 .tooltip(Tooltip::text(error.to_string()))
844 .into_any_element()
845 } else {
846 restore_checkpoint_button.into_any_element()
847 };
848
849 parent.child(h_flex().pl_2().child(restore_checkpoint_button))
850 })
851 .child(styled_message)
852 .into_any()
853 }
854
855 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
856 let is_open = self
857 .expanded_tool_uses
858 .get(&tool_use.id)
859 .copied()
860 .unwrap_or_default();
861
862 let lighter_border = cx.theme().colors().border.opacity(0.5);
863
864 div().px_4().child(
865 v_flex()
866 .rounded_lg()
867 .border_1()
868 .border_color(lighter_border)
869 .child(
870 h_flex()
871 .justify_between()
872 .py_1()
873 .pl_1()
874 .pr_2()
875 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
876 .map(|element| {
877 if is_open {
878 element.border_b_1().rounded_t_md()
879 } else {
880 element.rounded_md()
881 }
882 })
883 .border_color(lighter_border)
884 .child(
885 h_flex()
886 .gap_1()
887 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
888 cx.listener({
889 let tool_use_id = tool_use.id.clone();
890 move |this, _event, _window, _cx| {
891 let is_open = this
892 .expanded_tool_uses
893 .entry(tool_use_id.clone())
894 .or_insert(false);
895
896 *is_open = !*is_open;
897 }
898 }),
899 ))
900 .child(div().text_ui_sm(cx).children(
901 self.rendered_tool_use_labels.get(&tool_use.id).cloned(),
902 ))
903 .truncate(),
904 )
905 .child({
906 let (icon_name, color, animated) = match &tool_use.status {
907 ToolUseStatus::Pending => {
908 (IconName::Warning, Color::Warning, false)
909 }
910 ToolUseStatus::Running => {
911 (IconName::ArrowCircle, Color::Accent, true)
912 }
913 ToolUseStatus::Finished(_) => {
914 (IconName::Check, Color::Success, false)
915 }
916 ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
917 };
918
919 let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
920
921 if animated {
922 icon.with_animation(
923 "arrow-circle",
924 Animation::new(Duration::from_secs(2)).repeat(),
925 |icon, delta| {
926 icon.transform(Transformation::rotate(percentage(delta)))
927 },
928 )
929 .into_any_element()
930 } else {
931 icon.into_any_element()
932 }
933 }),
934 )
935 .map(|parent| {
936 if !is_open {
937 return parent;
938 }
939
940 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
941
942 parent.child(
943 v_flex()
944 .gap_1()
945 .bg(cx.theme().colors().editor_background)
946 .rounded_b_lg()
947 .child(
948 content_container()
949 .border_b_1()
950 .border_color(lighter_border)
951 .child(
952 Label::new("Input")
953 .size(LabelSize::XSmall)
954 .color(Color::Muted)
955 .buffer_font(cx),
956 )
957 .child(
958 Label::new(
959 serde_json::to_string_pretty(&tool_use.input)
960 .unwrap_or_default(),
961 )
962 .size(LabelSize::Small)
963 .buffer_font(cx),
964 ),
965 )
966 .map(|container| match tool_use.status {
967 ToolUseStatus::Finished(output) => container.child(
968 content_container()
969 .child(
970 Label::new("Result")
971 .size(LabelSize::XSmall)
972 .color(Color::Muted)
973 .buffer_font(cx),
974 )
975 .child(
976 Label::new(output)
977 .size(LabelSize::Small)
978 .buffer_font(cx),
979 ),
980 ),
981 ToolUseStatus::Running => container.child(
982 content_container().child(
983 h_flex()
984 .gap_1()
985 .pb_1()
986 .child(
987 Icon::new(IconName::ArrowCircle)
988 .size(IconSize::Small)
989 .color(Color::Accent)
990 .with_animation(
991 "arrow-circle",
992 Animation::new(Duration::from_secs(2))
993 .repeat(),
994 |icon, delta| {
995 icon.transform(Transformation::rotate(
996 percentage(delta),
997 ))
998 },
999 ),
1000 )
1001 .child(
1002 Label::new("Running…")
1003 .size(LabelSize::XSmall)
1004 .color(Color::Muted)
1005 .buffer_font(cx),
1006 ),
1007 ),
1008 ),
1009 ToolUseStatus::Error(err) => container.child(
1010 content_container()
1011 .child(
1012 Label::new("Error")
1013 .size(LabelSize::XSmall)
1014 .color(Color::Muted)
1015 .buffer_font(cx),
1016 )
1017 .child(
1018 Label::new(err).size(LabelSize::Small).buffer_font(cx),
1019 ),
1020 ),
1021 ToolUseStatus::Pending => container,
1022 }),
1023 )
1024 }),
1025 )
1026 }
1027
1028 fn render_scripting_tool_use(
1029 &self,
1030 tool_use: ToolUse,
1031 window: &Window,
1032 cx: &mut Context<Self>,
1033 ) -> impl IntoElement {
1034 let is_open = self
1035 .expanded_tool_uses
1036 .get(&tool_use.id)
1037 .copied()
1038 .unwrap_or_default();
1039
1040 div().px_2p5().child(
1041 v_flex()
1042 .gap_1()
1043 .rounded_lg()
1044 .border_1()
1045 .border_color(cx.theme().colors().border)
1046 .child(
1047 h_flex()
1048 .justify_between()
1049 .py_0p5()
1050 .pl_1()
1051 .pr_2()
1052 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
1053 .map(|element| {
1054 if is_open {
1055 element.border_b_1().rounded_t_md()
1056 } else {
1057 element.rounded_md()
1058 }
1059 })
1060 .border_color(cx.theme().colors().border)
1061 .child(
1062 h_flex()
1063 .gap_1()
1064 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
1065 cx.listener({
1066 let tool_use_id = tool_use.id.clone();
1067 move |this, _event, _window, _cx| {
1068 let is_open = this
1069 .expanded_tool_uses
1070 .entry(tool_use_id.clone())
1071 .or_insert(false);
1072
1073 *is_open = !*is_open;
1074 }
1075 }),
1076 ))
1077 .child(div().text_ui_sm(cx).child(self.render_markdown(
1078 tool_use.ui_text.clone(),
1079 window,
1080 cx,
1081 )))
1082 .truncate(),
1083 )
1084 .child(
1085 Label::new(match tool_use.status {
1086 ToolUseStatus::Pending => "Pending",
1087 ToolUseStatus::Running => "Running",
1088 ToolUseStatus::Finished(_) => "Finished",
1089 ToolUseStatus::Error(_) => "Error",
1090 })
1091 .size(LabelSize::XSmall)
1092 .buffer_font(cx),
1093 ),
1094 )
1095 .map(|parent| {
1096 if !is_open {
1097 return parent;
1098 }
1099
1100 let lua_script_markdown =
1101 self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
1102
1103 parent.child(
1104 v_flex()
1105 .child(
1106 v_flex()
1107 .gap_0p5()
1108 .py_1()
1109 .px_2p5()
1110 .border_b_1()
1111 .border_color(cx.theme().colors().border)
1112 .child(Label::new("Input:"))
1113 .map(|parent| {
1114 if let Some(markdown) = lua_script_markdown {
1115 parent.child(markdown)
1116 } else {
1117 parent.child(Label::new(
1118 "Failed to render script input to Markdown",
1119 ))
1120 }
1121 }),
1122 )
1123 .map(|parent| match tool_use.status {
1124 ToolUseStatus::Finished(output) => parent.child(
1125 v_flex()
1126 .gap_0p5()
1127 .py_1()
1128 .px_2p5()
1129 .child(Label::new("Result:"))
1130 .child(Label::new(output)),
1131 ),
1132 ToolUseStatus::Error(err) => parent.child(
1133 v_flex()
1134 .gap_0p5()
1135 .py_1()
1136 .px_2p5()
1137 .child(Label::new("Error:"))
1138 .child(Label::new(err)),
1139 ),
1140 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
1141 }),
1142 )
1143 }),
1144 )
1145 }
1146
1147 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1148 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1149 else {
1150 return div().into_any();
1151 };
1152
1153 let rules_files = system_prompt_context
1154 .worktrees
1155 .iter()
1156 .filter_map(|worktree| worktree.rules_file.as_ref())
1157 .collect::<Vec<_>>();
1158
1159 let label_text = match rules_files.as_slice() {
1160 &[] => return div().into_any(),
1161 &[rules_file] => {
1162 format!("Using {:?} file", rules_file.rel_path)
1163 }
1164 rules_files => {
1165 format!("Using {} rules files", rules_files.len())
1166 }
1167 };
1168
1169 div()
1170 .pt_1()
1171 .px_2p5()
1172 .child(
1173 h_flex()
1174 .group("rules-item")
1175 .w_full()
1176 .gap_2()
1177 .justify_between()
1178 .child(
1179 h_flex()
1180 .gap_1p5()
1181 .child(
1182 Icon::new(IconName::File)
1183 .size(IconSize::XSmall)
1184 .color(Color::Disabled),
1185 )
1186 .child(
1187 Label::new(label_text)
1188 .size(LabelSize::XSmall)
1189 .color(Color::Muted)
1190 .buffer_font(cx),
1191 ),
1192 )
1193 .child(
1194 div().visible_on_hover("rules-item").child(
1195 Button::new("open-rules", "Open Rules")
1196 .label_size(LabelSize::XSmall)
1197 .on_click(cx.listener(Self::handle_open_rules)),
1198 ),
1199 ),
1200 )
1201 .into_any()
1202 }
1203
1204 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1205 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1206 else {
1207 return;
1208 };
1209
1210 let abs_paths = system_prompt_context
1211 .worktrees
1212 .iter()
1213 .flat_map(|worktree| worktree.rules_file.as_ref())
1214 .map(|rules_file| rules_file.abs_path.to_path_buf())
1215 .collect::<Vec<_>>();
1216
1217 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1218 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1219 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1220 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1221 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1222 }) {
1223 task.detach();
1224 }
1225 }
1226}
1227
1228impl Render for ActiveThread {
1229 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1230 v_flex()
1231 .size_full()
1232 .child(list(self.list_state.clone()).flex_grow())
1233 }
1234}