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::DoneStreaming => {}
 302            ThreadEvent::StreamedAssistantText(message_id, text) => {
 303                if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
 304                    markdown.update(cx, |markdown, cx| {
 305                        markdown.append(text, cx);
 306                    });
 307                }
 308            }
 309            ThreadEvent::MessageAdded(message_id) => {
 310                if let Some(message_text) = self
 311                    .thread
 312                    .read(cx)
 313                    .message(*message_id)
 314                    .map(|message| message.text.clone())
 315                {
 316                    self.push_message(message_id, message_text, window, cx);
 317                }
 318
 319                self.save_thread(cx);
 320                cx.notify();
 321            }
 322            ThreadEvent::MessageEdited(message_id) => {
 323                if let Some(message_text) = self
 324                    .thread
 325                    .read(cx)
 326                    .message(*message_id)
 327                    .map(|message| message.text.clone())
 328                {
 329                    self.edited_message(message_id, message_text, window, cx);
 330                }
 331
 332                self.save_thread(cx);
 333                cx.notify();
 334            }
 335            ThreadEvent::MessageDeleted(message_id) => {
 336                self.deleted_message(message_id);
 337                self.save_thread(cx);
 338                cx.notify();
 339            }
 340            ThreadEvent::UsePendingTools => {
 341                self.thread.update(cx, |thread, cx| {
 342                    thread.use_pending_tools(cx);
 343                });
 344            }
 345            ThreadEvent::ToolFinished {
 346                pending_tool_use, ..
 347            } => {
 348                if let Some(tool_use) = pending_tool_use {
 349                    self.render_scripting_tool_use_markdown(
 350                        tool_use.id.clone(),
 351                        tool_use.name.as_ref(),
 352                        tool_use.input.clone(),
 353                        window,
 354                        cx,
 355                    );
 356                }
 357
 358                if self.thread.read(cx).all_tools_finished() {
 359                    let pending_refresh_buffers = self.thread.update(cx, |thread, cx| {
 360                        thread.action_log().update(cx, |action_log, _cx| {
 361                            action_log.take_pending_refresh_buffers()
 362                        })
 363                    });
 364
 365                    let context_update_task = if !pending_refresh_buffers.is_empty() {
 366                        let refresh_task = refresh_context_store_text(
 367                            self.context_store.clone(),
 368                            &pending_refresh_buffers,
 369                            cx,
 370                        );
 371
 372                        cx.spawn(|this, mut cx| async move {
 373                            let updated_context_ids = refresh_task.await;
 374
 375                            this.update(&mut cx, |this, cx| {
 376                                this.context_store.read_with(cx, |context_store, cx| {
 377                                    context_store
 378                                        .context()
 379                                        .iter()
 380                                        .filter(|context| {
 381                                            updated_context_ids.contains(&context.id())
 382                                        })
 383                                        .flat_map(|context| context.snapshot(cx))
 384                                        .collect()
 385                                })
 386                            })
 387                        })
 388                    } else {
 389                        Task::ready(anyhow::Ok(Vec::new()))
 390                    };
 391
 392                    let model_registry = LanguageModelRegistry::read_global(cx);
 393                    if let Some(model) = model_registry.active_model() {
 394                        cx.spawn(|this, mut cx| async move {
 395                            let updated_context = context_update_task.await?;
 396
 397                            this.update(&mut cx, |this, cx| {
 398                                this.thread.update(cx, |thread, cx| {
 399                                    thread.send_tool_results_to_model(model, updated_context, cx);
 400                                });
 401                            })
 402                        })
 403                        .detach();
 404                    }
 405                }
 406            }
 407        }
 408    }
 409
 410    /// Spawns a task to save the active thread.
 411    ///
 412    /// Only one task to save the thread will be in flight at a time.
 413    fn save_thread(&mut self, cx: &mut Context<Self>) {
 414        let thread = self.thread.clone();
 415        self.save_thread_task = Some(cx.spawn(|this, mut cx| async move {
 416            let task = this
 417                .update(&mut cx, |this, cx| {
 418                    this.thread_store
 419                        .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
 420                })
 421                .ok();
 422
 423            if let Some(task) = task {
 424                task.await.log_err();
 425            }
 426        }));
 427    }
 428
 429    fn start_editing_message(
 430        &mut self,
 431        message_id: MessageId,
 432        message_text: String,
 433        window: &mut Window,
 434        cx: &mut Context<Self>,
 435    ) {
 436        let buffer = cx.new(|cx| {
 437            MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
 438        });
 439        let editor = cx.new(|cx| {
 440            let mut editor = Editor::new(
 441                editor::EditorMode::AutoHeight { max_lines: 8 },
 442                buffer,
 443                None,
 444                window,
 445                cx,
 446            );
 447            editor.focus_handle(cx).focus(window);
 448            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
 449            editor
 450        });
 451        self.editing_message = Some((
 452            message_id,
 453            EditMessageState {
 454                editor: editor.clone(),
 455            },
 456        ));
 457        cx.notify();
 458    }
 459
 460    fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 461        self.editing_message.take();
 462        cx.notify();
 463    }
 464
 465    fn confirm_editing_message(
 466        &mut self,
 467        _: &menu::Confirm,
 468        _: &mut Window,
 469        cx: &mut Context<Self>,
 470    ) {
 471        let Some((message_id, state)) = self.editing_message.take() else {
 472            return;
 473        };
 474        let edited_text = state.editor.read(cx).text(cx);
 475        self.thread.update(cx, |thread, cx| {
 476            thread.edit_message(message_id, Role::User, edited_text, cx);
 477            for message_id in self.messages_after(message_id) {
 478                thread.delete_message(*message_id, cx);
 479            }
 480        });
 481
 482        let provider = LanguageModelRegistry::read_global(cx).active_provider();
 483        if provider
 484            .as_ref()
 485            .map_or(false, |provider| provider.must_accept_terms(cx))
 486        {
 487            cx.notify();
 488            return;
 489        }
 490        let model_registry = LanguageModelRegistry::read_global(cx);
 491        let Some(model) = model_registry.active_model() else {
 492            return;
 493        };
 494
 495        self.thread.update(cx, |thread, cx| {
 496            thread.send_to_model(model, RequestKind::Chat, cx)
 497        });
 498        cx.notify();
 499    }
 500
 501    fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
 502        self.messages
 503            .iter()
 504            .rev()
 505            .find(|message_id| {
 506                self.thread
 507                    .read(cx)
 508                    .message(**message_id)
 509                    .map_or(false, |message| message.role == Role::User)
 510            })
 511            .cloned()
 512    }
 513
 514    fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
 515        self.messages
 516            .iter()
 517            .position(|id| *id == message_id)
 518            .map(|index| &self.messages[index + 1..])
 519            .unwrap_or(&[])
 520    }
 521
 522    fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 523        self.cancel_editing_message(&menu::Cancel, window, cx);
 524    }
 525
 526    fn handle_regenerate_click(
 527        &mut self,
 528        _: &ClickEvent,
 529        window: &mut Window,
 530        cx: &mut Context<Self>,
 531    ) {
 532        self.confirm_editing_message(&menu::Confirm, window, cx);
 533    }
 534
 535    fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 536        let message_id = self.messages[ix];
 537        let Some(message) = self.thread.read(cx).message(message_id) else {
 538            return Empty.into_any();
 539        };
 540
 541        let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
 542            return Empty.into_any();
 543        };
 544
 545        let thread = self.thread.read(cx);
 546        // Get all the data we need from thread before we start using it in closures
 547        let context = thread.context_for_message(message_id);
 548        let tool_uses = thread.tool_uses_for_message(message_id);
 549        let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
 550
 551        // Don't render user messages that are just there for returning tool results.
 552        if message.role == Role::User
 553            && (thread.message_has_tool_results(message_id)
 554                || thread.message_has_scripting_tool_results(message_id))
 555        {
 556            return Empty.into_any();
 557        }
 558
 559        let allow_editing_message =
 560            message.role == Role::User && self.last_user_message(cx) == Some(message_id);
 561
 562        let edit_message_editor = self
 563            .editing_message
 564            .as_ref()
 565            .filter(|(id, _)| *id == message_id)
 566            .map(|(_, state)| state.editor.clone());
 567
 568        let colors = cx.theme().colors();
 569
 570        let message_content = v_flex()
 571            .child(
 572                if let Some(edit_message_editor) = edit_message_editor.clone() {
 573                    div()
 574                        .key_context("EditMessageEditor")
 575                        .on_action(cx.listener(Self::cancel_editing_message))
 576                        .on_action(cx.listener(Self::confirm_editing_message))
 577                        .p_2p5()
 578                        .child(edit_message_editor)
 579                } else {
 580                    div().p_2p5().text_ui(cx).child(markdown.clone())
 581                },
 582            )
 583            .when_some(context, |parent, context| {
 584                if !context.is_empty() {
 585                    parent.child(
 586                        h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
 587                            context
 588                                .into_iter()
 589                                .map(|context| ContextPill::added(context, false, false, None)),
 590                        ),
 591                    )
 592                } else {
 593                    parent
 594                }
 595            });
 596
 597        let styled_message = match message.role {
 598            Role::User => v_flex()
 599                .id(("message-container", ix))
 600                .pt_2p5()
 601                .px_2p5()
 602                .child(
 603                    v_flex()
 604                        .bg(colors.editor_background)
 605                        .rounded_lg()
 606                        .border_1()
 607                        .border_color(colors.border)
 608                        .shadow_sm()
 609                        .child(
 610                            h_flex()
 611                                .py_1()
 612                                .pl_2()
 613                                .pr_1()
 614                                .bg(colors.editor_foreground.opacity(0.05))
 615                                .border_b_1()
 616                                .border_color(colors.border)
 617                                .justify_between()
 618                                .rounded_t(px(6.))
 619                                .child(
 620                                    h_flex()
 621                                        .gap_1p5()
 622                                        .child(
 623                                            Icon::new(IconName::PersonCircle)
 624                                                .size(IconSize::XSmall)
 625                                                .color(Color::Muted),
 626                                        )
 627                                        .child(
 628                                            Label::new("You")
 629                                                .size(LabelSize::Small)
 630                                                .color(Color::Muted),
 631                                        ),
 632                                )
 633                                .when_some(
 634                                    edit_message_editor.clone(),
 635                                    |this, edit_message_editor| {
 636                                        let focus_handle = edit_message_editor.focus_handle(cx);
 637                                        this.child(
 638                                            h_flex()
 639                                                .gap_1()
 640                                                .child(
 641                                                    Button::new("cancel-edit-message", "Cancel")
 642                                                        .label_size(LabelSize::Small)
 643                                                        .key_binding(
 644                                                            KeyBinding::for_action_in(
 645                                                                &menu::Cancel,
 646                                                                &focus_handle,
 647                                                                window,
 648                                                                cx,
 649                                                            )
 650                                                            .map(|kb| kb.size(rems_from_px(12.))),
 651                                                        )
 652                                                        .on_click(
 653                                                            cx.listener(Self::handle_cancel_click),
 654                                                        ),
 655                                                )
 656                                                .child(
 657                                                    Button::new(
 658                                                        "confirm-edit-message",
 659                                                        "Regenerate",
 660                                                    )
 661                                                    .label_size(LabelSize::Small)
 662                                                    .key_binding(
 663                                                        KeyBinding::for_action_in(
 664                                                            &menu::Confirm,
 665                                                            &focus_handle,
 666                                                            window,
 667                                                            cx,
 668                                                        )
 669                                                        .map(|kb| kb.size(rems_from_px(12.))),
 670                                                    )
 671                                                    .on_click(
 672                                                        cx.listener(Self::handle_regenerate_click),
 673                                                    ),
 674                                                ),
 675                                        )
 676                                    },
 677                                )
 678                                .when(
 679                                    edit_message_editor.is_none() && allow_editing_message,
 680                                    |this| {
 681                                        this.child(
 682                                            Button::new("edit-message", "Edit")
 683                                                .label_size(LabelSize::Small)
 684                                                .on_click(cx.listener({
 685                                                    let message_text = message.text.clone();
 686                                                    move |this, _, window, cx| {
 687                                                        this.start_editing_message(
 688                                                            message_id,
 689                                                            message_text.clone(),
 690                                                            window,
 691                                                            cx,
 692                                                        );
 693                                                    }
 694                                                })),
 695                                        )
 696                                    },
 697                                ),
 698                        )
 699                        .child(message_content),
 700                ),
 701            Role::Assistant => {
 702                v_flex()
 703                    .id(("message-container", ix))
 704                    .child(message_content)
 705                    .when(
 706                        !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
 707                        |parent| {
 708                            parent.child(
 709                                v_flex()
 710                                    .children(
 711                                        tool_uses
 712                                            .into_iter()
 713                                            .map(|tool_use| self.render_tool_use(tool_use, cx)),
 714                                    )
 715                                    .children(scripting_tool_uses.into_iter().map(|tool_use| {
 716                                        self.render_scripting_tool_use(tool_use, cx)
 717                                    })),
 718                            )
 719                        },
 720                    )
 721            }
 722            Role::System => div().id(("message-container", ix)).py_1().px_2().child(
 723                v_flex()
 724                    .bg(colors.editor_background)
 725                    .rounded_sm()
 726                    .child(message_content),
 727            ),
 728        };
 729
 730        styled_message.into_any()
 731    }
 732
 733    fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
 734        let is_open = self
 735            .expanded_tool_uses
 736            .get(&tool_use.id)
 737            .copied()
 738            .unwrap_or_default();
 739
 740        let lighter_border = cx.theme().colors().border.opacity(0.5);
 741
 742        div().px_2p5().child(
 743            v_flex()
 744                .rounded_lg()
 745                .border_1()
 746                .border_color(lighter_border)
 747                .child(
 748                    h_flex()
 749                        .justify_between()
 750                        .py_1()
 751                        .pl_1()
 752                        .pr_2()
 753                        .bg(cx.theme().colors().editor_foreground.opacity(0.025))
 754                        .map(|element| {
 755                            if is_open {
 756                                element.border_b_1().rounded_t_md()
 757                            } else {
 758                                element.rounded_md()
 759                            }
 760                        })
 761                        .border_color(lighter_border)
 762                        .child(
 763                            h_flex()
 764                                .gap_1()
 765                                .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
 766                                    cx.listener({
 767                                        let tool_use_id = tool_use.id.clone();
 768                                        move |this, _event, _window, _cx| {
 769                                            let is_open = this
 770                                                .expanded_tool_uses
 771                                                .entry(tool_use_id.clone())
 772                                                .or_insert(false);
 773
 774                                            *is_open = !*is_open;
 775                                        }
 776                                    }),
 777                                ))
 778                                .child(
 779                                    Label::new(tool_use.name)
 780                                        .size(LabelSize::Small)
 781                                        .buffer_font(cx),
 782                                ),
 783                        )
 784                        .child({
 785                            let (icon_name, color, animated) = match &tool_use.status {
 786                                ToolUseStatus::Pending => {
 787                                    (IconName::Warning, Color::Warning, false)
 788                                }
 789                                ToolUseStatus::Running => {
 790                                    (IconName::ArrowCircle, Color::Accent, true)
 791                                }
 792                                ToolUseStatus::Finished(_) => {
 793                                    (IconName::Check, Color::Success, false)
 794                                }
 795                                ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
 796                            };
 797
 798                            let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
 799
 800                            if animated {
 801                                icon.with_animation(
 802                                    "arrow-circle",
 803                                    Animation::new(Duration::from_secs(2)).repeat(),
 804                                    |icon, delta| {
 805                                        icon.transform(Transformation::rotate(percentage(delta)))
 806                                    },
 807                                )
 808                                .into_any_element()
 809                            } else {
 810                                icon.into_any_element()
 811                            }
 812                        }),
 813                )
 814                .map(|parent| {
 815                    if !is_open {
 816                        return parent;
 817                    }
 818
 819                    let content_container = || v_flex().py_1().gap_0p5().px_2p5();
 820
 821                    parent.child(
 822                        v_flex()
 823                            .gap_1()
 824                            .bg(cx.theme().colors().editor_background)
 825                            .rounded_b_lg()
 826                            .child(
 827                                content_container()
 828                                    .border_b_1()
 829                                    .border_color(lighter_border)
 830                                    .child(
 831                                        Label::new("Input")
 832                                            .size(LabelSize::XSmall)
 833                                            .color(Color::Muted)
 834                                            .buffer_font(cx),
 835                                    )
 836                                    .child(
 837                                        Label::new(
 838                                            serde_json::to_string_pretty(&tool_use.input)
 839                                                .unwrap_or_default(),
 840                                        )
 841                                        .size(LabelSize::Small)
 842                                        .buffer_font(cx),
 843                                    ),
 844                            )
 845                            .map(|container| match tool_use.status {
 846                                ToolUseStatus::Finished(output) => container.child(
 847                                    content_container()
 848                                        .child(
 849                                            Label::new("Result")
 850                                                .size(LabelSize::XSmall)
 851                                                .color(Color::Muted)
 852                                                .buffer_font(cx),
 853                                        )
 854                                        .child(
 855                                            Label::new(output)
 856                                                .size(LabelSize::Small)
 857                                                .buffer_font(cx),
 858                                        ),
 859                                ),
 860                                ToolUseStatus::Running => container.child(
 861                                    content_container().child(
 862                                        h_flex()
 863                                            .gap_1()
 864                                            .pb_1()
 865                                            .child(
 866                                                Icon::new(IconName::ArrowCircle)
 867                                                    .size(IconSize::Small)
 868                                                    .color(Color::Accent)
 869                                                    .with_animation(
 870                                                        "arrow-circle",
 871                                                        Animation::new(Duration::from_secs(2))
 872                                                            .repeat(),
 873                                                        |icon, delta| {
 874                                                            icon.transform(Transformation::rotate(
 875                                                                percentage(delta),
 876                                                            ))
 877                                                        },
 878                                                    ),
 879                                            )
 880                                            .child(
 881                                                Label::new("Running…")
 882                                                    .size(LabelSize::XSmall)
 883                                                    .color(Color::Muted)
 884                                                    .buffer_font(cx),
 885                                            ),
 886                                    ),
 887                                ),
 888                                ToolUseStatus::Error(err) => container.child(
 889                                    content_container()
 890                                        .child(
 891                                            Label::new("Error")
 892                                                .size(LabelSize::XSmall)
 893                                                .color(Color::Muted)
 894                                                .buffer_font(cx),
 895                                        )
 896                                        .child(
 897                                            Label::new(err).size(LabelSize::Small).buffer_font(cx),
 898                                        ),
 899                                ),
 900                                ToolUseStatus::Pending => container,
 901                            }),
 902                    )
 903                }),
 904        )
 905    }
 906
 907    fn render_scripting_tool_use(
 908        &self,
 909        tool_use: ToolUse,
 910        cx: &mut Context<Self>,
 911    ) -> impl IntoElement {
 912        let is_open = self
 913            .expanded_tool_uses
 914            .get(&tool_use.id)
 915            .copied()
 916            .unwrap_or_default();
 917
 918        div().px_2p5().child(
 919            v_flex()
 920                .gap_1()
 921                .rounded_lg()
 922                .border_1()
 923                .border_color(cx.theme().colors().border)
 924                .child(
 925                    h_flex()
 926                        .justify_between()
 927                        .py_0p5()
 928                        .pl_1()
 929                        .pr_2()
 930                        .bg(cx.theme().colors().editor_foreground.opacity(0.02))
 931                        .map(|element| {
 932                            if is_open {
 933                                element.border_b_1().rounded_t_md()
 934                            } else {
 935                                element.rounded_md()
 936                            }
 937                        })
 938                        .border_color(cx.theme().colors().border)
 939                        .child(
 940                            h_flex()
 941                                .gap_1()
 942                                .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
 943                                    cx.listener({
 944                                        let tool_use_id = tool_use.id.clone();
 945                                        move |this, _event, _window, _cx| {
 946                                            let is_open = this
 947                                                .expanded_tool_uses
 948                                                .entry(tool_use_id.clone())
 949                                                .or_insert(false);
 950
 951                                            *is_open = !*is_open;
 952                                        }
 953                                    }),
 954                                ))
 955                                .child(Label::new(tool_use.name)),
 956                        )
 957                        .child(
 958                            Label::new(match tool_use.status {
 959                                ToolUseStatus::Pending => "Pending",
 960                                ToolUseStatus::Running => "Running",
 961                                ToolUseStatus::Finished(_) => "Finished",
 962                                ToolUseStatus::Error(_) => "Error",
 963                            })
 964                            .size(LabelSize::XSmall)
 965                            .buffer_font(cx),
 966                        ),
 967                )
 968                .map(|parent| {
 969                    if !is_open {
 970                        return parent;
 971                    }
 972
 973                    let lua_script_markdown =
 974                        self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
 975
 976                    parent.child(
 977                        v_flex()
 978                            .child(
 979                                v_flex()
 980                                    .gap_0p5()
 981                                    .py_1()
 982                                    .px_2p5()
 983                                    .border_b_1()
 984                                    .border_color(cx.theme().colors().border)
 985                                    .child(Label::new("Input:"))
 986                                    .map(|parent| {
 987                                        if let Some(markdown) = lua_script_markdown {
 988                                            parent.child(markdown)
 989                                        } else {
 990                                            parent.child(Label::new(
 991                                                "Failed to render script input to Markdown",
 992                                            ))
 993                                        }
 994                                    }),
 995                            )
 996                            .map(|parent| match tool_use.status {
 997                                ToolUseStatus::Finished(output) => parent.child(
 998                                    v_flex()
 999                                        .gap_0p5()
1000                                        .py_1()
1001                                        .px_2p5()
1002                                        .child(Label::new("Result:"))
1003                                        .child(Label::new(output)),
1004                                ),
1005                                ToolUseStatus::Error(err) => parent.child(
1006                                    v_flex()
1007                                        .gap_0p5()
1008                                        .py_1()
1009                                        .px_2p5()
1010                                        .child(Label::new("Error:"))
1011                                        .child(Label::new(err)),
1012                                ),
1013                                ToolUseStatus::Pending | ToolUseStatus::Running => parent,
1014                            }),
1015                    )
1016                }),
1017        )
1018    }
1019}
1020
1021impl Render for ActiveThread {
1022    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1023        v_flex()
1024            .size_full()
1025            .child(list(self.list_state.clone()).flex_grow())
1026    }
1027}