active_thread.rs

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