active_thread.rs

   1use crate::thread::{
   2    LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
   3    ThreadEvent, ThreadFeedback,
   4};
   5use crate::thread_store::ThreadStore;
   6use crate::tool_use::{ToolUse, ToolUseStatus};
   7use crate::ui::ContextPill;
   8use collections::HashMap;
   9use editor::{Editor, MultiBuffer};
  10use gpui::{
  11    linear_color_stop, linear_gradient, list, percentage, pulsating_between, AbsoluteLength,
  12    Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
  13    Entity, Focusable, Length, ListAlignment, ListOffset, ListState, ScrollHandle, StyleRefinement,
  14    Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
  15};
  16use language::{Buffer, LanguageRegistry};
  17use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
  18use markdown::{Markdown, MarkdownStyle};
  19use scripting_tool::{ScriptingTool, ScriptingToolInput};
  20use settings::Settings as _;
  21use std::sync::Arc;
  22use std::time::Duration;
  23use theme::ThemeSettings;
  24use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Tooltip};
  25use util::ResultExt as _;
  26use workspace::{OpenOptions, Workspace};
  27
  28use crate::context_store::{refresh_context_store_text, ContextStore};
  29
  30pub struct ActiveThread {
  31    language_registry: Arc<LanguageRegistry>,
  32    thread_store: Entity<ThreadStore>,
  33    thread: Entity<Thread>,
  34    context_store: Entity<ContextStore>,
  35    workspace: WeakEntity<Workspace>,
  36    save_thread_task: Option<Task<()>>,
  37    messages: Vec<MessageId>,
  38    list_state: ListState,
  39    rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
  40    rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
  41    rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
  42    editing_message: Option<(MessageId, EditMessageState)>,
  43    expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
  44    expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
  45    last_error: Option<ThreadError>,
  46    _subscriptions: Vec<Subscription>,
  47}
  48
  49struct RenderedMessage {
  50    language_registry: Arc<LanguageRegistry>,
  51    segments: Vec<RenderedMessageSegment>,
  52}
  53
  54impl RenderedMessage {
  55    fn from_segments(
  56        segments: &[MessageSegment],
  57        language_registry: Arc<LanguageRegistry>,
  58        window: &Window,
  59        cx: &mut App,
  60    ) -> Self {
  61        let mut this = Self {
  62            language_registry,
  63            segments: Vec::with_capacity(segments.len()),
  64        };
  65        for segment in segments {
  66            this.push_segment(segment, window, cx);
  67        }
  68        this
  69    }
  70
  71    fn append_thinking(&mut self, text: &String, window: &Window, cx: &mut App) {
  72        if let Some(RenderedMessageSegment::Thinking {
  73            content,
  74            scroll_handle,
  75        }) = self.segments.last_mut()
  76        {
  77            content.update(cx, |markdown, cx| {
  78                markdown.append(text, cx);
  79            });
  80            scroll_handle.scroll_to_bottom();
  81        } else {
  82            self.segments.push(RenderedMessageSegment::Thinking {
  83                content: render_markdown(text.into(), self.language_registry.clone(), window, cx),
  84                scroll_handle: ScrollHandle::default(),
  85            });
  86        }
  87    }
  88
  89    fn append_text(&mut self, text: &String, window: &Window, cx: &mut App) {
  90        if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {
  91            markdown.update(cx, |markdown, cx| markdown.append(text, cx));
  92        } else {
  93            self.segments
  94                .push(RenderedMessageSegment::Text(render_markdown(
  95                    SharedString::from(text),
  96                    self.language_registry.clone(),
  97                    window,
  98                    cx,
  99                )));
 100        }
 101    }
 102
 103    fn push_segment(&mut self, segment: &MessageSegment, window: &Window, cx: &mut App) {
 104        let rendered_segment = match segment {
 105            MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
 106                content: render_markdown(text.into(), self.language_registry.clone(), window, cx),
 107                scroll_handle: ScrollHandle::default(),
 108            },
 109            MessageSegment::Text(text) => RenderedMessageSegment::Text(render_markdown(
 110                text.into(),
 111                self.language_registry.clone(),
 112                window,
 113                cx,
 114            )),
 115        };
 116        self.segments.push(rendered_segment);
 117    }
 118}
 119
 120enum RenderedMessageSegment {
 121    Thinking {
 122        content: Entity<Markdown>,
 123        scroll_handle: ScrollHandle,
 124    },
 125    Text(Entity<Markdown>),
 126}
 127
 128fn render_markdown(
 129    text: SharedString,
 130    language_registry: Arc<LanguageRegistry>,
 131    window: &Window,
 132    cx: &mut App,
 133) -> Entity<Markdown> {
 134    let theme_settings = ThemeSettings::get_global(cx);
 135    let colors = cx.theme().colors();
 136    let ui_font_size = TextSize::Default.rems(cx);
 137    let buffer_font_size = TextSize::Small.rems(cx);
 138    let mut text_style = window.text_style();
 139
 140    text_style.refine(&TextStyleRefinement {
 141        font_family: Some(theme_settings.ui_font.family.clone()),
 142        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
 143        font_features: Some(theme_settings.ui_font.features.clone()),
 144        font_size: Some(ui_font_size.into()),
 145        color: Some(cx.theme().colors().text),
 146        ..Default::default()
 147    });
 148
 149    let markdown_style = MarkdownStyle {
 150        base_text_style: text_style,
 151        syntax: cx.theme().syntax().clone(),
 152        selection_background_color: cx.theme().players().local().selection,
 153        code_block_overflow_x_scroll: true,
 154        table_overflow_x_scroll: true,
 155        code_block: StyleRefinement {
 156            margin: EdgesRefinement {
 157                top: Some(Length::Definite(rems(0.).into())),
 158                left: Some(Length::Definite(rems(0.).into())),
 159                right: Some(Length::Definite(rems(0.).into())),
 160                bottom: Some(Length::Definite(rems(0.5).into())),
 161            },
 162            padding: EdgesRefinement {
 163                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 164                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 165                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 166                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 167            },
 168            background: Some(colors.editor_background.into()),
 169            border_color: Some(colors.border_variant),
 170            border_widths: EdgesRefinement {
 171                top: Some(AbsoluteLength::Pixels(Pixels(1.))),
 172                left: Some(AbsoluteLength::Pixels(Pixels(1.))),
 173                right: Some(AbsoluteLength::Pixels(Pixels(1.))),
 174                bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
 175            },
 176            text: Some(TextStyleRefinement {
 177                font_family: Some(theme_settings.buffer_font.family.clone()),
 178                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 179                font_features: Some(theme_settings.buffer_font.features.clone()),
 180                font_size: Some(buffer_font_size.into()),
 181                ..Default::default()
 182            }),
 183            ..Default::default()
 184        },
 185        inline_code: TextStyleRefinement {
 186            font_family: Some(theme_settings.buffer_font.family.clone()),
 187            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 188            font_features: Some(theme_settings.buffer_font.features.clone()),
 189            font_size: Some(buffer_font_size.into()),
 190            background_color: Some(colors.editor_foreground.opacity(0.1)),
 191            ..Default::default()
 192        },
 193        link: TextStyleRefinement {
 194            background_color: Some(colors.editor_foreground.opacity(0.025)),
 195            underline: Some(UnderlineStyle {
 196                color: Some(colors.text_accent.opacity(0.5)),
 197                thickness: px(1.),
 198                ..Default::default()
 199            }),
 200            ..Default::default()
 201        },
 202        ..Default::default()
 203    };
 204
 205    cx.new(|cx| Markdown::new(text, markdown_style, Some(language_registry), None, cx))
 206}
 207
 208struct EditMessageState {
 209    editor: Entity<Editor>,
 210}
 211
 212impl ActiveThread {
 213    pub fn new(
 214        thread: Entity<Thread>,
 215        thread_store: Entity<ThreadStore>,
 216        language_registry: Arc<LanguageRegistry>,
 217        context_store: Entity<ContextStore>,
 218        workspace: WeakEntity<Workspace>,
 219        window: &mut Window,
 220        cx: &mut Context<Self>,
 221    ) -> Self {
 222        let subscriptions = vec![
 223            cx.observe(&thread, |_, _, cx| cx.notify()),
 224            cx.subscribe_in(&thread, window, Self::handle_thread_event),
 225        ];
 226
 227        let mut this = Self {
 228            language_registry,
 229            thread_store,
 230            thread: thread.clone(),
 231            context_store,
 232            workspace,
 233            save_thread_task: None,
 234            messages: Vec::new(),
 235            rendered_messages_by_id: HashMap::default(),
 236            rendered_scripting_tool_uses: HashMap::default(),
 237            rendered_tool_use_labels: HashMap::default(),
 238            expanded_tool_uses: HashMap::default(),
 239            expanded_thinking_segments: HashMap::default(),
 240            list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
 241                let this = cx.entity().downgrade();
 242                move |ix, window: &mut Window, cx: &mut App| {
 243                    this.update(cx, |this, cx| this.render_message(ix, window, cx))
 244                        .unwrap()
 245                }
 246            }),
 247            editing_message: None,
 248            last_error: None,
 249            _subscriptions: subscriptions,
 250        };
 251
 252        for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
 253            this.push_message(&message.id, &message.segments, window, cx);
 254
 255            for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
 256                this.render_tool_use_label_markdown(
 257                    tool_use.id.clone(),
 258                    tool_use.ui_text.clone(),
 259                    window,
 260                    cx,
 261                );
 262            }
 263
 264            for tool_use in thread
 265                .read(cx)
 266                .scripting_tool_uses_for_message(message.id, cx)
 267            {
 268                this.render_tool_use_label_markdown(
 269                    tool_use.id.clone(),
 270                    tool_use.ui_text.clone(),
 271                    window,
 272                    cx,
 273                );
 274
 275                this.render_scripting_tool_use_markdown(
 276                    tool_use.id.clone(),
 277                    tool_use.ui_text.as_ref(),
 278                    tool_use.input.clone(),
 279                    window,
 280                    cx,
 281                );
 282            }
 283        }
 284
 285        this
 286    }
 287
 288    pub fn thread(&self) -> &Entity<Thread> {
 289        &self.thread
 290    }
 291
 292    pub fn is_empty(&self) -> bool {
 293        self.messages.is_empty()
 294    }
 295
 296    pub fn summary(&self, cx: &App) -> Option<SharedString> {
 297        self.thread.read(cx).summary()
 298    }
 299
 300    pub fn summary_or_default(&self, cx: &App) -> SharedString {
 301        self.thread.read(cx).summary_or_default()
 302    }
 303
 304    pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
 305        self.last_error.take();
 306        self.thread
 307            .update(cx, |thread, cx| thread.cancel_last_completion(cx))
 308    }
 309
 310    pub fn last_error(&self) -> Option<ThreadError> {
 311        self.last_error.clone()
 312    }
 313
 314    pub fn clear_last_error(&mut self) {
 315        self.last_error.take();
 316    }
 317
 318    fn push_message(
 319        &mut self,
 320        id: &MessageId,
 321        segments: &[MessageSegment],
 322        window: &mut Window,
 323        cx: &mut Context<Self>,
 324    ) {
 325        let old_len = self.messages.len();
 326        self.messages.push(*id);
 327        self.list_state.splice(old_len..old_len, 1);
 328
 329        let rendered_message =
 330            RenderedMessage::from_segments(segments, self.language_registry.clone(), window, cx);
 331        self.rendered_messages_by_id.insert(*id, rendered_message);
 332        self.list_state.scroll_to(ListOffset {
 333            item_ix: old_len,
 334            offset_in_item: Pixels(0.0),
 335        });
 336    }
 337
 338    fn edited_message(
 339        &mut self,
 340        id: &MessageId,
 341        segments: &[MessageSegment],
 342        window: &mut Window,
 343        cx: &mut Context<Self>,
 344    ) {
 345        let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
 346            return;
 347        };
 348        self.list_state.splice(index..index + 1, 1);
 349        let rendered_message =
 350            RenderedMessage::from_segments(segments, self.language_registry.clone(), window, cx);
 351        self.rendered_messages_by_id.insert(*id, rendered_message);
 352    }
 353
 354    fn deleted_message(&mut self, id: &MessageId) {
 355        let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
 356            return;
 357        };
 358        self.messages.remove(index);
 359        self.list_state.splice(index..index + 1, 0);
 360        self.rendered_messages_by_id.remove(id);
 361    }
 362
 363    /// Renders the input of a scripting tool use to Markdown.
 364    ///
 365    /// Does nothing if the tool use does not correspond to the scripting tool.
 366    fn render_scripting_tool_use_markdown(
 367        &mut self,
 368        tool_use_id: LanguageModelToolUseId,
 369        tool_name: &str,
 370        tool_input: serde_json::Value,
 371        window: &mut Window,
 372        cx: &mut Context<Self>,
 373    ) {
 374        if tool_name != ScriptingTool::NAME {
 375            return;
 376        }
 377
 378        let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
 379            .map(|input| input.lua_script)
 380            .unwrap_or_default();
 381
 382        let lua_script = render_markdown(
 383            format!("```lua\n{lua_script}\n```").into(),
 384            self.language_registry.clone(),
 385            window,
 386            cx,
 387        );
 388
 389        self.rendered_scripting_tool_uses
 390            .insert(tool_use_id, lua_script);
 391    }
 392
 393    fn render_tool_use_label_markdown(
 394        &mut self,
 395        tool_use_id: LanguageModelToolUseId,
 396        tool_label: impl Into<SharedString>,
 397        window: &mut Window,
 398        cx: &mut Context<Self>,
 399    ) {
 400        self.rendered_tool_use_labels.insert(
 401            tool_use_id,
 402            render_markdown(
 403                tool_label.into(),
 404                self.language_registry.clone(),
 405                window,
 406                cx,
 407            ),
 408        );
 409    }
 410
 411    fn handle_thread_event(
 412        &mut self,
 413        _thread: &Entity<Thread>,
 414        event: &ThreadEvent,
 415        window: &mut Window,
 416        cx: &mut Context<Self>,
 417    ) {
 418        match event {
 419            ThreadEvent::ShowError(error) => {
 420                self.last_error = Some(error.clone());
 421            }
 422            ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
 423                self.save_thread(cx);
 424            }
 425            ThreadEvent::DoneStreaming => {}
 426            ThreadEvent::StreamedAssistantText(message_id, text) => {
 427                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
 428                    rendered_message.append_text(text, window, cx);
 429                }
 430            }
 431            ThreadEvent::StreamedAssistantThinking(message_id, text) => {
 432                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
 433                    rendered_message.append_thinking(text, window, cx);
 434                }
 435            }
 436            ThreadEvent::MessageAdded(message_id) => {
 437                if let Some(message_segments) = self
 438                    .thread
 439                    .read(cx)
 440                    .message(*message_id)
 441                    .map(|message| message.segments.clone())
 442                {
 443                    self.push_message(message_id, &message_segments, window, cx);
 444                }
 445
 446                self.save_thread(cx);
 447                cx.notify();
 448            }
 449            ThreadEvent::MessageEdited(message_id) => {
 450                if let Some(message_segments) = self
 451                    .thread
 452                    .read(cx)
 453                    .message(*message_id)
 454                    .map(|message| message.segments.clone())
 455                {
 456                    self.edited_message(message_id, &message_segments, window, cx);
 457                }
 458
 459                self.save_thread(cx);
 460                cx.notify();
 461            }
 462            ThreadEvent::MessageDeleted(message_id) => {
 463                self.deleted_message(message_id);
 464                self.save_thread(cx);
 465                cx.notify();
 466            }
 467            ThreadEvent::UsePendingTools => {
 468                let tool_uses = self
 469                    .thread
 470                    .update(cx, |thread, cx| thread.use_pending_tools(cx));
 471
 472                for tool_use in tool_uses {
 473                    self.render_tool_use_label_markdown(
 474                        tool_use.id,
 475                        tool_use.ui_text.clone(),
 476                        window,
 477                        cx,
 478                    );
 479                }
 480            }
 481            ThreadEvent::ToolFinished {
 482                pending_tool_use,
 483                canceled,
 484                ..
 485            } => {
 486                let canceled = *canceled;
 487                if let Some(tool_use) = pending_tool_use {
 488                    self.render_tool_use_label_markdown(
 489                        tool_use.id.clone(),
 490                        SharedString::from(tool_use.ui_text.clone()),
 491                        window,
 492                        cx,
 493                    );
 494                    self.render_scripting_tool_use_markdown(
 495                        tool_use.id.clone(),
 496                        tool_use.name.as_ref(),
 497                        tool_use.input.clone(),
 498                        window,
 499                        cx,
 500                    );
 501                }
 502
 503                if self.thread.read(cx).all_tools_finished() {
 504                    let pending_refresh_buffers = self.thread.update(cx, |thread, cx| {
 505                        thread.action_log().update(cx, |action_log, _cx| {
 506                            action_log.take_stale_buffers_in_context()
 507                        })
 508                    });
 509
 510                    let context_update_task = if !pending_refresh_buffers.is_empty() {
 511                        let refresh_task = refresh_context_store_text(
 512                            self.context_store.clone(),
 513                            &pending_refresh_buffers,
 514                            cx,
 515                        );
 516
 517                        cx.spawn(async move |this, cx| {
 518                            let updated_context_ids = refresh_task.await;
 519
 520                            this.update(cx, |this, cx| {
 521                                this.context_store.read_with(cx, |context_store, cx| {
 522                                    context_store
 523                                        .context()
 524                                        .iter()
 525                                        .filter(|context| {
 526                                            updated_context_ids.contains(&context.id())
 527                                        })
 528                                        .flat_map(|context| context.snapshot(cx))
 529                                        .collect()
 530                                })
 531                            })
 532                        })
 533                    } else {
 534                        Task::ready(anyhow::Ok(Vec::new()))
 535                    };
 536
 537                    let model_registry = LanguageModelRegistry::read_global(cx);
 538                    if let Some(model) = model_registry.active_model() {
 539                        cx.spawn(async move |this, cx| {
 540                            let updated_context = context_update_task.await?;
 541
 542                            this.update(cx, |this, cx| {
 543                                this.thread.update(cx, |thread, cx| {
 544                                    thread.attach_tool_results(updated_context, cx);
 545                                    if !canceled {
 546                                        thread.send_to_model(model, RequestKind::Chat, cx);
 547                                    }
 548                                });
 549                            })
 550                        })
 551                        .detach();
 552                    }
 553                }
 554            }
 555            ThreadEvent::CheckpointChanged => cx.notify(),
 556        }
 557    }
 558
 559    /// Spawns a task to save the active thread.
 560    ///
 561    /// Only one task to save the thread will be in flight at a time.
 562    fn save_thread(&mut self, cx: &mut Context<Self>) {
 563        let thread = self.thread.clone();
 564        self.save_thread_task = Some(cx.spawn(async move |this, cx| {
 565            let task = this
 566                .update(cx, |this, cx| {
 567                    this.thread_store
 568                        .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
 569                })
 570                .ok();
 571
 572            if let Some(task) = task {
 573                task.await.log_err();
 574            }
 575        }));
 576    }
 577
 578    fn start_editing_message(
 579        &mut self,
 580        message_id: MessageId,
 581        message_segments: &[MessageSegment],
 582        window: &mut Window,
 583        cx: &mut Context<Self>,
 584    ) {
 585        // User message should always consist of a single text segment,
 586        // therefore we can skip returning early if it's not a text segment.
 587        let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
 588            return;
 589        };
 590
 591        let buffer = cx.new(|cx| {
 592            MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
 593        });
 594        let editor = cx.new(|cx| {
 595            let mut editor = Editor::new(
 596                editor::EditorMode::AutoHeight { max_lines: 8 },
 597                buffer,
 598                None,
 599                window,
 600                cx,
 601            );
 602            editor.focus_handle(cx).focus(window);
 603            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
 604            editor
 605        });
 606        self.editing_message = Some((
 607            message_id,
 608            EditMessageState {
 609                editor: editor.clone(),
 610            },
 611        ));
 612        cx.notify();
 613    }
 614
 615    fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 616        self.editing_message.take();
 617        cx.notify();
 618    }
 619
 620    fn confirm_editing_message(
 621        &mut self,
 622        _: &menu::Confirm,
 623        _: &mut Window,
 624        cx: &mut Context<Self>,
 625    ) {
 626        let Some((message_id, state)) = self.editing_message.take() else {
 627            return;
 628        };
 629        let edited_text = state.editor.read(cx).text(cx);
 630        self.thread.update(cx, |thread, cx| {
 631            thread.edit_message(
 632                message_id,
 633                Role::User,
 634                vec![MessageSegment::Text(edited_text)],
 635                cx,
 636            );
 637            for message_id in self.messages_after(message_id) {
 638                thread.delete_message(*message_id, cx);
 639            }
 640        });
 641
 642        let provider = LanguageModelRegistry::read_global(cx).active_provider();
 643        if provider
 644            .as_ref()
 645            .map_or(false, |provider| provider.must_accept_terms(cx))
 646        {
 647            cx.notify();
 648            return;
 649        }
 650        let model_registry = LanguageModelRegistry::read_global(cx);
 651        let Some(model) = model_registry.active_model() else {
 652            return;
 653        };
 654
 655        self.thread.update(cx, |thread, cx| {
 656            thread.send_to_model(model, RequestKind::Chat, cx)
 657        });
 658        cx.notify();
 659    }
 660
 661    fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
 662        self.messages
 663            .iter()
 664            .rev()
 665            .find(|message_id| {
 666                self.thread
 667                    .read(cx)
 668                    .message(**message_id)
 669                    .map_or(false, |message| message.role == Role::User)
 670            })
 671            .cloned()
 672    }
 673
 674    fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
 675        self.messages
 676            .iter()
 677            .position(|id| *id == message_id)
 678            .map(|index| &self.messages[index + 1..])
 679            .unwrap_or(&[])
 680    }
 681
 682    fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 683        self.cancel_editing_message(&menu::Cancel, window, cx);
 684    }
 685
 686    fn handle_regenerate_click(
 687        &mut self,
 688        _: &ClickEvent,
 689        window: &mut Window,
 690        cx: &mut Context<Self>,
 691    ) {
 692        self.confirm_editing_message(&menu::Confirm, window, cx);
 693    }
 694
 695    fn handle_feedback_click(
 696        &mut self,
 697        feedback: ThreadFeedback,
 698        _window: &mut Window,
 699        cx: &mut Context<Self>,
 700    ) {
 701        let report = self
 702            .thread
 703            .update(cx, |thread, cx| thread.report_feedback(feedback, cx));
 704
 705        let this = cx.entity().downgrade();
 706        cx.spawn(async move |_, cx| {
 707            report.await?;
 708            this.update(cx, |_this, cx| cx.notify())
 709        })
 710        .detach_and_log_err(cx);
 711    }
 712
 713    fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 714        let message_id = self.messages[ix];
 715        let Some(message) = self.thread.read(cx).message(message_id) else {
 716            return Empty.into_any();
 717        };
 718
 719        let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
 720            return Empty.into_any();
 721        };
 722
 723        let thread = self.thread.read(cx);
 724        // Get all the data we need from thread before we start using it in closures
 725        let checkpoint = thread.checkpoint_for_message(message_id);
 726        let context = thread.context_for_message(message_id);
 727        let tool_uses = thread.tool_uses_for_message(message_id, cx);
 728        let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id, cx);
 729
 730        // Don't render user messages that are just there for returning tool results.
 731        if message.role == Role::User
 732            && (thread.message_has_tool_results(message_id)
 733                || thread.message_has_scripting_tool_results(message_id))
 734        {
 735            return Empty.into_any();
 736        }
 737
 738        let allow_editing_message =
 739            message.role == Role::User && self.last_user_message(cx) == Some(message_id);
 740
 741        let edit_message_editor = self
 742            .editing_message
 743            .as_ref()
 744            .filter(|(id, _)| *id == message_id)
 745            .map(|(_, state)| state.editor.clone());
 746
 747        let first_message = ix == 0;
 748        let is_last_message = ix == self.messages.len() - 1;
 749
 750        let colors = cx.theme().colors();
 751        let active_color = colors.element_active;
 752        let editor_bg_color = colors.editor_background;
 753        let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
 754
 755        let feedback_container = h_flex().pb_4().px_4().gap_1().justify_between();
 756        let feedback_items = match self.thread.read(cx).feedback() {
 757            Some(feedback) => feedback_container
 758                .child(
 759                    Label::new(match feedback {
 760                        ThreadFeedback::Positive => "Thanks for your feedback!",
 761                        ThreadFeedback::Negative => {
 762                            "We appreciate your feedback and will use it to improve."
 763                        }
 764                    })
 765                    .color(Color::Muted)
 766                    .size(LabelSize::XSmall),
 767                )
 768                .child(
 769                    h_flex()
 770                        .gap_1()
 771                        .child(
 772                            IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
 773                                .icon_size(IconSize::XSmall)
 774                                .icon_color(match feedback {
 775                                    ThreadFeedback::Positive => Color::Accent,
 776                                    ThreadFeedback::Negative => Color::Ignored,
 777                                })
 778                                .shape(ui::IconButtonShape::Square)
 779                                .tooltip(Tooltip::text("Helpful Response"))
 780                                .on_click(cx.listener(move |this, _, window, cx| {
 781                                    this.handle_feedback_click(
 782                                        ThreadFeedback::Positive,
 783                                        window,
 784                                        cx,
 785                                    );
 786                                })),
 787                        )
 788                        .child(
 789                            IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
 790                                .icon_size(IconSize::XSmall)
 791                                .icon_color(match feedback {
 792                                    ThreadFeedback::Positive => Color::Ignored,
 793                                    ThreadFeedback::Negative => Color::Accent,
 794                                })
 795                                .shape(ui::IconButtonShape::Square)
 796                                .tooltip(Tooltip::text("Not Helpful"))
 797                                .on_click(cx.listener(move |this, _, window, cx| {
 798                                    this.handle_feedback_click(
 799                                        ThreadFeedback::Negative,
 800                                        window,
 801                                        cx,
 802                                    );
 803                                })),
 804                        ),
 805                )
 806                .into_any_element(),
 807            None => feedback_container
 808                .child(
 809                    Label::new(
 810                        "Rating the thread sends all of your current conversation to the Zed team.",
 811                    )
 812                    .color(Color::Muted)
 813                    .size(LabelSize::XSmall),
 814                )
 815                .child(
 816                    h_flex()
 817                        .gap_1()
 818                        .child(
 819                            IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
 820                                .icon_size(IconSize::XSmall)
 821                                .icon_color(Color::Ignored)
 822                                .shape(ui::IconButtonShape::Square)
 823                                .tooltip(Tooltip::text("Helpful Response"))
 824                                .on_click(cx.listener(move |this, _, window, cx| {
 825                                    this.handle_feedback_click(
 826                                        ThreadFeedback::Positive,
 827                                        window,
 828                                        cx,
 829                                    );
 830                                })),
 831                        )
 832                        .child(
 833                            IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
 834                                .icon_size(IconSize::XSmall)
 835                                .icon_color(Color::Ignored)
 836                                .shape(ui::IconButtonShape::Square)
 837                                .tooltip(Tooltip::text("Not Helpful"))
 838                                .on_click(cx.listener(move |this, _, window, cx| {
 839                                    this.handle_feedback_click(
 840                                        ThreadFeedback::Negative,
 841                                        window,
 842                                        cx,
 843                                    );
 844                                })),
 845                        ),
 846                )
 847                .into_any_element(),
 848        };
 849
 850        let message_content = v_flex()
 851            .gap_1p5()
 852            .child(
 853                if let Some(edit_message_editor) = edit_message_editor.clone() {
 854                    div()
 855                        .key_context("EditMessageEditor")
 856                        .on_action(cx.listener(Self::cancel_editing_message))
 857                        .on_action(cx.listener(Self::confirm_editing_message))
 858                        .min_h_6()
 859                        .child(edit_message_editor)
 860                } else {
 861                    div()
 862                        .min_h_6()
 863                        .text_ui(cx)
 864                        .child(self.render_message_content(message_id, rendered_message, cx))
 865                },
 866            )
 867            .when_some(context, |parent, context| {
 868                if !context.is_empty() {
 869                    parent.child(
 870                        h_flex().flex_wrap().gap_1().children(
 871                            context
 872                                .into_iter()
 873                                .map(|context| ContextPill::added(context, false, false, None)),
 874                        ),
 875                    )
 876                } else {
 877                    parent
 878                }
 879            });
 880
 881        let styled_message = match message.role {
 882            Role::User => v_flex()
 883                .id(("message-container", ix))
 884                .py_2()
 885                .pl_2()
 886                .pr_2p5()
 887                .child(
 888                    v_flex()
 889                        .bg(colors.editor_background)
 890                        .rounded_lg()
 891                        .border_1()
 892                        .border_color(colors.border)
 893                        .shadow_md()
 894                        .child(
 895                            h_flex()
 896                                .py_1()
 897                                .pl_2()
 898                                .pr_1()
 899                                .bg(bg_user_message_header)
 900                                .border_b_1()
 901                                .border_color(colors.border)
 902                                .justify_between()
 903                                .rounded_t_md()
 904                                .child(
 905                                    h_flex()
 906                                        .gap_1p5()
 907                                        .child(
 908                                            Icon::new(IconName::PersonCircle)
 909                                                .size(IconSize::XSmall)
 910                                                .color(Color::Muted),
 911                                        )
 912                                        .child(
 913                                            Label::new("You")
 914                                                .size(LabelSize::Small)
 915                                                .color(Color::Muted),
 916                                        ),
 917                                )
 918                                .child(
 919                                    h_flex()
 920                                        // DL: To double-check whether we want to fully remove
 921                                        // the editing feature from meassages. Checkpoint sort of
 922                                        // solve the same problem.
 923                                        .invisible()
 924                                        .gap_1()
 925                                        .when_some(
 926                                            edit_message_editor.clone(),
 927                                            |this, edit_message_editor| {
 928                                                let focus_handle =
 929                                                    edit_message_editor.focus_handle(cx);
 930                                                this.child(
 931                                                    Button::new("cancel-edit-message", "Cancel")
 932                                                        .label_size(LabelSize::Small)
 933                                                        .key_binding(
 934                                                            KeyBinding::for_action_in(
 935                                                                &menu::Cancel,
 936                                                                &focus_handle,
 937                                                                window,
 938                                                                cx,
 939                                                            )
 940                                                            .map(|kb| kb.size(rems_from_px(12.))),
 941                                                        )
 942                                                        .on_click(
 943                                                            cx.listener(Self::handle_cancel_click),
 944                                                        ),
 945                                                )
 946                                                .child(
 947                                                    Button::new(
 948                                                        "confirm-edit-message",
 949                                                        "Regenerate",
 950                                                    )
 951                                                    .label_size(LabelSize::Small)
 952                                                    .key_binding(
 953                                                        KeyBinding::for_action_in(
 954                                                            &menu::Confirm,
 955                                                            &focus_handle,
 956                                                            window,
 957                                                            cx,
 958                                                        )
 959                                                        .map(|kb| kb.size(rems_from_px(12.))),
 960                                                    )
 961                                                    .on_click(
 962                                                        cx.listener(Self::handle_regenerate_click),
 963                                                    ),
 964                                                )
 965                                            },
 966                                        )
 967                                        .when(
 968                                            edit_message_editor.is_none() && allow_editing_message,
 969                                            |this| {
 970                                                this.child(
 971                                                    Button::new("edit-message", "Edit")
 972                                                        .label_size(LabelSize::Small)
 973                                                        .on_click(cx.listener({
 974                                                            let message_segments =
 975                                                                message.segments.clone();
 976                                                            move |this, _, window, cx| {
 977                                                                this.start_editing_message(
 978                                                                    message_id,
 979                                                                    &message_segments,
 980                                                                    window,
 981                                                                    cx,
 982                                                                );
 983                                                            }
 984                                                        })),
 985                                                )
 986                                            },
 987                                        ),
 988                                ),
 989                        )
 990                        .child(div().p_2().child(message_content)),
 991                ),
 992            Role::Assistant => v_flex()
 993                .id(("message-container", ix))
 994                .child(v_flex().py_2().px_4().child(message_content))
 995                .when(
 996                    !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
 997                    |parent| {
 998                        parent.child(
 999                            v_flex()
1000                                .children(
1001                                    tool_uses
1002                                        .into_iter()
1003                                        .map(|tool_use| self.render_tool_use(tool_use, cx)),
1004                                )
1005                                .children(scripting_tool_uses.into_iter().map(|tool_use| {
1006                                    self.render_scripting_tool_use(tool_use, window, cx)
1007                                })),
1008                        )
1009                    },
1010                ),
1011            Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1012                v_flex()
1013                    .bg(colors.editor_background)
1014                    .rounded_sm()
1015                    .child(div().p_4().child(message_content)),
1016            ),
1017        };
1018
1019        v_flex()
1020            .w_full()
1021            .when(first_message, |parent| {
1022                parent.child(self.render_rules_item(cx))
1023            })
1024            .when_some(checkpoint, |parent, checkpoint| {
1025                let mut is_pending = false;
1026                let mut error = None;
1027                if let Some(last_restore_checkpoint) =
1028                    self.thread.read(cx).last_restore_checkpoint()
1029                {
1030                    if last_restore_checkpoint.message_id() == message_id {
1031                        match last_restore_checkpoint {
1032                            LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1033                            LastRestoreCheckpoint::Error { error: err, .. } => {
1034                                error = Some(err.clone());
1035                            }
1036                        }
1037                    }
1038                }
1039
1040                let restore_checkpoint_button =
1041                    Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1042                        .icon(if error.is_some() {
1043                            IconName::XCircle
1044                        } else {
1045                            IconName::Undo
1046                        })
1047                        .icon_size(IconSize::XSmall)
1048                        .icon_position(IconPosition::Start)
1049                        .icon_color(if error.is_some() {
1050                            Some(Color::Error)
1051                        } else {
1052                            None
1053                        })
1054                        .label_size(LabelSize::XSmall)
1055                        .disabled(is_pending)
1056                        .on_click(cx.listener(move |this, _, _window, cx| {
1057                            this.thread.update(cx, |thread, cx| {
1058                                thread
1059                                    .restore_checkpoint(checkpoint.clone(), cx)
1060                                    .detach_and_log_err(cx);
1061                            });
1062                        }));
1063
1064                let restore_checkpoint_button = if is_pending {
1065                    restore_checkpoint_button
1066                        .with_animation(
1067                            ("pulsating-restore-checkpoint-button", ix),
1068                            Animation::new(Duration::from_secs(2))
1069                                .repeat()
1070                                .with_easing(pulsating_between(0.6, 1.)),
1071                            |label, delta| label.alpha(delta),
1072                        )
1073                        .into_any_element()
1074                } else if let Some(error) = error {
1075                    restore_checkpoint_button
1076                        .tooltip(Tooltip::text(error.to_string()))
1077                        .into_any_element()
1078                } else {
1079                    restore_checkpoint_button.into_any_element()
1080                };
1081
1082                parent.child(
1083                    h_flex()
1084                        .px_2p5()
1085                        .w_full()
1086                        .gap_1()
1087                        .child(ui::Divider::horizontal())
1088                        .child(restore_checkpoint_button)
1089                        .child(ui::Divider::horizontal()),
1090                )
1091            })
1092            .child(styled_message)
1093            .when(
1094                is_last_message && !self.thread.read(cx).is_generating(),
1095                |parent| parent.child(feedback_items),
1096            )
1097            .into_any()
1098    }
1099
1100    fn render_message_content(
1101        &self,
1102        message_id: MessageId,
1103        rendered_message: &RenderedMessage,
1104        cx: &Context<Self>,
1105    ) -> impl IntoElement {
1106        let pending_thinking_segment_index = rendered_message
1107            .segments
1108            .iter()
1109            .enumerate()
1110            .last()
1111            .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1112            .map(|(index, _)| index);
1113
1114        div()
1115            .text_ui(cx)
1116            .gap_2()
1117            .children(
1118                rendered_message.segments.iter().enumerate().map(
1119                    |(index, segment)| match segment {
1120                        RenderedMessageSegment::Thinking {
1121                            content,
1122                            scroll_handle,
1123                        } => self
1124                            .render_message_thinking_segment(
1125                                message_id,
1126                                index,
1127                                content.clone(),
1128                                &scroll_handle,
1129                                Some(index) == pending_thinking_segment_index,
1130                                cx,
1131                            )
1132                            .into_any_element(),
1133                        RenderedMessageSegment::Text(markdown) => {
1134                            div().p_2p5().child(markdown.clone()).into_any_element()
1135                        }
1136                    },
1137                ),
1138            )
1139    }
1140
1141    fn render_message_thinking_segment(
1142        &self,
1143        message_id: MessageId,
1144        ix: usize,
1145        markdown: Entity<Markdown>,
1146        scroll_handle: &ScrollHandle,
1147        pending: bool,
1148        cx: &Context<Self>,
1149    ) -> impl IntoElement {
1150        let is_open = self
1151            .expanded_thinking_segments
1152            .get(&(message_id, ix))
1153            .copied()
1154            .unwrap_or_default();
1155
1156        let lighter_border = cx.theme().colors().border.opacity(0.5);
1157        let editor_bg = cx.theme().colors().editor_background;
1158
1159        v_flex()
1160            .rounded_lg()
1161            .border_1()
1162            .border_color(lighter_border)
1163            .child(
1164                h_flex()
1165                    .justify_between()
1166                    .py_1()
1167                    .pl_1()
1168                    .pr_2()
1169                    .bg(cx.theme().colors().editor_foreground.opacity(0.025))
1170                    .map(|this| {
1171                        if is_open {
1172                            this.rounded_t_md()
1173                                .border_b_1()
1174                                .border_color(lighter_border)
1175                        } else {
1176                            this.rounded_md()
1177                        }
1178                    })
1179                    .child(
1180                        h_flex()
1181                            .gap_1()
1182                            .child(Disclosure::new("thinking-disclosure", is_open).on_click(
1183                                cx.listener({
1184                                    move |this, _event, _window, _cx| {
1185                                        let is_open = this
1186                                            .expanded_thinking_segments
1187                                            .entry((message_id, ix))
1188                                            .or_insert(false);
1189
1190                                        *is_open = !*is_open;
1191                                    }
1192                                }),
1193                            ))
1194                            .child({
1195                                if pending {
1196                                    Label::new("Thinking…")
1197                                        .size(LabelSize::Small)
1198                                        .buffer_font(cx)
1199                                        .with_animation(
1200                                            "pulsating-label",
1201                                            Animation::new(Duration::from_secs(2))
1202                                                .repeat()
1203                                                .with_easing(pulsating_between(0.4, 0.8)),
1204                                            |label, delta| label.alpha(delta),
1205                                        )
1206                                        .into_any_element()
1207                                } else {
1208                                    Label::new("Thought Process")
1209                                        .size(LabelSize::Small)
1210                                        .buffer_font(cx)
1211                                        .into_any_element()
1212                                }
1213                            }),
1214                    )
1215                    .child({
1216                        let (icon_name, color, animated) = if pending {
1217                            (IconName::ArrowCircle, Color::Accent, true)
1218                        } else {
1219                            (IconName::Check, Color::Success, false)
1220                        };
1221
1222                        let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
1223
1224                        if animated {
1225                            icon.with_animation(
1226                                "arrow-circle",
1227                                Animation::new(Duration::from_secs(2)).repeat(),
1228                                |icon, delta| {
1229                                    icon.transform(Transformation::rotate(percentage(delta)))
1230                                },
1231                            )
1232                            .into_any_element()
1233                        } else {
1234                            icon.into_any_element()
1235                        }
1236                    }),
1237            )
1238            .when(pending && !is_open, |this| {
1239                let gradient_overlay = div()
1240                    .rounded_b_lg()
1241                    .h_20()
1242                    .absolute()
1243                    .w_full()
1244                    .bottom_0()
1245                    .left_0()
1246                    .bg(linear_gradient(
1247                        180.,
1248                        linear_color_stop(editor_bg, 1.),
1249                        linear_color_stop(editor_bg.opacity(0.2), 0.),
1250                    ));
1251
1252                this.child(
1253                    div()
1254                        .relative()
1255                        .bg(editor_bg)
1256                        .rounded_b_lg()
1257                        .text_ui_sm(cx)
1258                        .child(
1259                            div()
1260                                .id(("thinking-content", ix))
1261                                .p_2()
1262                                .h_20()
1263                                .track_scroll(scroll_handle)
1264                                .child(markdown.clone())
1265                                .overflow_hidden(),
1266                        )
1267                        .child(gradient_overlay),
1268                )
1269            })
1270            .when(is_open, |this| {
1271                this.child(
1272                    div()
1273                        .id(("thinking-content", ix))
1274                        .h_full()
1275                        .p_2()
1276                        .rounded_b_lg()
1277                        .bg(editor_bg)
1278                        .text_ui_sm(cx)
1279                        .child(markdown.clone()),
1280                )
1281            })
1282    }
1283
1284    fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
1285        let is_open = self
1286            .expanded_tool_uses
1287            .get(&tool_use.id)
1288            .copied()
1289            .unwrap_or_default();
1290
1291        let lighter_border = cx.theme().colors().border.opacity(0.5);
1292
1293        let tool_icon = match tool_use.name.as_ref() {
1294            "bash" => IconName::Terminal,
1295            "delete-path" => IconName::Trash,
1296            "diagnostics" => IconName::Warning,
1297            "edit-files" => IconName::Pencil,
1298            "fetch" => IconName::Globe,
1299            "list-directory" => IconName::Folder,
1300            "now" => IconName::Info,
1301            "path-search" => IconName::SearchCode,
1302            "read-file" => IconName::Eye,
1303            "regex-search" => IconName::Regex,
1304            "thinking" => IconName::Brain,
1305            _ => IconName::Terminal,
1306        };
1307
1308        div().px_4().child(
1309            v_flex()
1310                .rounded_lg()
1311                .border_1()
1312                .border_color(lighter_border)
1313                .overflow_hidden()
1314                .child(
1315                    h_flex()
1316                        .group("disclosure-header")
1317                        .justify_between()
1318                        .py_1()
1319                        .px_2()
1320                        .bg(cx.theme().colors().editor_foreground.opacity(0.025))
1321                        .map(|element| {
1322                            if is_open {
1323                                element.border_b_1().rounded_t_md()
1324                            } else {
1325                                element.rounded_md()
1326                            }
1327                        })
1328                        .border_color(lighter_border)
1329                        .child(
1330                            h_flex()
1331                                .gap_1p5()
1332                                .child(
1333                                    Icon::new(tool_icon)
1334                                        .size(IconSize::XSmall)
1335                                        .color(Color::Muted),
1336                                )
1337                                .child(
1338                                    div()
1339                                        .text_ui_sm(cx)
1340                                        .children(
1341                                            self.rendered_tool_use_labels
1342                                                .get(&tool_use.id)
1343                                                .cloned(),
1344                                        )
1345                                        .truncate(),
1346                                ),
1347                        )
1348                        .child(
1349                            h_flex()
1350                                .gap_1()
1351                                .child(
1352                                    div().visible_on_hover("disclosure-header").child(
1353                                        Disclosure::new("tool-use-disclosure", is_open)
1354                                            .opened_icon(IconName::ChevronUp)
1355                                            .closed_icon(IconName::ChevronDown)
1356                                            .on_click(cx.listener({
1357                                                let tool_use_id = tool_use.id.clone();
1358                                                move |this, _event, _window, _cx| {
1359                                                    let is_open = this
1360                                                        .expanded_tool_uses
1361                                                        .entry(tool_use_id.clone())
1362                                                        .or_insert(false);
1363
1364                                                    *is_open = !*is_open;
1365                                                }
1366                                            })),
1367                                    ),
1368                                )
1369                                .child({
1370                                    let (icon_name, color, animated) = match &tool_use.status {
1371                                        ToolUseStatus::Pending => {
1372                                            (IconName::Warning, Color::Warning, false)
1373                                        }
1374                                        ToolUseStatus::Running => {
1375                                            (IconName::ArrowCircle, Color::Accent, true)
1376                                        }
1377                                        ToolUseStatus::Finished(_) => {
1378                                            (IconName::Check, Color::Success, false)
1379                                        }
1380                                        ToolUseStatus::Error(_) => {
1381                                            (IconName::Close, Color::Error, false)
1382                                        }
1383                                    };
1384
1385                                    let icon =
1386                                        Icon::new(icon_name).color(color).size(IconSize::Small);
1387
1388                                    if animated {
1389                                        icon.with_animation(
1390                                            "arrow-circle",
1391                                            Animation::new(Duration::from_secs(2)).repeat(),
1392                                            |icon, delta| {
1393                                                icon.transform(Transformation::rotate(percentage(
1394                                                    delta,
1395                                                )))
1396                                            },
1397                                        )
1398                                        .into_any_element()
1399                                    } else {
1400                                        icon.into_any_element()
1401                                    }
1402                                }),
1403                        ),
1404                )
1405                .map(|parent| {
1406                    if !is_open {
1407                        return parent;
1408                    }
1409
1410                    let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1411
1412                    parent.child(
1413                        v_flex()
1414                            .gap_1()
1415                            .bg(cx.theme().colors().editor_background)
1416                            .rounded_b_lg()
1417                            .child(
1418                                content_container()
1419                                    .border_b_1()
1420                                    .border_color(lighter_border)
1421                                    .child(
1422                                        Label::new("Input")
1423                                            .size(LabelSize::XSmall)
1424                                            .color(Color::Muted)
1425                                            .buffer_font(cx),
1426                                    )
1427                                    .child(
1428                                        Label::new(
1429                                            serde_json::to_string_pretty(&tool_use.input)
1430                                                .unwrap_or_default(),
1431                                        )
1432                                        .size(LabelSize::Small)
1433                                        .buffer_font(cx),
1434                                    ),
1435                            )
1436                            .map(|container| match tool_use.status {
1437                                ToolUseStatus::Finished(output) => container.child(
1438                                    content_container()
1439                                        .child(
1440                                            Label::new("Result")
1441                                                .size(LabelSize::XSmall)
1442                                                .color(Color::Muted)
1443                                                .buffer_font(cx),
1444                                        )
1445                                        .child(
1446                                            Label::new(output)
1447                                                .size(LabelSize::Small)
1448                                                .buffer_font(cx),
1449                                        ),
1450                                ),
1451                                ToolUseStatus::Running => container.child(
1452                                    content_container().child(
1453                                        h_flex()
1454                                            .gap_1()
1455                                            .pb_1()
1456                                            .child(
1457                                                Icon::new(IconName::ArrowCircle)
1458                                                    .size(IconSize::Small)
1459                                                    .color(Color::Accent)
1460                                                    .with_animation(
1461                                                        "arrow-circle",
1462                                                        Animation::new(Duration::from_secs(2))
1463                                                            .repeat(),
1464                                                        |icon, delta| {
1465                                                            icon.transform(Transformation::rotate(
1466                                                                percentage(delta),
1467                                                            ))
1468                                                        },
1469                                                    ),
1470                                            )
1471                                            .child(
1472                                                Label::new("Running…")
1473                                                    .size(LabelSize::XSmall)
1474                                                    .color(Color::Muted)
1475                                                    .buffer_font(cx),
1476                                            ),
1477                                    ),
1478                                ),
1479                                ToolUseStatus::Error(err) => container.child(
1480                                    content_container()
1481                                        .child(
1482                                            Label::new("Error")
1483                                                .size(LabelSize::XSmall)
1484                                                .color(Color::Muted)
1485                                                .buffer_font(cx),
1486                                        )
1487                                        .child(
1488                                            Label::new(err).size(LabelSize::Small).buffer_font(cx),
1489                                        ),
1490                                ),
1491                                ToolUseStatus::Pending => container,
1492                            }),
1493                    )
1494                }),
1495        )
1496    }
1497
1498    fn render_scripting_tool_use(
1499        &self,
1500        tool_use: ToolUse,
1501        window: &Window,
1502        cx: &mut Context<Self>,
1503    ) -> impl IntoElement {
1504        let is_open = self
1505            .expanded_tool_uses
1506            .get(&tool_use.id)
1507            .copied()
1508            .unwrap_or_default();
1509
1510        div().px_2p5().child(
1511            v_flex()
1512                .gap_1()
1513                .rounded_lg()
1514                .border_1()
1515                .border_color(cx.theme().colors().border)
1516                .child(
1517                    h_flex()
1518                        .justify_between()
1519                        .py_0p5()
1520                        .pl_1()
1521                        .pr_2()
1522                        .bg(cx.theme().colors().editor_foreground.opacity(0.02))
1523                        .map(|element| {
1524                            if is_open {
1525                                element.border_b_1().rounded_t_md()
1526                            } else {
1527                                element.rounded_md()
1528                            }
1529                        })
1530                        .border_color(cx.theme().colors().border)
1531                        .child(
1532                            h_flex()
1533                                .gap_1()
1534                                .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
1535                                    cx.listener({
1536                                        let tool_use_id = tool_use.id.clone();
1537                                        move |this, _event, _window, _cx| {
1538                                            let is_open = this
1539                                                .expanded_tool_uses
1540                                                .entry(tool_use_id.clone())
1541                                                .or_insert(false);
1542
1543                                            *is_open = !*is_open;
1544                                        }
1545                                    }),
1546                                ))
1547                                .child(div().text_ui_sm(cx).child(render_markdown(
1548                                    tool_use.ui_text.clone(),
1549                                    self.language_registry.clone(),
1550                                    window,
1551                                    cx,
1552                                )))
1553                                .truncate(),
1554                        )
1555                        .child(
1556                            Label::new(match tool_use.status {
1557                                ToolUseStatus::Pending => "Pending",
1558                                ToolUseStatus::Running => "Running",
1559                                ToolUseStatus::Finished(_) => "Finished",
1560                                ToolUseStatus::Error(_) => "Error",
1561                            })
1562                            .size(LabelSize::XSmall)
1563                            .buffer_font(cx),
1564                        ),
1565                )
1566                .map(|parent| {
1567                    if !is_open {
1568                        return parent;
1569                    }
1570
1571                    let lua_script_markdown =
1572                        self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
1573
1574                    parent.child(
1575                        v_flex()
1576                            .child(
1577                                v_flex()
1578                                    .gap_0p5()
1579                                    .py_1()
1580                                    .px_2p5()
1581                                    .border_b_1()
1582                                    .border_color(cx.theme().colors().border)
1583                                    .child(Label::new("Input:"))
1584                                    .map(|parent| {
1585                                        if let Some(markdown) = lua_script_markdown {
1586                                            parent.child(markdown)
1587                                        } else {
1588                                            parent.child(Label::new(
1589                                                "Failed to render script input to Markdown",
1590                                            ))
1591                                        }
1592                                    }),
1593                            )
1594                            .map(|parent| match tool_use.status {
1595                                ToolUseStatus::Finished(output) => parent.child(
1596                                    v_flex()
1597                                        .gap_0p5()
1598                                        .py_1()
1599                                        .px_2p5()
1600                                        .child(Label::new("Result:"))
1601                                        .child(Label::new(output)),
1602                                ),
1603                                ToolUseStatus::Error(err) => parent.child(
1604                                    v_flex()
1605                                        .gap_0p5()
1606                                        .py_1()
1607                                        .px_2p5()
1608                                        .child(Label::new("Error:"))
1609                                        .child(Label::new(err)),
1610                                ),
1611                                ToolUseStatus::Pending | ToolUseStatus::Running => parent,
1612                            }),
1613                    )
1614                }),
1615        )
1616    }
1617
1618    fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1619        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1620        else {
1621            return div().into_any();
1622        };
1623
1624        let rules_files = system_prompt_context
1625            .worktrees
1626            .iter()
1627            .filter_map(|worktree| worktree.rules_file.as_ref())
1628            .collect::<Vec<_>>();
1629
1630        let label_text = match rules_files.as_slice() {
1631            &[] => return div().into_any(),
1632            &[rules_file] => {
1633                format!("Using {:?} file", rules_file.rel_path)
1634            }
1635            rules_files => {
1636                format!("Using {} rules files", rules_files.len())
1637            }
1638        };
1639
1640        div()
1641            .pt_1()
1642            .px_2p5()
1643            .child(
1644                h_flex()
1645                    .w_full()
1646                    .gap_0p5()
1647                    .child(
1648                        h_flex()
1649                            .gap_1p5()
1650                            .child(
1651                                Icon::new(IconName::File)
1652                                    .size(IconSize::XSmall)
1653                                    .color(Color::Disabled),
1654                            )
1655                            .child(
1656                                Label::new(label_text)
1657                                    .size(LabelSize::XSmall)
1658                                    .color(Color::Muted)
1659                                    .buffer_font(cx),
1660                            ),
1661                    )
1662                    .child(
1663                        IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1664                            .shape(ui::IconButtonShape::Square)
1665                            .icon_size(IconSize::XSmall)
1666                            .icon_color(Color::Ignored)
1667                            .on_click(cx.listener(Self::handle_open_rules))
1668                            .tooltip(Tooltip::text("View Rules")),
1669                    ),
1670            )
1671            .into_any()
1672    }
1673
1674    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1675        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1676        else {
1677            return;
1678        };
1679
1680        let abs_paths = system_prompt_context
1681            .worktrees
1682            .iter()
1683            .flat_map(|worktree| worktree.rules_file.as_ref())
1684            .map(|rules_file| rules_file.abs_path.to_path_buf())
1685            .collect::<Vec<_>>();
1686
1687        if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1688            // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1689            // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1690            // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1691            workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1692        }) {
1693            task.detach();
1694        }
1695    }
1696}
1697
1698impl Render for ActiveThread {
1699    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1700        v_flex()
1701            .size_full()
1702            .child(list(self.list_state.clone()).flex_grow())
1703    }
1704}