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::{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                                        .flat_map(|context| context.snapshot(cx))
 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);
 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 =
 930            v_flex()
 931                .gap_1p5()
 932                .child(
 933                    if let Some(edit_message_editor) = edit_message_editor.clone() {
 934                        div()
 935                            .key_context("EditMessageEditor")
 936                            .on_action(cx.listener(Self::cancel_editing_message))
 937                            .on_action(cx.listener(Self::confirm_editing_message))
 938                            .min_h_6()
 939                            .child(edit_message_editor)
 940                    } else {
 941                        div()
 942                            .min_h_6()
 943                            .text_ui(cx)
 944                            .child(self.render_message_content(message_id, rendered_message, cx))
 945                    },
 946                )
 947                .when_some(context, |parent, context| {
 948                    if !context.is_empty() {
 949                        parent.child(h_flex().flex_wrap().gap_1().children(
 950                            context.into_iter().map(|context| {
 951                                let context_id = context.id;
 952                                ContextPill::added(context, false, false, None).on_click(Rc::new(
 953                                    cx.listener({
 954                                        let workspace = workspace.clone();
 955                                        let context_store = context_store.clone();
 956                                        move |_, _, window, cx| {
 957                                            if let Some(workspace) = workspace.upgrade() {
 958                                                open_context(
 959                                                    context_id,
 960                                                    context_store.clone(),
 961                                                    workspace,
 962                                                    window,
 963                                                    cx,
 964                                                );
 965                                                cx.notify();
 966                                            }
 967                                        }
 968                                    }),
 969                                ))
 970                            }),
 971                        ))
 972                    } else {
 973                        parent
 974                    }
 975                });
 976
 977        let styled_message = match message.role {
 978            Role::User => v_flex()
 979                .id(("message-container", ix))
 980                .map(|this| {
 981                    if first_message {
 982                        this.pt_2()
 983                    } else {
 984                        this.pt_4()
 985                    }
 986                })
 987                .pb_4()
 988                .pl_2()
 989                .pr_2p5()
 990                .child(
 991                    v_flex()
 992                        .bg(colors.editor_background)
 993                        .rounded_lg()
 994                        .border_1()
 995                        .border_color(colors.border)
 996                        .shadow_md()
 997                        .child(
 998                            h_flex()
 999                                .py_1()
1000                                .pl_2()
1001                                .pr_1()
1002                                .bg(bg_user_message_header)
1003                                .border_b_1()
1004                                .border_color(colors.border)
1005                                .justify_between()
1006                                .rounded_t_md()
1007                                .child(
1008                                    h_flex()
1009                                        .gap_1p5()
1010                                        .child(
1011                                            Icon::new(IconName::PersonCircle)
1012                                                .size(IconSize::XSmall)
1013                                                .color(Color::Muted),
1014                                        )
1015                                        .child(
1016                                            Label::new("You")
1017                                                .size(LabelSize::Small)
1018                                                .color(Color::Muted),
1019                                        ),
1020                                )
1021                                .child(
1022                                    h_flex()
1023                                        // DL: To double-check whether we want to fully remove
1024                                        // the editing feature from meassages. Checkpoint sort of
1025                                        // solve the same problem.
1026                                        .invisible()
1027                                        .gap_1()
1028                                        .when_some(
1029                                            edit_message_editor.clone(),
1030                                            |this, edit_message_editor| {
1031                                                let focus_handle =
1032                                                    edit_message_editor.focus_handle(cx);
1033                                                this.child(
1034                                                    Button::new("cancel-edit-message", "Cancel")
1035                                                        .label_size(LabelSize::Small)
1036                                                        .key_binding(
1037                                                            KeyBinding::for_action_in(
1038                                                                &menu::Cancel,
1039                                                                &focus_handle,
1040                                                                window,
1041                                                                cx,
1042                                                            )
1043                                                            .map(|kb| kb.size(rems_from_px(12.))),
1044                                                        )
1045                                                        .on_click(
1046                                                            cx.listener(Self::handle_cancel_click),
1047                                                        ),
1048                                                )
1049                                                .child(
1050                                                    Button::new(
1051                                                        "confirm-edit-message",
1052                                                        "Regenerate",
1053                                                    )
1054                                                    .label_size(LabelSize::Small)
1055                                                    .key_binding(
1056                                                        KeyBinding::for_action_in(
1057                                                            &menu::Confirm,
1058                                                            &focus_handle,
1059                                                            window,
1060                                                            cx,
1061                                                        )
1062                                                        .map(|kb| kb.size(rems_from_px(12.))),
1063                                                    )
1064                                                    .on_click(
1065                                                        cx.listener(Self::handle_regenerate_click),
1066                                                    ),
1067                                                )
1068                                            },
1069                                        )
1070                                        .when(
1071                                            edit_message_editor.is_none() && allow_editing_message,
1072                                            |this| {
1073                                                this.child(
1074                                                    Button::new("edit-message", "Edit")
1075                                                        .label_size(LabelSize::Small)
1076                                                        .on_click(cx.listener({
1077                                                            let message_segments =
1078                                                                message.segments.clone();
1079                                                            move |this, _, window, cx| {
1080                                                                this.start_editing_message(
1081                                                                    message_id,
1082                                                                    &message_segments,
1083                                                                    window,
1084                                                                    cx,
1085                                                                );
1086                                                            }
1087                                                        })),
1088                                                )
1089                                            },
1090                                        ),
1091                                ),
1092                        )
1093                        .child(div().p_2().child(message_content)),
1094                ),
1095            Role::Assistant => v_flex()
1096                .id(("message-container", ix))
1097                .ml_2()
1098                .pl_2()
1099                .pr_4()
1100                .border_l_1()
1101                .border_color(cx.theme().colors().border_variant)
1102                .child(message_content)
1103                .when(!tool_uses.is_empty(), |parent| {
1104                    parent.child(
1105                        v_flex().children(
1106                            tool_uses
1107                                .into_iter()
1108                                .map(|tool_use| self.render_tool_use(tool_use, cx)),
1109                        ),
1110                    )
1111                }),
1112            Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1113                v_flex()
1114                    .bg(colors.editor_background)
1115                    .rounded_sm()
1116                    .child(div().p_4().child(message_content)),
1117            ),
1118        };
1119
1120        v_flex()
1121            .w_full()
1122            .when(first_message, |parent| {
1123                parent.child(self.render_rules_item(cx))
1124            })
1125            .when_some(checkpoint, |parent, checkpoint| {
1126                let mut is_pending = false;
1127                let mut error = None;
1128                if let Some(last_restore_checkpoint) =
1129                    self.thread.read(cx).last_restore_checkpoint()
1130                {
1131                    if last_restore_checkpoint.message_id() == message_id {
1132                        match last_restore_checkpoint {
1133                            LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1134                            LastRestoreCheckpoint::Error { error: err, .. } => {
1135                                error = Some(err.clone());
1136                            }
1137                        }
1138                    }
1139                }
1140
1141                let restore_checkpoint_button =
1142                    Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1143                        .icon(if error.is_some() {
1144                            IconName::XCircle
1145                        } else {
1146                            IconName::Undo
1147                        })
1148                        .icon_size(IconSize::XSmall)
1149                        .icon_position(IconPosition::Start)
1150                        .icon_color(if error.is_some() {
1151                            Some(Color::Error)
1152                        } else {
1153                            None
1154                        })
1155                        .label_size(LabelSize::XSmall)
1156                        .disabled(is_pending)
1157                        .on_click(cx.listener(move |this, _, _window, cx| {
1158                            this.thread.update(cx, |thread, cx| {
1159                                thread
1160                                    .restore_checkpoint(checkpoint.clone(), cx)
1161                                    .detach_and_log_err(cx);
1162                            });
1163                        }));
1164
1165                let restore_checkpoint_button = if is_pending {
1166                    restore_checkpoint_button
1167                        .with_animation(
1168                            ("pulsating-restore-checkpoint-button", ix),
1169                            Animation::new(Duration::from_secs(2))
1170                                .repeat()
1171                                .with_easing(pulsating_between(0.6, 1.)),
1172                            |label, delta| label.alpha(delta),
1173                        )
1174                        .into_any_element()
1175                } else if let Some(error) = error {
1176                    restore_checkpoint_button
1177                        .tooltip(Tooltip::text(error.to_string()))
1178                        .into_any_element()
1179                } else {
1180                    restore_checkpoint_button.into_any_element()
1181                };
1182
1183                parent.child(
1184                    h_flex()
1185                        .pt_2p5()
1186                        .px_2p5()
1187                        .w_full()
1188                        .gap_1()
1189                        .child(ui::Divider::horizontal())
1190                        .child(restore_checkpoint_button)
1191                        .child(ui::Divider::horizontal()),
1192                )
1193            })
1194            .child(styled_message)
1195            .when(
1196                is_last_message && !self.thread.read(cx).is_generating(),
1197                |parent| parent.child(feedback_items),
1198            )
1199            .into_any()
1200    }
1201
1202    fn render_message_content(
1203        &self,
1204        message_id: MessageId,
1205        rendered_message: &RenderedMessage,
1206        cx: &Context<Self>,
1207    ) -> impl IntoElement {
1208        let pending_thinking_segment_index = rendered_message
1209            .segments
1210            .iter()
1211            .enumerate()
1212            .last()
1213            .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1214            .map(|(index, _)| index);
1215
1216        div()
1217            .text_ui(cx)
1218            .gap_2()
1219            .children(
1220                rendered_message.segments.iter().enumerate().map(
1221                    |(index, segment)| match segment {
1222                        RenderedMessageSegment::Thinking {
1223                            content,
1224                            scroll_handle,
1225                        } => self
1226                            .render_message_thinking_segment(
1227                                message_id,
1228                                index,
1229                                content.clone(),
1230                                &scroll_handle,
1231                                Some(index) == pending_thinking_segment_index,
1232                                cx,
1233                            )
1234                            .into_any_element(),
1235                        RenderedMessageSegment::Text(markdown) => {
1236                            div().child(markdown.clone()).into_any_element()
1237                        }
1238                    },
1239                ),
1240            )
1241    }
1242
1243    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1244        cx.theme().colors().border.opacity(0.5)
1245    }
1246
1247    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1248        cx.theme()
1249            .colors()
1250            .element_background
1251            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1252    }
1253
1254    fn render_message_thinking_segment(
1255        &self,
1256        message_id: MessageId,
1257        ix: usize,
1258        markdown: Entity<Markdown>,
1259        scroll_handle: &ScrollHandle,
1260        pending: bool,
1261        cx: &Context<Self>,
1262    ) -> impl IntoElement {
1263        let is_open = self
1264            .expanded_thinking_segments
1265            .get(&(message_id, ix))
1266            .copied()
1267            .unwrap_or_default();
1268
1269        let editor_bg = cx.theme().colors().editor_background;
1270
1271        div().pt_0p5().pb_2().child(
1272            v_flex()
1273                .rounded_lg()
1274                .border_1()
1275                .border_color(self.tool_card_border_color(cx))
1276                .child(
1277                    h_flex()
1278                        .group("disclosure-header")
1279                        .justify_between()
1280                        .py_1()
1281                        .px_2()
1282                        .bg(self.tool_card_header_bg(cx))
1283                        .map(|this| {
1284                            if pending || is_open {
1285                                this.rounded_t_md()
1286                                    .border_b_1()
1287                                    .border_color(self.tool_card_border_color(cx))
1288                            } else {
1289                                this.rounded_md()
1290                            }
1291                        })
1292                        .child(
1293                            h_flex()
1294                                .gap_1p5()
1295                                .child(
1296                                    Icon::new(IconName::Brain)
1297                                        .size(IconSize::XSmall)
1298                                        .color(Color::Muted),
1299                                )
1300                                .child({
1301                                    if pending {
1302                                        Label::new("Thinking…")
1303                                            .size(LabelSize::Small)
1304                                            .buffer_font(cx)
1305                                            .with_animation(
1306                                                "pulsating-label",
1307                                                Animation::new(Duration::from_secs(2))
1308                                                    .repeat()
1309                                                    .with_easing(pulsating_between(0.4, 0.8)),
1310                                                |label, delta| label.alpha(delta),
1311                                            )
1312                                            .into_any_element()
1313                                    } else {
1314                                        Label::new("Thought Process")
1315                                            .size(LabelSize::Small)
1316                                            .buffer_font(cx)
1317                                            .into_any_element()
1318                                    }
1319                                }),
1320                        )
1321                        .child(
1322                            h_flex()
1323                                .gap_1()
1324                                .child(
1325                                    div().visible_on_hover("disclosure-header").child(
1326                                        Disclosure::new("thinking-disclosure", is_open)
1327                                            .opened_icon(IconName::ChevronUp)
1328                                            .closed_icon(IconName::ChevronDown)
1329                                            .on_click(cx.listener({
1330                                                move |this, _event, _window, _cx| {
1331                                                    let is_open = this
1332                                                        .expanded_thinking_segments
1333                                                        .entry((message_id, ix))
1334                                                        .or_insert(false);
1335
1336                                                    *is_open = !*is_open;
1337                                                }
1338                                            })),
1339                                    ),
1340                                )
1341                                .child({
1342                                    let (icon_name, color, animated) = if pending {
1343                                        (IconName::ArrowCircle, Color::Accent, true)
1344                                    } else {
1345                                        (IconName::Check, Color::Success, false)
1346                                    };
1347
1348                                    let icon =
1349                                        Icon::new(icon_name).color(color).size(IconSize::Small);
1350
1351                                    if animated {
1352                                        icon.with_animation(
1353                                            "arrow-circle",
1354                                            Animation::new(Duration::from_secs(2)).repeat(),
1355                                            |icon, delta| {
1356                                                icon.transform(Transformation::rotate(percentage(
1357                                                    delta,
1358                                                )))
1359                                            },
1360                                        )
1361                                        .into_any_element()
1362                                    } else {
1363                                        icon.into_any_element()
1364                                    }
1365                                }),
1366                        ),
1367                )
1368                .when(pending && !is_open, |this| {
1369                    let gradient_overlay = div()
1370                        .rounded_b_lg()
1371                        .h_20()
1372                        .absolute()
1373                        .w_full()
1374                        .bottom_0()
1375                        .left_0()
1376                        .bg(linear_gradient(
1377                            180.,
1378                            linear_color_stop(editor_bg, 1.),
1379                            linear_color_stop(editor_bg.opacity(0.2), 0.),
1380                        ));
1381
1382                    this.child(
1383                        div()
1384                            .relative()
1385                            .bg(editor_bg)
1386                            .rounded_b_lg()
1387                            .child(
1388                                div()
1389                                    .id(("thinking-content", ix))
1390                                    .p_2()
1391                                    .h_20()
1392                                    .track_scroll(scroll_handle)
1393                                    .text_ui_sm(cx)
1394                                    .child(markdown.clone())
1395                                    .overflow_hidden(),
1396                            )
1397                            .child(gradient_overlay),
1398                    )
1399                })
1400                .when(is_open, |this| {
1401                    this.child(
1402                        div()
1403                            .id(("thinking-content", ix))
1404                            .h_full()
1405                            .p_2()
1406                            .rounded_b_lg()
1407                            .bg(editor_bg)
1408                            .text_ui_sm(cx)
1409                            .child(markdown.clone()),
1410                    )
1411                }),
1412        )
1413    }
1414
1415    fn render_tool_use(
1416        &self,
1417        tool_use: ToolUse,
1418        cx: &mut Context<Self>,
1419    ) -> impl IntoElement + use<> {
1420        let is_open = self
1421            .expanded_tool_uses
1422            .get(&tool_use.id)
1423            .copied()
1424            .unwrap_or_default();
1425
1426        let status_icons = div().child({
1427            let (icon_name, color, animated) = match &tool_use.status {
1428                ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
1429                    (IconName::Warning, Color::Warning, false)
1430                }
1431                ToolUseStatus::Running => (IconName::ArrowCircle, Color::Accent, true),
1432                ToolUseStatus::Finished(_) => (IconName::Check, Color::Success, false),
1433                ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
1434            };
1435
1436            let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
1437
1438            if animated {
1439                icon.with_animation(
1440                    "arrow-circle",
1441                    Animation::new(Duration::from_secs(2)).repeat(),
1442                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1443                )
1444                .into_any_element()
1445            } else {
1446                icon.into_any_element()
1447            }
1448        });
1449
1450        let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1451        let results_content = v_flex()
1452            .gap_1()
1453            .child(
1454                content_container()
1455                    .child(
1456                        Label::new("Input")
1457                            .size(LabelSize::XSmall)
1458                            .color(Color::Muted)
1459                            .buffer_font(cx),
1460                    )
1461                    .child(
1462                        Label::new(
1463                            serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1464                        )
1465                        .size(LabelSize::Small)
1466                        .buffer_font(cx),
1467                    ),
1468            )
1469            .map(|container| match tool_use.status {
1470                ToolUseStatus::Finished(output) => container.child(
1471                    content_container()
1472                        .border_t_1()
1473                        .border_color(self.tool_card_border_color(cx))
1474                        .child(
1475                            Label::new("Result")
1476                                .size(LabelSize::XSmall)
1477                                .color(Color::Muted)
1478                                .buffer_font(cx),
1479                        )
1480                        .child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
1481                ),
1482                ToolUseStatus::Running => container.child(
1483                    content_container().child(
1484                        h_flex()
1485                            .gap_1()
1486                            .pb_1()
1487                            .border_t_1()
1488                            .border_color(self.tool_card_border_color(cx))
1489                            .child(
1490                                Icon::new(IconName::ArrowCircle)
1491                                    .size(IconSize::Small)
1492                                    .color(Color::Accent)
1493                                    .with_animation(
1494                                        "arrow-circle",
1495                                        Animation::new(Duration::from_secs(2)).repeat(),
1496                                        |icon, delta| {
1497                                            icon.transform(Transformation::rotate(percentage(
1498                                                delta,
1499                                            )))
1500                                        },
1501                                    ),
1502                            )
1503                            .child(
1504                                Label::new("Running…")
1505                                    .size(LabelSize::XSmall)
1506                                    .color(Color::Muted)
1507                                    .buffer_font(cx),
1508                            ),
1509                    ),
1510                ),
1511                ToolUseStatus::Error(err) => container.child(
1512                    content_container()
1513                        .border_t_1()
1514                        .border_color(self.tool_card_border_color(cx))
1515                        .child(
1516                            Label::new("Error")
1517                                .size(LabelSize::XSmall)
1518                                .color(Color::Muted)
1519                                .buffer_font(cx),
1520                        )
1521                        .child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
1522                ),
1523                ToolUseStatus::Pending => container,
1524                ToolUseStatus::NeedsConfirmation => container.child(
1525                    content_container()
1526                        .border_t_1()
1527                        .border_color(self.tool_card_border_color(cx))
1528                        .child(
1529                            Label::new("Asking Permission")
1530                                .size(LabelSize::Small)
1531                                .color(Color::Muted)
1532                                .buffer_font(cx),
1533                        ),
1534                ),
1535            });
1536
1537        fn gradient_overlay(color: Hsla) -> impl IntoElement {
1538            div()
1539                .h_full()
1540                .absolute()
1541                .w_8()
1542                .bottom_0()
1543                .right_12()
1544                .bg(linear_gradient(
1545                    90.,
1546                    linear_color_stop(color, 1.),
1547                    linear_color_stop(color.opacity(0.2), 0.),
1548                ))
1549        }
1550
1551        div().map(|this| {
1552            if !tool_use.needs_confirmation {
1553                this.py_2p5().child(
1554                    v_flex()
1555                        .child(
1556                            h_flex()
1557                                .group("disclosure-header")
1558                                .relative()
1559                                .gap_1p5()
1560                                .justify_between()
1561                                .opacity(0.8)
1562                                .hover(|style| style.opacity(1.))
1563                                .pr_2()
1564                                .child(
1565                                    h_flex()
1566                                        .id("tool-label-container")
1567                                        .gap_1p5()
1568                                        .max_w_full()
1569                                        .overflow_x_scroll()
1570                                        .child(
1571                                            Icon::new(tool_use.icon)
1572                                                .size(IconSize::XSmall)
1573                                                .color(Color::Muted),
1574                                        )
1575                                        .child(
1576                                            h_flex().pr_8().text_ui_sm(cx).children(
1577                                                self.rendered_tool_use_labels
1578                                                    .get(&tool_use.id)
1579                                                    .cloned(),
1580                                            ),
1581                                        ),
1582                                )
1583                                .child(
1584                                    h_flex()
1585                                        .gap_1()
1586                                        .child(
1587                                            div().visible_on_hover("disclosure-header").child(
1588                                                Disclosure::new("tool-use-disclosure", is_open)
1589                                                    .opened_icon(IconName::ChevronUp)
1590                                                    .closed_icon(IconName::ChevronDown)
1591                                                    .on_click(cx.listener({
1592                                                        let tool_use_id = tool_use.id.clone();
1593                                                        move |this, _event, _window, _cx| {
1594                                                            let is_open = this
1595                                                                .expanded_tool_uses
1596                                                                .entry(tool_use_id.clone())
1597                                                                .or_insert(false);
1598
1599                                                            *is_open = !*is_open;
1600                                                        }
1601                                                    })),
1602                                            ),
1603                                        )
1604                                        .child(status_icons),
1605                                )
1606                                .child(gradient_overlay(cx.theme().colors().panel_background)),
1607                        )
1608                        .map(|parent| {
1609                            if !is_open {
1610                                return parent;
1611                            }
1612
1613                            parent.child(
1614                                v_flex()
1615                                    .mt_1()
1616                                    .border_1()
1617                                    .border_color(self.tool_card_border_color(cx))
1618                                    .bg(cx.theme().colors().editor_background)
1619                                    .rounded_lg()
1620                                    .child(results_content),
1621                            )
1622                        }),
1623                )
1624            } else {
1625                this.py_2().child(
1626                    v_flex()
1627                        .rounded_lg()
1628                        .border_1()
1629                        .border_color(self.tool_card_border_color(cx))
1630                        .overflow_hidden()
1631                        .child(
1632                            h_flex()
1633                                .group("disclosure-header")
1634                                .relative()
1635                                .gap_1p5()
1636                                .justify_between()
1637                                .py_1()
1638                                .px_2()
1639                                .bg(self.tool_card_header_bg(cx))
1640                                .map(|element| {
1641                                    if is_open {
1642                                        element.border_b_1().rounded_t_md()
1643                                    } else {
1644                                        element.rounded_md()
1645                                    }
1646                                })
1647                                .border_color(self.tool_card_border_color(cx))
1648                                .child(
1649                                    h_flex()
1650                                        .id("tool-label-container")
1651                                        .gap_1p5()
1652                                        .max_w_full()
1653                                        .overflow_x_scroll()
1654                                        .child(
1655                                            Icon::new(tool_use.icon)
1656                                                .size(IconSize::XSmall)
1657                                                .color(Color::Muted),
1658                                        )
1659                                        .child(
1660                                            h_flex().pr_8().text_ui_sm(cx).children(
1661                                                self.rendered_tool_use_labels
1662                                                    .get(&tool_use.id)
1663                                                    .cloned(),
1664                                            ),
1665                                        ),
1666                                )
1667                                .child(
1668                                    h_flex()
1669                                        .gap_1()
1670                                        .child(
1671                                            div().visible_on_hover("disclosure-header").child(
1672                                                Disclosure::new("tool-use-disclosure", is_open)
1673                                                    .opened_icon(IconName::ChevronUp)
1674                                                    .closed_icon(IconName::ChevronDown)
1675                                                    .on_click(cx.listener({
1676                                                        let tool_use_id = tool_use.id.clone();
1677                                                        move |this, _event, _window, _cx| {
1678                                                            let is_open = this
1679                                                                .expanded_tool_uses
1680                                                                .entry(tool_use_id.clone())
1681                                                                .or_insert(false);
1682
1683                                                            *is_open = !*is_open;
1684                                                        }
1685                                                    })),
1686                                            ),
1687                                        )
1688                                        .child(status_icons),
1689                                )
1690                                .child(gradient_overlay(self.tool_card_header_bg(cx))),
1691                        )
1692                        .map(|parent| {
1693                            if !is_open {
1694                                return parent;
1695                            }
1696
1697                            parent.child(
1698                                v_flex()
1699                                    .bg(cx.theme().colors().editor_background)
1700                                    .rounded_b_lg()
1701                                    .child(results_content),
1702                            )
1703                        }),
1704                )
1705            }
1706        })
1707    }
1708
1709    fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1710        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1711        else {
1712            return div().into_any();
1713        };
1714
1715        let rules_files = system_prompt_context
1716            .worktrees
1717            .iter()
1718            .filter_map(|worktree| worktree.rules_file.as_ref())
1719            .collect::<Vec<_>>();
1720
1721        let label_text = match rules_files.as_slice() {
1722            &[] => return div().into_any(),
1723            &[rules_file] => {
1724                format!("Using {:?} file", rules_file.rel_path)
1725            }
1726            rules_files => {
1727                format!("Using {} rules files", rules_files.len())
1728            }
1729        };
1730
1731        div()
1732            .pt_1()
1733            .px_2p5()
1734            .child(
1735                h_flex()
1736                    .w_full()
1737                    .gap_0p5()
1738                    .child(
1739                        h_flex()
1740                            .gap_1p5()
1741                            .child(
1742                                Icon::new(IconName::File)
1743                                    .size(IconSize::XSmall)
1744                                    .color(Color::Disabled),
1745                            )
1746                            .child(
1747                                Label::new(label_text)
1748                                    .size(LabelSize::XSmall)
1749                                    .color(Color::Muted)
1750                                    .buffer_font(cx),
1751                            ),
1752                    )
1753                    .child(
1754                        IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1755                            .shape(ui::IconButtonShape::Square)
1756                            .icon_size(IconSize::XSmall)
1757                            .icon_color(Color::Ignored)
1758                            .on_click(cx.listener(Self::handle_open_rules))
1759                            .tooltip(Tooltip::text("View Rules")),
1760                    ),
1761            )
1762            .into_any()
1763    }
1764
1765    fn handle_allow_tool(
1766        &mut self,
1767        tool_use_id: LanguageModelToolUseId,
1768        _: &ClickEvent,
1769        _window: &mut Window,
1770        cx: &mut Context<Self>,
1771    ) {
1772        if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
1773            .thread
1774            .read(cx)
1775            .pending_tool(&tool_use_id)
1776            .map(|tool_use| tool_use.status.clone())
1777        {
1778            self.thread.update(cx, |thread, cx| {
1779                thread.run_tool(
1780                    c.tool_use_id.clone(),
1781                    c.ui_text.clone(),
1782                    c.input.clone(),
1783                    &c.messages,
1784                    c.tool.clone(),
1785                    cx,
1786                );
1787            });
1788        }
1789    }
1790
1791    fn handle_deny_tool(
1792        &mut self,
1793        tool_use_id: LanguageModelToolUseId,
1794        tool_name: Arc<str>,
1795        _: &ClickEvent,
1796        _window: &mut Window,
1797        cx: &mut Context<Self>,
1798    ) {
1799        self.thread.update(cx, |thread, cx| {
1800            thread.deny_tool_use(tool_use_id, tool_name, cx);
1801        });
1802    }
1803
1804    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1805        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1806        else {
1807            return;
1808        };
1809
1810        let abs_paths = system_prompt_context
1811            .worktrees
1812            .iter()
1813            .flat_map(|worktree| worktree.rules_file.as_ref())
1814            .map(|rules_file| rules_file.abs_path.to_path_buf())
1815            .collect::<Vec<_>>();
1816
1817        if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1818            // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1819            // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1820            // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1821            workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1822        }) {
1823            task.detach();
1824        }
1825    }
1826
1827    fn render_confirmations<'a>(
1828        &'a mut self,
1829        cx: &'a mut Context<Self>,
1830    ) -> impl Iterator<Item = AnyElement> + 'a {
1831        let thread = self.thread.read(cx);
1832
1833        thread
1834            .tools_needing_confirmation()
1835            .map(|tool| {
1836                div()
1837                    .m_3()
1838                    .p_2()
1839                    .bg(cx.theme().colors().editor_background)
1840                    .border_1()
1841                    .border_color(cx.theme().colors().border)
1842                    .rounded_lg()
1843                    .child(
1844                        v_flex()
1845                            .gap_1()
1846                            .child(
1847                                v_flex()
1848                                    .gap_0p5()
1849                                    .child(
1850                                        Label::new("The agent wants to run this action:")
1851                                            .color(Color::Muted),
1852                                    )
1853                                    .child(div().p_3().child(Label::new(&tool.ui_text))),
1854                            )
1855                            .child(
1856                                h_flex()
1857                                    .gap_1()
1858                                    .child({
1859                                        let tool_id = tool.id.clone();
1860                                        Button::new("allow-tool-action", "Allow").on_click(
1861                                            cx.listener(move |this, event, window, cx| {
1862                                                this.handle_allow_tool(
1863                                                    tool_id.clone(),
1864                                                    event,
1865                                                    window,
1866                                                    cx,
1867                                                )
1868                                            }),
1869                                        )
1870                                    })
1871                                    .child({
1872                                        let tool_id = tool.id.clone();
1873                                        let tool_name = tool.name.clone();
1874                                        Button::new("deny-tool", "Deny").on_click(cx.listener(
1875                                            move |this, event, window, cx| {
1876                                                this.handle_deny_tool(
1877                                                    tool_id.clone(),
1878                                                    tool_name.clone(),
1879                                                    event,
1880                                                    window,
1881                                                    cx,
1882                                                )
1883                                            },
1884                                        ))
1885                                    }),
1886                            )
1887                            .child(
1888                                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.")
1889                                    .color(Color::Muted)
1890                                    .size(LabelSize::Small),
1891                            ),
1892                    )
1893                    .into_any()
1894            })
1895    }
1896
1897    fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
1898        for window in self.notifications.drain(..) {
1899            window
1900                .update(cx, |_, window, _| {
1901                    window.remove_window();
1902                })
1903                .ok();
1904
1905            self.notification_subscriptions.remove(&window);
1906        }
1907    }
1908
1909    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
1910        div()
1911            .occlude()
1912            .id("active-thread-scrollbar")
1913            .on_mouse_move(cx.listener(|_, _, _, cx| {
1914                cx.notify();
1915                cx.stop_propagation()
1916            }))
1917            .on_hover(|_, _, cx| {
1918                cx.stop_propagation();
1919            })
1920            .on_any_mouse_down(|_, _, cx| {
1921                cx.stop_propagation();
1922            })
1923            .on_mouse_up(
1924                MouseButton::Left,
1925                cx.listener(|_, _, _, cx| {
1926                    cx.stop_propagation();
1927                }),
1928            )
1929            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
1930                cx.notify();
1931            }))
1932            .h_full()
1933            .absolute()
1934            .right_1()
1935            .top_1()
1936            .bottom_0()
1937            .w(px(12.))
1938            .cursor_default()
1939            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
1940    }
1941}
1942
1943impl Render for ActiveThread {
1944    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1945        v_flex()
1946            .size_full()
1947            .relative()
1948            .child(list(self.list_state.clone()).flex_grow())
1949            .children(self.render_confirmations(cx))
1950            .child(self.render_vertical_scrollbar(cx))
1951    }
1952}
1953
1954pub(crate) fn open_context(
1955    id: ContextId,
1956    context_store: Entity<ContextStore>,
1957    workspace: Entity<Workspace>,
1958    window: &mut Window,
1959    cx: &mut App,
1960) {
1961    let Some(context) = context_store.read(cx).context_for_id(id) else {
1962        return;
1963    };
1964
1965    match context {
1966        AssistantContext::File(file_context) => {
1967            if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
1968            {
1969                workspace.update(cx, |workspace, cx| {
1970                    workspace
1971                        .open_path(project_path, None, true, window, cx)
1972                        .detach_and_log_err(cx);
1973                });
1974            }
1975        }
1976        AssistantContext::Directory(directory_context) => {
1977            let path = directory_context.path.clone();
1978            workspace.update(cx, |workspace, cx| {
1979                workspace.project().update(cx, |project, cx| {
1980                    if let Some(entry) = project.entry_for_path(&path, cx) {
1981                        cx.emit(project::Event::RevealInProjectPanel(entry.id));
1982                    }
1983                })
1984            })
1985        }
1986        AssistantContext::Symbol(symbol_context) => {
1987            if let Some(project_path) = symbol_context
1988                .context_symbol
1989                .buffer
1990                .read(cx)
1991                .project_path(cx)
1992            {
1993                let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
1994                let target_position = symbol_context
1995                    .context_symbol
1996                    .id
1997                    .range
1998                    .start
1999                    .to_point(&snapshot);
2000
2001                let open_task = workspace.update(cx, |workspace, cx| {
2002                    workspace.open_path(project_path, None, true, window, cx)
2003                });
2004                window
2005                    .spawn(cx, async move |cx| {
2006                        if let Some(active_editor) = open_task
2007                            .await
2008                            .log_err()
2009                            .and_then(|item| item.downcast::<Editor>())
2010                        {
2011                            active_editor
2012                                .downgrade()
2013                                .update_in(cx, |editor, window, cx| {
2014                                    editor.go_to_singleton_buffer_point(
2015                                        target_position,
2016                                        window,
2017                                        cx,
2018                                    );
2019                                })
2020                                .log_err();
2021                        }
2022                    })
2023                    .detach();
2024            }
2025        }
2026        AssistantContext::FetchedUrl(fetched_url_context) => {
2027            cx.open_url(&fetched_url_context.url);
2028        }
2029        AssistantContext::Thread(thread_context) => {
2030            let thread_id = thread_context.thread.read(cx).id().clone();
2031            workspace.update(cx, |workspace, cx| {
2032                if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
2033                    panel.update(cx, |panel, cx| {
2034                        panel
2035                            .open_thread(&thread_id, window, cx)
2036                            .detach_and_log_err(cx)
2037                    });
2038                }
2039            })
2040        }
2041    }
2042}