active_thread.rs

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