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