active_thread.rs

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