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