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 status_icons = div().child({
1424            let (icon_name, color, animated) = match &tool_use.status {
1425                ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
1426                    (IconName::Warning, Color::Warning, false)
1427                }
1428                ToolUseStatus::Running => (IconName::ArrowCircle, Color::Accent, true),
1429                ToolUseStatus::Finished(_) => (IconName::Check, Color::Success, false),
1430                ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
1431            };
1432
1433            let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
1434
1435            if animated {
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            } else {
1443                icon.into_any_element()
1444            }
1445        });
1446
1447        let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1448        let results_content = v_flex()
1449            .gap_1()
1450            .child(
1451                content_container()
1452                    .child(
1453                        Label::new("Input")
1454                            .size(LabelSize::XSmall)
1455                            .color(Color::Muted)
1456                            .buffer_font(cx),
1457                    )
1458                    .child(
1459                        Label::new(
1460                            serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1461                        )
1462                        .size(LabelSize::Small)
1463                        .buffer_font(cx),
1464                    ),
1465            )
1466            .map(|container| match tool_use.status {
1467                ToolUseStatus::Finished(output) => container.child(
1468                    content_container()
1469                        .border_t_1()
1470                        .border_color(self.tool_card_border_color(cx))
1471                        .child(
1472                            Label::new("Result")
1473                                .size(LabelSize::XSmall)
1474                                .color(Color::Muted)
1475                                .buffer_font(cx),
1476                        )
1477                        .child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
1478                ),
1479                ToolUseStatus::Running => container.child(
1480                    content_container().child(
1481                        h_flex()
1482                            .gap_1()
1483                            .pb_1()
1484                            .border_t_1()
1485                            .border_color(self.tool_card_border_color(cx))
1486                            .child(
1487                                Icon::new(IconName::ArrowCircle)
1488                                    .size(IconSize::Small)
1489                                    .color(Color::Accent)
1490                                    .with_animation(
1491                                        "arrow-circle",
1492                                        Animation::new(Duration::from_secs(2)).repeat(),
1493                                        |icon, delta| {
1494                                            icon.transform(Transformation::rotate(percentage(
1495                                                delta,
1496                                            )))
1497                                        },
1498                                    ),
1499                            )
1500                            .child(
1501                                Label::new("Running…")
1502                                    .size(LabelSize::XSmall)
1503                                    .color(Color::Muted)
1504                                    .buffer_font(cx),
1505                            ),
1506                    ),
1507                ),
1508                ToolUseStatus::Error(err) => container.child(
1509                    content_container()
1510                        .border_t_1()
1511                        .border_color(self.tool_card_border_color(cx))
1512                        .child(
1513                            Label::new("Error")
1514                                .size(LabelSize::XSmall)
1515                                .color(Color::Muted)
1516                                .buffer_font(cx),
1517                        )
1518                        .child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
1519                ),
1520                ToolUseStatus::Pending => container,
1521                ToolUseStatus::NeedsConfirmation => container.child(
1522                    content_container()
1523                        .border_t_1()
1524                        .border_color(self.tool_card_border_color(cx))
1525                        .child(
1526                            Label::new("Asking Permission")
1527                                .size(LabelSize::Small)
1528                                .color(Color::Muted)
1529                                .buffer_font(cx),
1530                        ),
1531                ),
1532            });
1533
1534        fn gradient_overlay(color: Hsla) -> impl IntoElement {
1535            div()
1536                .h_full()
1537                .absolute()
1538                .w_8()
1539                .bottom_0()
1540                .right_12()
1541                .bg(linear_gradient(
1542                    90.,
1543                    linear_color_stop(color, 1.),
1544                    linear_color_stop(color.opacity(0.2), 0.),
1545                ))
1546        }
1547
1548        div().map(|this| {
1549            if !tool_use.needs_confirmation {
1550                this.py_2p5().child(
1551                    v_flex()
1552                        .child(
1553                            h_flex()
1554                                .group("disclosure-header")
1555                                .relative()
1556                                .gap_1p5()
1557                                .justify_between()
1558                                .opacity(0.8)
1559                                .hover(|style| style.opacity(1.))
1560                                .pr_2()
1561                                .child(
1562                                    h_flex()
1563                                        .id("tool-label-container")
1564                                        .gap_1p5()
1565                                        .max_w_full()
1566                                        .overflow_x_scroll()
1567                                        .child(
1568                                            Icon::new(tool_use.icon)
1569                                                .size(IconSize::XSmall)
1570                                                .color(Color::Muted),
1571                                        )
1572                                        .child(
1573                                            h_flex().pr_8().text_ui_sm(cx).children(
1574                                                self.rendered_tool_use_labels
1575                                                    .get(&tool_use.id)
1576                                                    .cloned(),
1577                                            ),
1578                                        ),
1579                                )
1580                                .child(
1581                                    h_flex()
1582                                        .gap_1()
1583                                        .child(
1584                                            div().visible_on_hover("disclosure-header").child(
1585                                                Disclosure::new("tool-use-disclosure", is_open)
1586                                                    .opened_icon(IconName::ChevronUp)
1587                                                    .closed_icon(IconName::ChevronDown)
1588                                                    .on_click(cx.listener({
1589                                                        let tool_use_id = tool_use.id.clone();
1590                                                        move |this, _event, _window, _cx| {
1591                                                            let is_open = this
1592                                                                .expanded_tool_uses
1593                                                                .entry(tool_use_id.clone())
1594                                                                .or_insert(false);
1595
1596                                                            *is_open = !*is_open;
1597                                                        }
1598                                                    })),
1599                                            ),
1600                                        )
1601                                        .child(status_icons),
1602                                )
1603                                .child(gradient_overlay(cx.theme().colors().panel_background)),
1604                        )
1605                        .map(|parent| {
1606                            if !is_open {
1607                                return parent;
1608                            }
1609
1610                            parent.child(
1611                                v_flex()
1612                                    .mt_1()
1613                                    .border_1()
1614                                    .border_color(self.tool_card_border_color(cx))
1615                                    .bg(cx.theme().colors().editor_background)
1616                                    .rounded_lg()
1617                                    .child(results_content),
1618                            )
1619                        }),
1620                )
1621            } else {
1622                this.py_2().child(
1623                    v_flex()
1624                        .rounded_lg()
1625                        .border_1()
1626                        .border_color(self.tool_card_border_color(cx))
1627                        .overflow_hidden()
1628                        .child(
1629                            h_flex()
1630                                .group("disclosure-header")
1631                                .relative()
1632                                .gap_1p5()
1633                                .justify_between()
1634                                .py_1()
1635                                .px_2()
1636                                .bg(self.tool_card_header_bg(cx))
1637                                .map(|element| {
1638                                    if is_open {
1639                                        element.border_b_1().rounded_t_md()
1640                                    } else {
1641                                        element.rounded_md()
1642                                    }
1643                                })
1644                                .border_color(self.tool_card_border_color(cx))
1645                                .child(
1646                                    h_flex()
1647                                        .id("tool-label-container")
1648                                        .gap_1p5()
1649                                        .max_w_full()
1650                                        .overflow_x_scroll()
1651                                        .child(
1652                                            Icon::new(tool_use.icon)
1653                                                .size(IconSize::XSmall)
1654                                                .color(Color::Muted),
1655                                        )
1656                                        .child(
1657                                            h_flex().pr_8().text_ui_sm(cx).children(
1658                                                self.rendered_tool_use_labels
1659                                                    .get(&tool_use.id)
1660                                                    .cloned(),
1661                                            ),
1662                                        ),
1663                                )
1664                                .child(
1665                                    h_flex()
1666                                        .gap_1()
1667                                        .child(
1668                                            div().visible_on_hover("disclosure-header").child(
1669                                                Disclosure::new("tool-use-disclosure", is_open)
1670                                                    .opened_icon(IconName::ChevronUp)
1671                                                    .closed_icon(IconName::ChevronDown)
1672                                                    .on_click(cx.listener({
1673                                                        let tool_use_id = tool_use.id.clone();
1674                                                        move |this, _event, _window, _cx| {
1675                                                            let is_open = this
1676                                                                .expanded_tool_uses
1677                                                                .entry(tool_use_id.clone())
1678                                                                .or_insert(false);
1679
1680                                                            *is_open = !*is_open;
1681                                                        }
1682                                                    })),
1683                                            ),
1684                                        )
1685                                        .child(status_icons),
1686                                )
1687                                .child(gradient_overlay(self.tool_card_header_bg(cx))),
1688                        )
1689                        .map(|parent| {
1690                            if !is_open {
1691                                return parent;
1692                            }
1693
1694                            parent.child(
1695                                v_flex()
1696                                    .bg(cx.theme().colors().editor_background)
1697                                    .rounded_b_lg()
1698                                    .child(results_content),
1699                            )
1700                        }),
1701                )
1702            }
1703        })
1704    }
1705
1706    fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1707        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1708        else {
1709            return div().into_any();
1710        };
1711
1712        let rules_files = system_prompt_context
1713            .worktrees
1714            .iter()
1715            .filter_map(|worktree| worktree.rules_file.as_ref())
1716            .collect::<Vec<_>>();
1717
1718        let label_text = match rules_files.as_slice() {
1719            &[] => return div().into_any(),
1720            &[rules_file] => {
1721                format!("Using {:?} file", rules_file.rel_path)
1722            }
1723            rules_files => {
1724                format!("Using {} rules files", rules_files.len())
1725            }
1726        };
1727
1728        div()
1729            .pt_1()
1730            .px_2p5()
1731            .child(
1732                h_flex()
1733                    .w_full()
1734                    .gap_0p5()
1735                    .child(
1736                        h_flex()
1737                            .gap_1p5()
1738                            .child(
1739                                Icon::new(IconName::File)
1740                                    .size(IconSize::XSmall)
1741                                    .color(Color::Disabled),
1742                            )
1743                            .child(
1744                                Label::new(label_text)
1745                                    .size(LabelSize::XSmall)
1746                                    .color(Color::Muted)
1747                                    .buffer_font(cx),
1748                            ),
1749                    )
1750                    .child(
1751                        IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1752                            .shape(ui::IconButtonShape::Square)
1753                            .icon_size(IconSize::XSmall)
1754                            .icon_color(Color::Ignored)
1755                            .on_click(cx.listener(Self::handle_open_rules))
1756                            .tooltip(Tooltip::text("View Rules")),
1757                    ),
1758            )
1759            .into_any()
1760    }
1761
1762    fn handle_allow_tool(
1763        &mut self,
1764        tool_use_id: LanguageModelToolUseId,
1765        _: &ClickEvent,
1766        _window: &mut Window,
1767        cx: &mut Context<Self>,
1768    ) {
1769        if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
1770            .thread
1771            .read(cx)
1772            .pending_tool(&tool_use_id)
1773            .map(|tool_use| tool_use.status.clone())
1774        {
1775            self.thread.update(cx, |thread, cx| {
1776                thread.run_tool(
1777                    c.tool_use_id.clone(),
1778                    c.ui_text.clone(),
1779                    c.input.clone(),
1780                    &c.messages,
1781                    c.tool.clone(),
1782                    cx,
1783                );
1784            });
1785        }
1786    }
1787
1788    fn handle_deny_tool(
1789        &mut self,
1790        tool_use_id: LanguageModelToolUseId,
1791        tool_name: Arc<str>,
1792        _: &ClickEvent,
1793        _window: &mut Window,
1794        cx: &mut Context<Self>,
1795    ) {
1796        self.thread.update(cx, |thread, cx| {
1797            thread.deny_tool_use(tool_use_id, tool_name, cx);
1798        });
1799    }
1800
1801    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1802        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1803        else {
1804            return;
1805        };
1806
1807        let abs_paths = system_prompt_context
1808            .worktrees
1809            .iter()
1810            .flat_map(|worktree| worktree.rules_file.as_ref())
1811            .map(|rules_file| rules_file.abs_path.to_path_buf())
1812            .collect::<Vec<_>>();
1813
1814        if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1815            // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1816            // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1817            // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1818            workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1819        }) {
1820            task.detach();
1821        }
1822    }
1823
1824    fn render_confirmations<'a>(
1825        &'a mut self,
1826        cx: &'a mut Context<Self>,
1827    ) -> impl Iterator<Item = AnyElement> + 'a {
1828        let thread = self.thread.read(cx);
1829
1830        thread
1831            .tools_needing_confirmation()
1832            .map(|tool| {
1833                div()
1834                    .m_3()
1835                    .p_2()
1836                    .bg(cx.theme().colors().editor_background)
1837                    .border_1()
1838                    .border_color(cx.theme().colors().border)
1839                    .rounded_lg()
1840                    .child(
1841                        v_flex()
1842                            .gap_1()
1843                            .child(
1844                                v_flex()
1845                                    .gap_0p5()
1846                                    .child(
1847                                        Label::new("The agent wants to run this action:")
1848                                            .color(Color::Muted),
1849                                    )
1850                                    .child(div().p_3().child(Label::new(&tool.ui_text))),
1851                            )
1852                            .child(
1853                                h_flex()
1854                                    .gap_1()
1855                                    .child({
1856                                        let tool_id = tool.id.clone();
1857                                        Button::new("allow-tool-action", "Allow").on_click(
1858                                            cx.listener(move |this, event, window, cx| {
1859                                                this.handle_allow_tool(
1860                                                    tool_id.clone(),
1861                                                    event,
1862                                                    window,
1863                                                    cx,
1864                                                )
1865                                            }),
1866                                        )
1867                                    })
1868                                    .child({
1869                                        let tool_id = tool.id.clone();
1870                                        let tool_name = tool.name.clone();
1871                                        Button::new("deny-tool", "Deny").on_click(cx.listener(
1872                                            move |this, event, window, cx| {
1873                                                this.handle_deny_tool(
1874                                                    tool_id.clone(),
1875                                                    tool_name.clone(),
1876                                                    event,
1877                                                    window,
1878                                                    cx,
1879                                                )
1880                                            },
1881                                        ))
1882                                    }),
1883                            )
1884                            .child(
1885                                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.")
1886                                    .color(Color::Muted)
1887                                    .size(LabelSize::Small),
1888                            ),
1889                    )
1890                    .into_any()
1891            })
1892    }
1893
1894    fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
1895        for window in self.notifications.drain(..) {
1896            window
1897                .update(cx, |_, window, _| {
1898                    window.remove_window();
1899                })
1900                .ok();
1901
1902            self.notification_subscriptions.remove(&window);
1903        }
1904    }
1905
1906    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
1907        div()
1908            .occlude()
1909            .id("active-thread-scrollbar")
1910            .on_mouse_move(cx.listener(|_, _, _, cx| {
1911                cx.notify();
1912                cx.stop_propagation()
1913            }))
1914            .on_hover(|_, _, cx| {
1915                cx.stop_propagation();
1916            })
1917            .on_any_mouse_down(|_, _, cx| {
1918                cx.stop_propagation();
1919            })
1920            .on_mouse_up(
1921                MouseButton::Left,
1922                cx.listener(|_, _, _, cx| {
1923                    cx.stop_propagation();
1924                }),
1925            )
1926            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
1927                cx.notify();
1928            }))
1929            .h_full()
1930            .absolute()
1931            .right_1()
1932            .top_1()
1933            .bottom_0()
1934            .w(px(12.))
1935            .cursor_default()
1936            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
1937    }
1938}
1939
1940impl Render for ActiveThread {
1941    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1942        v_flex()
1943            .size_full()
1944            .relative()
1945            .child(list(self.list_state.clone()).flex_grow())
1946            .children(self.render_confirmations(cx))
1947            .child(self.render_vertical_scrollbar(cx))
1948    }
1949}
1950
1951pub(crate) fn open_context(
1952    id: ContextId,
1953    context_store: Entity<ContextStore>,
1954    workspace: Entity<Workspace>,
1955    window: &mut Window,
1956    cx: &mut App,
1957) {
1958    let Some(context) = context_store.read(cx).context_for_id(id) else {
1959        return;
1960    };
1961
1962    match context {
1963        AssistantContext::File(file_context) => {
1964            if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
1965            {
1966                workspace.update(cx, |workspace, cx| {
1967                    workspace
1968                        .open_path(project_path, None, true, window, cx)
1969                        .detach_and_log_err(cx);
1970                });
1971            }
1972        }
1973        AssistantContext::Directory(directory_context) => {
1974            let path = directory_context.project_path.clone();
1975            workspace.update(cx, |workspace, cx| {
1976                workspace.project().update(cx, |project, cx| {
1977                    if let Some(entry) = project.entry_for_path(&path, cx) {
1978                        cx.emit(project::Event::RevealInProjectPanel(entry.id));
1979                    }
1980                })
1981            })
1982        }
1983        AssistantContext::Symbol(symbol_context) => {
1984            if let Some(project_path) = symbol_context
1985                .context_symbol
1986                .buffer
1987                .read(cx)
1988                .project_path(cx)
1989            {
1990                let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
1991                let target_position = symbol_context
1992                    .context_symbol
1993                    .id
1994                    .range
1995                    .start
1996                    .to_point(&snapshot);
1997
1998                let open_task = workspace.update(cx, |workspace, cx| {
1999                    workspace.open_path(project_path, None, true, window, cx)
2000                });
2001                window
2002                    .spawn(cx, async move |cx| {
2003                        if let Some(active_editor) = open_task
2004                            .await
2005                            .log_err()
2006                            .and_then(|item| item.downcast::<Editor>())
2007                        {
2008                            active_editor
2009                                .downgrade()
2010                                .update_in(cx, |editor, window, cx| {
2011                                    editor.go_to_singleton_buffer_point(
2012                                        target_position,
2013                                        window,
2014                                        cx,
2015                                    );
2016                                })
2017                                .log_err();
2018                        }
2019                    })
2020                    .detach();
2021            }
2022        }
2023        AssistantContext::FetchedUrl(fetched_url_context) => {
2024            cx.open_url(&fetched_url_context.url);
2025        }
2026        AssistantContext::Thread(thread_context) => {
2027            let thread_id = thread_context.thread.read(cx).id().clone();
2028            workspace.update(cx, |workspace, cx| {
2029                if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
2030                    panel.update(cx, |panel, cx| {
2031                        panel
2032                            .open_thread(&thread_id, window, cx)
2033                            .detach_and_log_err(cx)
2034                    });
2035                }
2036            })
2037        }
2038    }
2039}