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