active_thread.rs

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