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