1use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
2use crate::thread_store::ThreadStore;
3use crate::tool_use::{ToolUse, ToolUseStatus};
4use crate::ui::ContextPill;
5use collections::HashMap;
6use editor::{Editor, MultiBuffer};
7use gpui::{
8 list, percentage, AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent,
9 DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment, ListOffset,
10 ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation,
11 UnderlineStyle,
12};
13use language::{Buffer, LanguageRegistry};
14use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
15use markdown::{Markdown, MarkdownStyle};
16use scripting_tool::{ScriptingTool, ScriptingToolInput};
17use settings::Settings as _;
18use std::sync::Arc;
19use std::time::Duration;
20use theme::ThemeSettings;
21use ui::Color;
22use ui::{prelude::*, Disclosure, KeyBinding};
23use util::ResultExt as _;
24
25use crate::context_store::{refresh_context_store_text, ContextStore};
26
27pub struct ActiveThread {
28 language_registry: Arc<LanguageRegistry>,
29 thread_store: Entity<ThreadStore>,
30 thread: Entity<Thread>,
31 context_store: Entity<ContextStore>,
32 save_thread_task: Option<Task<()>>,
33 messages: Vec<MessageId>,
34 list_state: ListState,
35 rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
36 rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
37 editing_message: Option<(MessageId, EditMessageState)>,
38 expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
39 last_error: Option<ThreadError>,
40 _subscriptions: Vec<Subscription>,
41}
42
43struct EditMessageState {
44 editor: Entity<Editor>,
45}
46
47impl ActiveThread {
48 pub fn new(
49 thread: Entity<Thread>,
50 thread_store: Entity<ThreadStore>,
51 language_registry: Arc<LanguageRegistry>,
52 context_store: Entity<ContextStore>,
53 window: &mut Window,
54 cx: &mut Context<Self>,
55 ) -> Self {
56 let subscriptions = vec![
57 cx.observe(&thread, |_, _, cx| cx.notify()),
58 cx.subscribe_in(&thread, window, Self::handle_thread_event),
59 ];
60
61 let mut this = Self {
62 language_registry,
63 thread_store,
64 thread: thread.clone(),
65 context_store,
66 save_thread_task: None,
67 messages: Vec::new(),
68 rendered_messages_by_id: HashMap::default(),
69 rendered_scripting_tool_uses: HashMap::default(),
70 expanded_tool_uses: HashMap::default(),
71 list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
72 let this = cx.entity().downgrade();
73 move |ix, window: &mut Window, cx: &mut App| {
74 this.update(cx, |this, cx| this.render_message(ix, window, cx))
75 .unwrap()
76 }
77 }),
78 editing_message: None,
79 last_error: None,
80 _subscriptions: subscriptions,
81 };
82
83 for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
84 this.push_message(&message.id, message.text.clone(), window, cx);
85
86 for tool_use in thread.read(cx).scripting_tool_uses_for_message(message.id) {
87 this.render_scripting_tool_use_markdown(
88 tool_use.id.clone(),
89 tool_use.name.as_ref(),
90 tool_use.input.clone(),
91 window,
92 cx,
93 );
94 }
95 }
96
97 this
98 }
99
100 pub fn thread(&self) -> &Entity<Thread> {
101 &self.thread
102 }
103
104 pub fn is_empty(&self) -> bool {
105 self.messages.is_empty()
106 }
107
108 pub fn summary(&self, cx: &App) -> Option<SharedString> {
109 self.thread.read(cx).summary()
110 }
111
112 pub fn summary_or_default(&self, cx: &App) -> SharedString {
113 self.thread.read(cx).summary_or_default()
114 }
115
116 pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
117 self.last_error.take();
118 self.thread
119 .update(cx, |thread, _cx| thread.cancel_last_completion())
120 }
121
122 pub fn last_error(&self) -> Option<ThreadError> {
123 self.last_error.clone()
124 }
125
126 pub fn clear_last_error(&mut self) {
127 self.last_error.take();
128 }
129
130 fn push_message(
131 &mut self,
132 id: &MessageId,
133 text: String,
134 window: &mut Window,
135 cx: &mut Context<Self>,
136 ) {
137 let old_len = self.messages.len();
138 self.messages.push(*id);
139 self.list_state.splice(old_len..old_len, 1);
140
141 let markdown = self.render_markdown(text.into(), window, cx);
142 self.rendered_messages_by_id.insert(*id, markdown);
143 self.list_state.scroll_to(ListOffset {
144 item_ix: old_len,
145 offset_in_item: Pixels(0.0),
146 });
147 }
148
149 fn edited_message(
150 &mut self,
151 id: &MessageId,
152 text: String,
153 window: &mut Window,
154 cx: &mut Context<Self>,
155 ) {
156 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
157 return;
158 };
159 self.list_state.splice(index..index + 1, 1);
160 let markdown = self.render_markdown(text.into(), window, cx);
161 self.rendered_messages_by_id.insert(*id, markdown);
162 }
163
164 fn deleted_message(&mut self, id: &MessageId) {
165 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
166 return;
167 };
168 self.messages.remove(index);
169 self.list_state.splice(index..index + 1, 0);
170 self.rendered_messages_by_id.remove(id);
171 }
172
173 fn render_markdown(
174 &self,
175 text: SharedString,
176 window: &Window,
177 cx: &mut Context<Self>,
178 ) -> Entity<Markdown> {
179 let theme_settings = ThemeSettings::get_global(cx);
180 let colors = cx.theme().colors();
181 let ui_font_size = TextSize::Default.rems(cx);
182 let buffer_font_size = TextSize::Small.rems(cx);
183 let mut text_style = window.text_style();
184
185 text_style.refine(&TextStyleRefinement {
186 font_family: Some(theme_settings.ui_font.family.clone()),
187 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
188 font_features: Some(theme_settings.ui_font.features.clone()),
189 font_size: Some(ui_font_size.into()),
190 color: Some(cx.theme().colors().text),
191 ..Default::default()
192 });
193
194 let markdown_style = MarkdownStyle {
195 base_text_style: text_style,
196 syntax: cx.theme().syntax().clone(),
197 selection_background_color: cx.theme().players().local().selection,
198 code_block_overflow_x_scroll: true,
199 table_overflow_x_scroll: true,
200 code_block: StyleRefinement {
201 margin: EdgesRefinement {
202 top: Some(Length::Definite(rems(0.).into())),
203 left: Some(Length::Definite(rems(0.).into())),
204 right: Some(Length::Definite(rems(0.).into())),
205 bottom: Some(Length::Definite(rems(0.5).into())),
206 },
207 padding: EdgesRefinement {
208 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
209 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
210 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
211 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
212 },
213 background: Some(colors.editor_background.into()),
214 border_color: Some(colors.border_variant),
215 border_widths: EdgesRefinement {
216 top: Some(AbsoluteLength::Pixels(Pixels(1.))),
217 left: Some(AbsoluteLength::Pixels(Pixels(1.))),
218 right: Some(AbsoluteLength::Pixels(Pixels(1.))),
219 bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
220 },
221 text: Some(TextStyleRefinement {
222 font_family: Some(theme_settings.buffer_font.family.clone()),
223 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
224 font_features: Some(theme_settings.buffer_font.features.clone()),
225 font_size: Some(buffer_font_size.into()),
226 ..Default::default()
227 }),
228 ..Default::default()
229 },
230 inline_code: TextStyleRefinement {
231 font_family: Some(theme_settings.buffer_font.family.clone()),
232 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
233 font_features: Some(theme_settings.buffer_font.features.clone()),
234 font_size: Some(buffer_font_size.into()),
235 background_color: Some(colors.editor_foreground.opacity(0.1)),
236 ..Default::default()
237 },
238 link: TextStyleRefinement {
239 background_color: Some(colors.editor_foreground.opacity(0.025)),
240 underline: Some(UnderlineStyle {
241 color: Some(colors.text_accent.opacity(0.5)),
242 thickness: px(1.),
243 ..Default::default()
244 }),
245 ..Default::default()
246 },
247 ..Default::default()
248 };
249
250 cx.new(|cx| {
251 Markdown::new(
252 text,
253 markdown_style,
254 Some(self.language_registry.clone()),
255 None,
256 cx,
257 )
258 })
259 }
260
261 /// Renders the input of a scripting tool use to Markdown.
262 ///
263 /// Does nothing if the tool use does not correspond to the scripting tool.
264 fn render_scripting_tool_use_markdown(
265 &mut self,
266 tool_use_id: LanguageModelToolUseId,
267 tool_name: &str,
268 tool_input: serde_json::Value,
269 window: &mut Window,
270 cx: &mut Context<Self>,
271 ) {
272 if tool_name != ScriptingTool::NAME {
273 return;
274 }
275
276 let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
277 .map(|input| input.lua_script)
278 .unwrap_or_default();
279
280 let lua_script =
281 self.render_markdown(format!("```lua\n{lua_script}\n```").into(), window, cx);
282
283 self.rendered_scripting_tool_uses
284 .insert(tool_use_id, lua_script);
285 }
286
287 fn handle_thread_event(
288 &mut self,
289 _thread: &Entity<Thread>,
290 event: &ThreadEvent,
291 window: &mut Window,
292 cx: &mut Context<Self>,
293 ) {
294 match event {
295 ThreadEvent::ShowError(error) => {
296 self.last_error = Some(error.clone());
297 }
298 ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
299 self.save_thread(cx);
300 }
301 ThreadEvent::DoneStreaming => {}
302 ThreadEvent::StreamedAssistantText(message_id, text) => {
303 if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
304 markdown.update(cx, |markdown, cx| {
305 markdown.append(text, cx);
306 });
307 }
308 }
309 ThreadEvent::MessageAdded(message_id) => {
310 if let Some(message_text) = self
311 .thread
312 .read(cx)
313 .message(*message_id)
314 .map(|message| message.text.clone())
315 {
316 self.push_message(message_id, message_text, window, cx);
317 }
318
319 self.save_thread(cx);
320 cx.notify();
321 }
322 ThreadEvent::MessageEdited(message_id) => {
323 if let Some(message_text) = self
324 .thread
325 .read(cx)
326 .message(*message_id)
327 .map(|message| message.text.clone())
328 {
329 self.edited_message(message_id, message_text, window, cx);
330 }
331
332 self.save_thread(cx);
333 cx.notify();
334 }
335 ThreadEvent::MessageDeleted(message_id) => {
336 self.deleted_message(message_id);
337 self.save_thread(cx);
338 cx.notify();
339 }
340 ThreadEvent::UsePendingTools => {
341 self.thread.update(cx, |thread, cx| {
342 thread.use_pending_tools(cx);
343 });
344 }
345 ThreadEvent::ToolFinished {
346 pending_tool_use, ..
347 } => {
348 if let Some(tool_use) = pending_tool_use {
349 self.render_scripting_tool_use_markdown(
350 tool_use.id.clone(),
351 tool_use.name.as_ref(),
352 tool_use.input.clone(),
353 window,
354 cx,
355 );
356 }
357
358 if self.thread.read(cx).all_tools_finished() {
359 let pending_refresh_buffers = self.thread.update(cx, |thread, cx| {
360 thread.action_log().update(cx, |action_log, _cx| {
361 action_log.take_pending_refresh_buffers()
362 })
363 });
364
365 let context_update_task = if !pending_refresh_buffers.is_empty() {
366 let refresh_task = refresh_context_store_text(
367 self.context_store.clone(),
368 &pending_refresh_buffers,
369 cx,
370 );
371
372 cx.spawn(|this, mut cx| async move {
373 let updated_context_ids = refresh_task.await;
374
375 this.update(&mut cx, |this, cx| {
376 this.context_store.read_with(cx, |context_store, cx| {
377 context_store
378 .context()
379 .iter()
380 .filter(|context| {
381 updated_context_ids.contains(&context.id())
382 })
383 .flat_map(|context| context.snapshot(cx))
384 .collect()
385 })
386 })
387 })
388 } else {
389 Task::ready(anyhow::Ok(Vec::new()))
390 };
391
392 let model_registry = LanguageModelRegistry::read_global(cx);
393 if let Some(model) = model_registry.active_model() {
394 cx.spawn(|this, mut cx| async move {
395 let updated_context = context_update_task.await?;
396
397 this.update(&mut cx, |this, cx| {
398 this.thread.update(cx, |thread, cx| {
399 thread.send_tool_results_to_model(model, updated_context, cx);
400 });
401 })
402 })
403 .detach();
404 }
405 }
406 }
407 }
408 }
409
410 /// Spawns a task to save the active thread.
411 ///
412 /// Only one task to save the thread will be in flight at a time.
413 fn save_thread(&mut self, cx: &mut Context<Self>) {
414 let thread = self.thread.clone();
415 self.save_thread_task = Some(cx.spawn(|this, mut cx| async move {
416 let task = this
417 .update(&mut cx, |this, cx| {
418 this.thread_store
419 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
420 })
421 .ok();
422
423 if let Some(task) = task {
424 task.await.log_err();
425 }
426 }));
427 }
428
429 fn start_editing_message(
430 &mut self,
431 message_id: MessageId,
432 message_text: String,
433 window: &mut Window,
434 cx: &mut Context<Self>,
435 ) {
436 let buffer = cx.new(|cx| {
437 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
438 });
439 let editor = cx.new(|cx| {
440 let mut editor = Editor::new(
441 editor::EditorMode::AutoHeight { max_lines: 8 },
442 buffer,
443 None,
444 window,
445 cx,
446 );
447 editor.focus_handle(cx).focus(window);
448 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
449 editor
450 });
451 self.editing_message = Some((
452 message_id,
453 EditMessageState {
454 editor: editor.clone(),
455 },
456 ));
457 cx.notify();
458 }
459
460 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
461 self.editing_message.take();
462 cx.notify();
463 }
464
465 fn confirm_editing_message(
466 &mut self,
467 _: &menu::Confirm,
468 _: &mut Window,
469 cx: &mut Context<Self>,
470 ) {
471 let Some((message_id, state)) = self.editing_message.take() else {
472 return;
473 };
474 let edited_text = state.editor.read(cx).text(cx);
475 self.thread.update(cx, |thread, cx| {
476 thread.edit_message(message_id, Role::User, edited_text, cx);
477 for message_id in self.messages_after(message_id) {
478 thread.delete_message(*message_id, cx);
479 }
480 });
481
482 let provider = LanguageModelRegistry::read_global(cx).active_provider();
483 if provider
484 .as_ref()
485 .map_or(false, |provider| provider.must_accept_terms(cx))
486 {
487 cx.notify();
488 return;
489 }
490 let model_registry = LanguageModelRegistry::read_global(cx);
491 let Some(model) = model_registry.active_model() else {
492 return;
493 };
494
495 self.thread.update(cx, |thread, cx| {
496 thread.send_to_model(model, RequestKind::Chat, cx)
497 });
498 cx.notify();
499 }
500
501 fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
502 self.messages
503 .iter()
504 .rev()
505 .find(|message_id| {
506 self.thread
507 .read(cx)
508 .message(**message_id)
509 .map_or(false, |message| message.role == Role::User)
510 })
511 .cloned()
512 }
513
514 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
515 self.messages
516 .iter()
517 .position(|id| *id == message_id)
518 .map(|index| &self.messages[index + 1..])
519 .unwrap_or(&[])
520 }
521
522 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
523 self.cancel_editing_message(&menu::Cancel, window, cx);
524 }
525
526 fn handle_regenerate_click(
527 &mut self,
528 _: &ClickEvent,
529 window: &mut Window,
530 cx: &mut Context<Self>,
531 ) {
532 self.confirm_editing_message(&menu::Confirm, window, cx);
533 }
534
535 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
536 let message_id = self.messages[ix];
537 let Some(message) = self.thread.read(cx).message(message_id) else {
538 return Empty.into_any();
539 };
540
541 let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
542 return Empty.into_any();
543 };
544
545 let thread = self.thread.read(cx);
546 // Get all the data we need from thread before we start using it in closures
547 let context = thread.context_for_message(message_id);
548 let tool_uses = thread.tool_uses_for_message(message_id);
549 let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
550
551 // Don't render user messages that are just there for returning tool results.
552 if message.role == Role::User
553 && (thread.message_has_tool_results(message_id)
554 || thread.message_has_scripting_tool_results(message_id))
555 {
556 return Empty.into_any();
557 }
558
559 let allow_editing_message =
560 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
561
562 let edit_message_editor = self
563 .editing_message
564 .as_ref()
565 .filter(|(id, _)| *id == message_id)
566 .map(|(_, state)| state.editor.clone());
567
568 let colors = cx.theme().colors();
569
570 let message_content = v_flex()
571 .child(
572 if let Some(edit_message_editor) = edit_message_editor.clone() {
573 div()
574 .key_context("EditMessageEditor")
575 .on_action(cx.listener(Self::cancel_editing_message))
576 .on_action(cx.listener(Self::confirm_editing_message))
577 .p_2p5()
578 .child(edit_message_editor)
579 } else {
580 div().p_2p5().text_ui(cx).child(markdown.clone())
581 },
582 )
583 .when_some(context, |parent, context| {
584 if !context.is_empty() {
585 parent.child(
586 h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
587 context
588 .into_iter()
589 .map(|context| ContextPill::added(context, false, false, None)),
590 ),
591 )
592 } else {
593 parent
594 }
595 });
596
597 let styled_message = match message.role {
598 Role::User => v_flex()
599 .id(("message-container", ix))
600 .pt_2p5()
601 .px_2p5()
602 .child(
603 v_flex()
604 .bg(colors.editor_background)
605 .rounded_lg()
606 .border_1()
607 .border_color(colors.border)
608 .shadow_sm()
609 .child(
610 h_flex()
611 .py_1()
612 .pl_2()
613 .pr_1()
614 .bg(colors.editor_foreground.opacity(0.05))
615 .border_b_1()
616 .border_color(colors.border)
617 .justify_between()
618 .rounded_t(px(6.))
619 .child(
620 h_flex()
621 .gap_1p5()
622 .child(
623 Icon::new(IconName::PersonCircle)
624 .size(IconSize::XSmall)
625 .color(Color::Muted),
626 )
627 .child(
628 Label::new("You")
629 .size(LabelSize::Small)
630 .color(Color::Muted),
631 ),
632 )
633 .when_some(
634 edit_message_editor.clone(),
635 |this, edit_message_editor| {
636 let focus_handle = edit_message_editor.focus_handle(cx);
637 this.child(
638 h_flex()
639 .gap_1()
640 .child(
641 Button::new("cancel-edit-message", "Cancel")
642 .label_size(LabelSize::Small)
643 .key_binding(
644 KeyBinding::for_action_in(
645 &menu::Cancel,
646 &focus_handle,
647 window,
648 cx,
649 )
650 .map(|kb| kb.size(rems_from_px(12.))),
651 )
652 .on_click(
653 cx.listener(Self::handle_cancel_click),
654 ),
655 )
656 .child(
657 Button::new(
658 "confirm-edit-message",
659 "Regenerate",
660 )
661 .label_size(LabelSize::Small)
662 .key_binding(
663 KeyBinding::for_action_in(
664 &menu::Confirm,
665 &focus_handle,
666 window,
667 cx,
668 )
669 .map(|kb| kb.size(rems_from_px(12.))),
670 )
671 .on_click(
672 cx.listener(Self::handle_regenerate_click),
673 ),
674 ),
675 )
676 },
677 )
678 .when(
679 edit_message_editor.is_none() && allow_editing_message,
680 |this| {
681 this.child(
682 Button::new("edit-message", "Edit")
683 .label_size(LabelSize::Small)
684 .on_click(cx.listener({
685 let message_text = message.text.clone();
686 move |this, _, window, cx| {
687 this.start_editing_message(
688 message_id,
689 message_text.clone(),
690 window,
691 cx,
692 );
693 }
694 })),
695 )
696 },
697 ),
698 )
699 .child(message_content),
700 ),
701 Role::Assistant => {
702 v_flex()
703 .id(("message-container", ix))
704 .child(message_content)
705 .when(
706 !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
707 |parent| {
708 parent.child(
709 v_flex()
710 .children(
711 tool_uses
712 .into_iter()
713 .map(|tool_use| self.render_tool_use(tool_use, cx)),
714 )
715 .children(scripting_tool_uses.into_iter().map(|tool_use| {
716 self.render_scripting_tool_use(tool_use, cx)
717 })),
718 )
719 },
720 )
721 }
722 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
723 v_flex()
724 .bg(colors.editor_background)
725 .rounded_sm()
726 .child(message_content),
727 ),
728 };
729
730 styled_message.into_any()
731 }
732
733 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
734 let is_open = self
735 .expanded_tool_uses
736 .get(&tool_use.id)
737 .copied()
738 .unwrap_or_default();
739
740 let lighter_border = cx.theme().colors().border.opacity(0.5);
741
742 div().px_2p5().child(
743 v_flex()
744 .rounded_lg()
745 .border_1()
746 .border_color(lighter_border)
747 .child(
748 h_flex()
749 .justify_between()
750 .py_1()
751 .pl_1()
752 .pr_2()
753 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
754 .map(|element| {
755 if is_open {
756 element.border_b_1().rounded_t_md()
757 } else {
758 element.rounded_md()
759 }
760 })
761 .border_color(lighter_border)
762 .child(
763 h_flex()
764 .gap_1()
765 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
766 cx.listener({
767 let tool_use_id = tool_use.id.clone();
768 move |this, _event, _window, _cx| {
769 let is_open = this
770 .expanded_tool_uses
771 .entry(tool_use_id.clone())
772 .or_insert(false);
773
774 *is_open = !*is_open;
775 }
776 }),
777 ))
778 .child(
779 Label::new(tool_use.name)
780 .size(LabelSize::Small)
781 .buffer_font(cx),
782 ),
783 )
784 .child({
785 let (icon_name, color, animated) = match &tool_use.status {
786 ToolUseStatus::Pending => {
787 (IconName::Warning, Color::Warning, false)
788 }
789 ToolUseStatus::Running => {
790 (IconName::ArrowCircle, Color::Accent, true)
791 }
792 ToolUseStatus::Finished(_) => {
793 (IconName::Check, Color::Success, false)
794 }
795 ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
796 };
797
798 let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
799
800 if animated {
801 icon.with_animation(
802 "arrow-circle",
803 Animation::new(Duration::from_secs(2)).repeat(),
804 |icon, delta| {
805 icon.transform(Transformation::rotate(percentage(delta)))
806 },
807 )
808 .into_any_element()
809 } else {
810 icon.into_any_element()
811 }
812 }),
813 )
814 .map(|parent| {
815 if !is_open {
816 return parent;
817 }
818
819 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
820
821 parent.child(
822 v_flex()
823 .gap_1()
824 .bg(cx.theme().colors().editor_background)
825 .rounded_b_lg()
826 .child(
827 content_container()
828 .border_b_1()
829 .border_color(lighter_border)
830 .child(
831 Label::new("Input")
832 .size(LabelSize::XSmall)
833 .color(Color::Muted)
834 .buffer_font(cx),
835 )
836 .child(
837 Label::new(
838 serde_json::to_string_pretty(&tool_use.input)
839 .unwrap_or_default(),
840 )
841 .size(LabelSize::Small)
842 .buffer_font(cx),
843 ),
844 )
845 .map(|container| match tool_use.status {
846 ToolUseStatus::Finished(output) => container.child(
847 content_container()
848 .child(
849 Label::new("Result")
850 .size(LabelSize::XSmall)
851 .color(Color::Muted)
852 .buffer_font(cx),
853 )
854 .child(
855 Label::new(output)
856 .size(LabelSize::Small)
857 .buffer_font(cx),
858 ),
859 ),
860 ToolUseStatus::Running => container.child(
861 content_container().child(
862 h_flex()
863 .gap_1()
864 .pb_1()
865 .child(
866 Icon::new(IconName::ArrowCircle)
867 .size(IconSize::Small)
868 .color(Color::Accent)
869 .with_animation(
870 "arrow-circle",
871 Animation::new(Duration::from_secs(2))
872 .repeat(),
873 |icon, delta| {
874 icon.transform(Transformation::rotate(
875 percentage(delta),
876 ))
877 },
878 ),
879 )
880 .child(
881 Label::new("Running…")
882 .size(LabelSize::XSmall)
883 .color(Color::Muted)
884 .buffer_font(cx),
885 ),
886 ),
887 ),
888 ToolUseStatus::Error(err) => container.child(
889 content_container()
890 .child(
891 Label::new("Error")
892 .size(LabelSize::XSmall)
893 .color(Color::Muted)
894 .buffer_font(cx),
895 )
896 .child(
897 Label::new(err).size(LabelSize::Small).buffer_font(cx),
898 ),
899 ),
900 ToolUseStatus::Pending => container,
901 }),
902 )
903 }),
904 )
905 }
906
907 fn render_scripting_tool_use(
908 &self,
909 tool_use: ToolUse,
910 cx: &mut Context<Self>,
911 ) -> impl IntoElement {
912 let is_open = self
913 .expanded_tool_uses
914 .get(&tool_use.id)
915 .copied()
916 .unwrap_or_default();
917
918 div().px_2p5().child(
919 v_flex()
920 .gap_1()
921 .rounded_lg()
922 .border_1()
923 .border_color(cx.theme().colors().border)
924 .child(
925 h_flex()
926 .justify_between()
927 .py_0p5()
928 .pl_1()
929 .pr_2()
930 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
931 .map(|element| {
932 if is_open {
933 element.border_b_1().rounded_t_md()
934 } else {
935 element.rounded_md()
936 }
937 })
938 .border_color(cx.theme().colors().border)
939 .child(
940 h_flex()
941 .gap_1()
942 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
943 cx.listener({
944 let tool_use_id = tool_use.id.clone();
945 move |this, _event, _window, _cx| {
946 let is_open = this
947 .expanded_tool_uses
948 .entry(tool_use_id.clone())
949 .or_insert(false);
950
951 *is_open = !*is_open;
952 }
953 }),
954 ))
955 .child(Label::new(tool_use.name)),
956 )
957 .child(
958 Label::new(match tool_use.status {
959 ToolUseStatus::Pending => "Pending",
960 ToolUseStatus::Running => "Running",
961 ToolUseStatus::Finished(_) => "Finished",
962 ToolUseStatus::Error(_) => "Error",
963 })
964 .size(LabelSize::XSmall)
965 .buffer_font(cx),
966 ),
967 )
968 .map(|parent| {
969 if !is_open {
970 return parent;
971 }
972
973 let lua_script_markdown =
974 self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
975
976 parent.child(
977 v_flex()
978 .child(
979 v_flex()
980 .gap_0p5()
981 .py_1()
982 .px_2p5()
983 .border_b_1()
984 .border_color(cx.theme().colors().border)
985 .child(Label::new("Input:"))
986 .map(|parent| {
987 if let Some(markdown) = lua_script_markdown {
988 parent.child(markdown)
989 } else {
990 parent.child(Label::new(
991 "Failed to render script input to Markdown",
992 ))
993 }
994 }),
995 )
996 .map(|parent| match tool_use.status {
997 ToolUseStatus::Finished(output) => parent.child(
998 v_flex()
999 .gap_0p5()
1000 .py_1()
1001 .px_2p5()
1002 .child(Label::new("Result:"))
1003 .child(Label::new(output)),
1004 ),
1005 ToolUseStatus::Error(err) => parent.child(
1006 v_flex()
1007 .gap_0p5()
1008 .py_1()
1009 .px_2p5()
1010 .child(Label::new("Error:"))
1011 .child(Label::new(err)),
1012 ),
1013 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
1014 }),
1015 )
1016 }),
1017 )
1018 }
1019}
1020
1021impl Render for ActiveThread {
1022 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1023 v_flex()
1024 .size_full()
1025 .child(list(self.list_state.clone()).flex_grow())
1026 }
1027}