active_thread.rs

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