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