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