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                .map(|this| {
 885                    if first_message {
 886                        this.pt_2()
 887                    } else {
 888                        this.pt_4()
 889                    }
 890                })
 891                .pb_4()
 892                .pl_2()
 893                .pr_2p5()
 894                .child(
 895                    v_flex()
 896                        .bg(colors.editor_background)
 897                        .rounded_lg()
 898                        .border_1()
 899                        .border_color(colors.border)
 900                        .shadow_md()
 901                        .child(
 902                            h_flex()
 903                                .py_1()
 904                                .pl_2()
 905                                .pr_1()
 906                                .bg(bg_user_message_header)
 907                                .border_b_1()
 908                                .border_color(colors.border)
 909                                .justify_between()
 910                                .rounded_t_md()
 911                                .child(
 912                                    h_flex()
 913                                        .gap_1p5()
 914                                        .child(
 915                                            Icon::new(IconName::PersonCircle)
 916                                                .size(IconSize::XSmall)
 917                                                .color(Color::Muted),
 918                                        )
 919                                        .child(
 920                                            Label::new("You")
 921                                                .size(LabelSize::Small)
 922                                                .color(Color::Muted),
 923                                        ),
 924                                )
 925                                .child(
 926                                    h_flex()
 927                                        // DL: To double-check whether we want to fully remove
 928                                        // the editing feature from meassages. Checkpoint sort of
 929                                        // solve the same problem.
 930                                        .invisible()
 931                                        .gap_1()
 932                                        .when_some(
 933                                            edit_message_editor.clone(),
 934                                            |this, edit_message_editor| {
 935                                                let focus_handle =
 936                                                    edit_message_editor.focus_handle(cx);
 937                                                this.child(
 938                                                    Button::new("cancel-edit-message", "Cancel")
 939                                                        .label_size(LabelSize::Small)
 940                                                        .key_binding(
 941                                                            KeyBinding::for_action_in(
 942                                                                &menu::Cancel,
 943                                                                &focus_handle,
 944                                                                window,
 945                                                                cx,
 946                                                            )
 947                                                            .map(|kb| kb.size(rems_from_px(12.))),
 948                                                        )
 949                                                        .on_click(
 950                                                            cx.listener(Self::handle_cancel_click),
 951                                                        ),
 952                                                )
 953                                                .child(
 954                                                    Button::new(
 955                                                        "confirm-edit-message",
 956                                                        "Regenerate",
 957                                                    )
 958                                                    .label_size(LabelSize::Small)
 959                                                    .key_binding(
 960                                                        KeyBinding::for_action_in(
 961                                                            &menu::Confirm,
 962                                                            &focus_handle,
 963                                                            window,
 964                                                            cx,
 965                                                        )
 966                                                        .map(|kb| kb.size(rems_from_px(12.))),
 967                                                    )
 968                                                    .on_click(
 969                                                        cx.listener(Self::handle_regenerate_click),
 970                                                    ),
 971                                                )
 972                                            },
 973                                        )
 974                                        .when(
 975                                            edit_message_editor.is_none() && allow_editing_message,
 976                                            |this| {
 977                                                this.child(
 978                                                    Button::new("edit-message", "Edit")
 979                                                        .label_size(LabelSize::Small)
 980                                                        .on_click(cx.listener({
 981                                                            let message_segments =
 982                                                                message.segments.clone();
 983                                                            move |this, _, window, cx| {
 984                                                                this.start_editing_message(
 985                                                                    message_id,
 986                                                                    &message_segments,
 987                                                                    window,
 988                                                                    cx,
 989                                                                );
 990                                                            }
 991                                                        })),
 992                                                )
 993                                            },
 994                                        ),
 995                                ),
 996                        )
 997                        .child(div().p_2().child(message_content)),
 998                ),
 999            Role::Assistant => v_flex()
1000                .id(("message-container", ix))
1001                .ml_2()
1002                .pl_2()
1003                .border_l_1()
1004                .border_color(cx.theme().colors().border_variant)
1005                .child(message_content)
1006                .when(
1007                    !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
1008                    |parent| {
1009                        parent.child(
1010                            v_flex()
1011                                .children(
1012                                    tool_uses
1013                                        .into_iter()
1014                                        .map(|tool_use| self.render_tool_use(tool_use, cx)),
1015                                )
1016                                .children(scripting_tool_uses.into_iter().map(|tool_use| {
1017                                    self.render_scripting_tool_use(tool_use, window, cx)
1018                                })),
1019                        )
1020                    },
1021                ),
1022            Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1023                v_flex()
1024                    .bg(colors.editor_background)
1025                    .rounded_sm()
1026                    .child(div().p_4().child(message_content)),
1027            ),
1028        };
1029
1030        v_flex()
1031            .w_full()
1032            .when(first_message, |parent| {
1033                parent.child(self.render_rules_item(cx))
1034            })
1035            .when_some(checkpoint, |parent, checkpoint| {
1036                let mut is_pending = false;
1037                let mut error = None;
1038                if let Some(last_restore_checkpoint) =
1039                    self.thread.read(cx).last_restore_checkpoint()
1040                {
1041                    if last_restore_checkpoint.message_id() == message_id {
1042                        match last_restore_checkpoint {
1043                            LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1044                            LastRestoreCheckpoint::Error { error: err, .. } => {
1045                                error = Some(err.clone());
1046                            }
1047                        }
1048                    }
1049                }
1050
1051                let restore_checkpoint_button =
1052                    Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1053                        .icon(if error.is_some() {
1054                            IconName::XCircle
1055                        } else {
1056                            IconName::Undo
1057                        })
1058                        .icon_size(IconSize::XSmall)
1059                        .icon_position(IconPosition::Start)
1060                        .icon_color(if error.is_some() {
1061                            Some(Color::Error)
1062                        } else {
1063                            None
1064                        })
1065                        .label_size(LabelSize::XSmall)
1066                        .disabled(is_pending)
1067                        .on_click(cx.listener(move |this, _, _window, cx| {
1068                            this.thread.update(cx, |thread, cx| {
1069                                thread
1070                                    .restore_checkpoint(checkpoint.clone(), cx)
1071                                    .detach_and_log_err(cx);
1072                            });
1073                        }));
1074
1075                let restore_checkpoint_button = if is_pending {
1076                    restore_checkpoint_button
1077                        .with_animation(
1078                            ("pulsating-restore-checkpoint-button", ix),
1079                            Animation::new(Duration::from_secs(2))
1080                                .repeat()
1081                                .with_easing(pulsating_between(0.6, 1.)),
1082                            |label, delta| label.alpha(delta),
1083                        )
1084                        .into_any_element()
1085                } else if let Some(error) = error {
1086                    restore_checkpoint_button
1087                        .tooltip(Tooltip::text(error.to_string()))
1088                        .into_any_element()
1089                } else {
1090                    restore_checkpoint_button.into_any_element()
1091                };
1092
1093                parent.child(
1094                    h_flex()
1095                        .px_2p5()
1096                        .w_full()
1097                        .gap_1()
1098                        .child(ui::Divider::horizontal())
1099                        .child(restore_checkpoint_button)
1100                        .child(ui::Divider::horizontal()),
1101                )
1102            })
1103            .child(styled_message)
1104            .when(
1105                is_last_message && !self.thread.read(cx).is_generating(),
1106                |parent| parent.child(feedback_items),
1107            )
1108            .into_any()
1109    }
1110
1111    fn render_message_content(
1112        &self,
1113        message_id: MessageId,
1114        rendered_message: &RenderedMessage,
1115        cx: &Context<Self>,
1116    ) -> impl IntoElement {
1117        let pending_thinking_segment_index = rendered_message
1118            .segments
1119            .iter()
1120            .enumerate()
1121            .last()
1122            .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1123            .map(|(index, _)| index);
1124
1125        div()
1126            .text_ui(cx)
1127            .gap_2()
1128            .children(
1129                rendered_message.segments.iter().enumerate().map(
1130                    |(index, segment)| match segment {
1131                        RenderedMessageSegment::Thinking {
1132                            content,
1133                            scroll_handle,
1134                        } => self
1135                            .render_message_thinking_segment(
1136                                message_id,
1137                                index,
1138                                content.clone(),
1139                                &scroll_handle,
1140                                Some(index) == pending_thinking_segment_index,
1141                                cx,
1142                            )
1143                            .into_any_element(),
1144                        RenderedMessageSegment::Text(markdown) => {
1145                            div().child(markdown.clone()).into_any_element()
1146                        }
1147                    },
1148                ),
1149            )
1150    }
1151
1152    fn render_message_thinking_segment(
1153        &self,
1154        message_id: MessageId,
1155        ix: usize,
1156        markdown: Entity<Markdown>,
1157        scroll_handle: &ScrollHandle,
1158        pending: bool,
1159        cx: &Context<Self>,
1160    ) -> impl IntoElement {
1161        let is_open = self
1162            .expanded_thinking_segments
1163            .get(&(message_id, ix))
1164            .copied()
1165            .unwrap_or_default();
1166
1167        let lighter_border = cx.theme().colors().border.opacity(0.5);
1168        let editor_bg = cx.theme().colors().editor_background;
1169
1170        v_flex()
1171            .rounded_lg()
1172            .border_1()
1173            .border_color(lighter_border)
1174            .child(
1175                h_flex()
1176                    .justify_between()
1177                    .py_1()
1178                    .pl_1()
1179                    .pr_2()
1180                    .bg(cx.theme().colors().editor_foreground.opacity(0.025))
1181                    .map(|this| {
1182                        if is_open {
1183                            this.rounded_t_md()
1184                                .border_b_1()
1185                                .border_color(lighter_border)
1186                        } else {
1187                            this.rounded_md()
1188                        }
1189                    })
1190                    .child(
1191                        h_flex()
1192                            .gap_1()
1193                            .child(Disclosure::new("thinking-disclosure", is_open).on_click(
1194                                cx.listener({
1195                                    move |this, _event, _window, _cx| {
1196                                        let is_open = this
1197                                            .expanded_thinking_segments
1198                                            .entry((message_id, ix))
1199                                            .or_insert(false);
1200
1201                                        *is_open = !*is_open;
1202                                    }
1203                                }),
1204                            ))
1205                            .child({
1206                                if pending {
1207                                    Label::new("Thinking…")
1208                                        .size(LabelSize::Small)
1209                                        .buffer_font(cx)
1210                                        .with_animation(
1211                                            "pulsating-label",
1212                                            Animation::new(Duration::from_secs(2))
1213                                                .repeat()
1214                                                .with_easing(pulsating_between(0.4, 0.8)),
1215                                            |label, delta| label.alpha(delta),
1216                                        )
1217                                        .into_any_element()
1218                                } else {
1219                                    Label::new("Thought Process")
1220                                        .size(LabelSize::Small)
1221                                        .buffer_font(cx)
1222                                        .into_any_element()
1223                                }
1224                            }),
1225                    )
1226                    .child({
1227                        let (icon_name, color, animated) = if pending {
1228                            (IconName::ArrowCircle, Color::Accent, true)
1229                        } else {
1230                            (IconName::Check, Color::Success, false)
1231                        };
1232
1233                        let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
1234
1235                        if animated {
1236                            icon.with_animation(
1237                                "arrow-circle",
1238                                Animation::new(Duration::from_secs(2)).repeat(),
1239                                |icon, delta| {
1240                                    icon.transform(Transformation::rotate(percentage(delta)))
1241                                },
1242                            )
1243                            .into_any_element()
1244                        } else {
1245                            icon.into_any_element()
1246                        }
1247                    }),
1248            )
1249            .when(pending && !is_open, |this| {
1250                let gradient_overlay = div()
1251                    .rounded_b_lg()
1252                    .h_20()
1253                    .absolute()
1254                    .w_full()
1255                    .bottom_0()
1256                    .left_0()
1257                    .bg(linear_gradient(
1258                        180.,
1259                        linear_color_stop(editor_bg, 1.),
1260                        linear_color_stop(editor_bg.opacity(0.2), 0.),
1261                    ));
1262
1263                this.child(
1264                    div()
1265                        .relative()
1266                        .bg(editor_bg)
1267                        .rounded_b_lg()
1268                        .text_ui_sm(cx)
1269                        .child(
1270                            div()
1271                                .id(("thinking-content", ix))
1272                                .p_2()
1273                                .h_20()
1274                                .track_scroll(scroll_handle)
1275                                .child(markdown.clone())
1276                                .overflow_hidden(),
1277                        )
1278                        .child(gradient_overlay),
1279                )
1280            })
1281            .when(is_open, |this| {
1282                this.child(
1283                    div()
1284                        .id(("thinking-content", ix))
1285                        .h_full()
1286                        .p_2()
1287                        .rounded_b_lg()
1288                        .bg(editor_bg)
1289                        .text_ui_sm(cx)
1290                        .child(markdown.clone()),
1291                )
1292            })
1293    }
1294
1295    fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
1296        let is_open = self
1297            .expanded_tool_uses
1298            .get(&tool_use.id)
1299            .copied()
1300            .unwrap_or_default();
1301
1302        let lighter_border = cx.theme().colors().border.opacity(0.5);
1303
1304        let tool_icon = match tool_use.name.as_ref() {
1305            "bash" => IconName::Terminal,
1306            "delete-path" => IconName::Trash,
1307            "diagnostics" => IconName::Warning,
1308            "edit-files" => IconName::Pencil,
1309            "fetch" => IconName::Globe,
1310            "list-directory" => IconName::Folder,
1311            "now" => IconName::Info,
1312            "path-search" => IconName::SearchCode,
1313            "read-file" => IconName::Eye,
1314            "regex-search" => IconName::Regex,
1315            "thinking" => IconName::Brain,
1316            _ => IconName::Terminal,
1317        };
1318
1319        div().py_2().pr_4().child(
1320            v_flex()
1321                .rounded_lg()
1322                .border_1()
1323                .border_color(lighter_border)
1324                .overflow_hidden()
1325                .child(
1326                    h_flex()
1327                        .group("disclosure-header")
1328                        .justify_between()
1329                        .py_1()
1330                        .px_2()
1331                        .bg(cx.theme().colors().editor_foreground.opacity(0.025))
1332                        .map(|element| {
1333                            if is_open {
1334                                element.border_b_1().rounded_t_md()
1335                            } else {
1336                                element.rounded_md()
1337                            }
1338                        })
1339                        .border_color(lighter_border)
1340                        .child(
1341                            h_flex()
1342                                .gap_1p5()
1343                                .child(
1344                                    Icon::new(tool_icon)
1345                                        .size(IconSize::XSmall)
1346                                        .color(Color::Muted),
1347                                )
1348                                .child(
1349                                    div()
1350                                        .text_ui_sm(cx)
1351                                        .children(
1352                                            self.rendered_tool_use_labels
1353                                                .get(&tool_use.id)
1354                                                .cloned(),
1355                                        )
1356                                        .truncate(),
1357                                ),
1358                        )
1359                        .child(
1360                            h_flex()
1361                                .gap_1()
1362                                .child(
1363                                    div().visible_on_hover("disclosure-header").child(
1364                                        Disclosure::new("tool-use-disclosure", is_open)
1365                                            .opened_icon(IconName::ChevronUp)
1366                                            .closed_icon(IconName::ChevronDown)
1367                                            .on_click(cx.listener({
1368                                                let tool_use_id = tool_use.id.clone();
1369                                                move |this, _event, _window, _cx| {
1370                                                    let is_open = this
1371                                                        .expanded_tool_uses
1372                                                        .entry(tool_use_id.clone())
1373                                                        .or_insert(false);
1374
1375                                                    *is_open = !*is_open;
1376                                                }
1377                                            })),
1378                                    ),
1379                                )
1380                                .child({
1381                                    let (icon_name, color, animated) = match &tool_use.status {
1382                                        ToolUseStatus::Pending => {
1383                                            (IconName::Warning, Color::Warning, false)
1384                                        }
1385                                        ToolUseStatus::Running => {
1386                                            (IconName::ArrowCircle, Color::Accent, true)
1387                                        }
1388                                        ToolUseStatus::Finished(_) => {
1389                                            (IconName::Check, Color::Success, false)
1390                                        }
1391                                        ToolUseStatus::Error(_) => {
1392                                            (IconName::Close, Color::Error, false)
1393                                        }
1394                                    };
1395
1396                                    let icon =
1397                                        Icon::new(icon_name).color(color).size(IconSize::Small);
1398
1399                                    if animated {
1400                                        icon.with_animation(
1401                                            "arrow-circle",
1402                                            Animation::new(Duration::from_secs(2)).repeat(),
1403                                            |icon, delta| {
1404                                                icon.transform(Transformation::rotate(percentage(
1405                                                    delta,
1406                                                )))
1407                                            },
1408                                        )
1409                                        .into_any_element()
1410                                    } else {
1411                                        icon.into_any_element()
1412                                    }
1413                                }),
1414                        ),
1415                )
1416                .map(|parent| {
1417                    if !is_open {
1418                        return parent;
1419                    }
1420
1421                    let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1422
1423                    parent.child(
1424                        v_flex()
1425                            .gap_1()
1426                            .bg(cx.theme().colors().editor_background)
1427                            .rounded_b_lg()
1428                            .child(
1429                                content_container()
1430                                    .border_b_1()
1431                                    .border_color(lighter_border)
1432                                    .child(
1433                                        Label::new("Input")
1434                                            .size(LabelSize::XSmall)
1435                                            .color(Color::Muted)
1436                                            .buffer_font(cx),
1437                                    )
1438                                    .child(
1439                                        Label::new(
1440                                            serde_json::to_string_pretty(&tool_use.input)
1441                                                .unwrap_or_default(),
1442                                        )
1443                                        .size(LabelSize::Small)
1444                                        .buffer_font(cx),
1445                                    ),
1446                            )
1447                            .map(|container| match tool_use.status {
1448                                ToolUseStatus::Finished(output) => container.child(
1449                                    content_container()
1450                                        .child(
1451                                            Label::new("Result")
1452                                                .size(LabelSize::XSmall)
1453                                                .color(Color::Muted)
1454                                                .buffer_font(cx),
1455                                        )
1456                                        .child(
1457                                            Label::new(output)
1458                                                .size(LabelSize::Small)
1459                                                .buffer_font(cx),
1460                                        ),
1461                                ),
1462                                ToolUseStatus::Running => container.child(
1463                                    content_container().child(
1464                                        h_flex()
1465                                            .gap_1()
1466                                            .pb_1()
1467                                            .child(
1468                                                Icon::new(IconName::ArrowCircle)
1469                                                    .size(IconSize::Small)
1470                                                    .color(Color::Accent)
1471                                                    .with_animation(
1472                                                        "arrow-circle",
1473                                                        Animation::new(Duration::from_secs(2))
1474                                                            .repeat(),
1475                                                        |icon, delta| {
1476                                                            icon.transform(Transformation::rotate(
1477                                                                percentage(delta),
1478                                                            ))
1479                                                        },
1480                                                    ),
1481                                            )
1482                                            .child(
1483                                                Label::new("Running…")
1484                                                    .size(LabelSize::XSmall)
1485                                                    .color(Color::Muted)
1486                                                    .buffer_font(cx),
1487                                            ),
1488                                    ),
1489                                ),
1490                                ToolUseStatus::Error(err) => container.child(
1491                                    content_container()
1492                                        .child(
1493                                            Label::new("Error")
1494                                                .size(LabelSize::XSmall)
1495                                                .color(Color::Muted)
1496                                                .buffer_font(cx),
1497                                        )
1498                                        .child(
1499                                            Label::new(err).size(LabelSize::Small).buffer_font(cx),
1500                                        ),
1501                                ),
1502                                ToolUseStatus::Pending => container,
1503                            }),
1504                    )
1505                }),
1506        )
1507    }
1508
1509    fn render_scripting_tool_use(
1510        &self,
1511        tool_use: ToolUse,
1512        window: &Window,
1513        cx: &mut Context<Self>,
1514    ) -> impl IntoElement {
1515        let is_open = self
1516            .expanded_tool_uses
1517            .get(&tool_use.id)
1518            .copied()
1519            .unwrap_or_default();
1520
1521        div().px_2p5().child(
1522            v_flex()
1523                .gap_1()
1524                .rounded_lg()
1525                .border_1()
1526                .border_color(cx.theme().colors().border)
1527                .child(
1528                    h_flex()
1529                        .justify_between()
1530                        .py_0p5()
1531                        .pl_1()
1532                        .pr_2()
1533                        .bg(cx.theme().colors().editor_foreground.opacity(0.02))
1534                        .map(|element| {
1535                            if is_open {
1536                                element.border_b_1().rounded_t_md()
1537                            } else {
1538                                element.rounded_md()
1539                            }
1540                        })
1541                        .border_color(cx.theme().colors().border)
1542                        .child(
1543                            h_flex()
1544                                .gap_1()
1545                                .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
1546                                    cx.listener({
1547                                        let tool_use_id = tool_use.id.clone();
1548                                        move |this, _event, _window, _cx| {
1549                                            let is_open = this
1550                                                .expanded_tool_uses
1551                                                .entry(tool_use_id.clone())
1552                                                .or_insert(false);
1553
1554                                            *is_open = !*is_open;
1555                                        }
1556                                    }),
1557                                ))
1558                                .child(div().text_ui_sm(cx).child(render_markdown(
1559                                    tool_use.ui_text.clone(),
1560                                    self.language_registry.clone(),
1561                                    window,
1562                                    cx,
1563                                )))
1564                                .truncate(),
1565                        )
1566                        .child(
1567                            Label::new(match tool_use.status {
1568                                ToolUseStatus::Pending => "Pending",
1569                                ToolUseStatus::Running => "Running",
1570                                ToolUseStatus::Finished(_) => "Finished",
1571                                ToolUseStatus::Error(_) => "Error",
1572                            })
1573                            .size(LabelSize::XSmall)
1574                            .buffer_font(cx),
1575                        ),
1576                )
1577                .map(|parent| {
1578                    if !is_open {
1579                        return parent;
1580                    }
1581
1582                    let lua_script_markdown =
1583                        self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
1584
1585                    parent.child(
1586                        v_flex()
1587                            .child(
1588                                v_flex()
1589                                    .gap_0p5()
1590                                    .py_1()
1591                                    .px_2p5()
1592                                    .border_b_1()
1593                                    .border_color(cx.theme().colors().border)
1594                                    .child(Label::new("Input:"))
1595                                    .map(|parent| {
1596                                        if let Some(markdown) = lua_script_markdown {
1597                                            parent.child(markdown)
1598                                        } else {
1599                                            parent.child(Label::new(
1600                                                "Failed to render script input to Markdown",
1601                                            ))
1602                                        }
1603                                    }),
1604                            )
1605                            .map(|parent| match tool_use.status {
1606                                ToolUseStatus::Finished(output) => parent.child(
1607                                    v_flex()
1608                                        .gap_0p5()
1609                                        .py_1()
1610                                        .px_2p5()
1611                                        .child(Label::new("Result:"))
1612                                        .child(Label::new(output)),
1613                                ),
1614                                ToolUseStatus::Error(err) => parent.child(
1615                                    v_flex()
1616                                        .gap_0p5()
1617                                        .py_1()
1618                                        .px_2p5()
1619                                        .child(Label::new("Error:"))
1620                                        .child(Label::new(err)),
1621                                ),
1622                                ToolUseStatus::Pending | ToolUseStatus::Running => parent,
1623                            }),
1624                    )
1625                }),
1626        )
1627    }
1628
1629    fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1630        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1631        else {
1632            return div().into_any();
1633        };
1634
1635        let rules_files = system_prompt_context
1636            .worktrees
1637            .iter()
1638            .filter_map(|worktree| worktree.rules_file.as_ref())
1639            .collect::<Vec<_>>();
1640
1641        let label_text = match rules_files.as_slice() {
1642            &[] => return div().into_any(),
1643            &[rules_file] => {
1644                format!("Using {:?} file", rules_file.rel_path)
1645            }
1646            rules_files => {
1647                format!("Using {} rules files", rules_files.len())
1648            }
1649        };
1650
1651        div()
1652            .pt_1()
1653            .px_2p5()
1654            .child(
1655                h_flex()
1656                    .w_full()
1657                    .gap_0p5()
1658                    .child(
1659                        h_flex()
1660                            .gap_1p5()
1661                            .child(
1662                                Icon::new(IconName::File)
1663                                    .size(IconSize::XSmall)
1664                                    .color(Color::Disabled),
1665                            )
1666                            .child(
1667                                Label::new(label_text)
1668                                    .size(LabelSize::XSmall)
1669                                    .color(Color::Muted)
1670                                    .buffer_font(cx),
1671                            ),
1672                    )
1673                    .child(
1674                        IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1675                            .shape(ui::IconButtonShape::Square)
1676                            .icon_size(IconSize::XSmall)
1677                            .icon_color(Color::Ignored)
1678                            .on_click(cx.listener(Self::handle_open_rules))
1679                            .tooltip(Tooltip::text("View Rules")),
1680                    ),
1681            )
1682            .into_any()
1683    }
1684
1685    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1686        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1687        else {
1688            return;
1689        };
1690
1691        let abs_paths = system_prompt_context
1692            .worktrees
1693            .iter()
1694            .flat_map(|worktree| worktree.rules_file.as_ref())
1695            .map(|rules_file| rules_file.abs_path.to_path_buf())
1696            .collect::<Vec<_>>();
1697
1698        if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1699            // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1700            // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1701            // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1702            workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1703        }) {
1704            task.detach();
1705        }
1706    }
1707}
1708
1709impl Render for ActiveThread {
1710    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1711        v_flex()
1712            .size_full()
1713            .child(list(self.list_state.clone()).flex_grow())
1714    }
1715}