active_thread.rs

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