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;
  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, ScrollHandle, Stateful,
  17    StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle,
  18    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()
 536            || !self.notifications.is_empty()
 537            || !AssistantSettings::get_global(cx).notify_when_agent_waiting
 538        {
 539            return;
 540        }
 541
 542        let caption = caption.into();
 543
 544        let title = self
 545            .thread
 546            .read(cx)
 547            .summary()
 548            .unwrap_or("Agent Panel".into());
 549
 550        for screen in cx.displays() {
 551            let options = AgentNotification::window_options(screen, cx);
 552
 553            if let Some(screen_window) = cx
 554                .open_window(options, |_, cx| {
 555                    cx.new(|_| AgentNotification::new(title.clone(), caption.clone(), icon))
 556                })
 557                .log_err()
 558            {
 559                if let Some(pop_up) = screen_window.entity(cx).log_err() {
 560                    self.notification_subscriptions
 561                        .entry(screen_window)
 562                        .or_insert_with(Vec::new)
 563                        .push(cx.subscribe_in(&pop_up, window, {
 564                            |this, _, event, window, cx| match event {
 565                                AgentNotificationEvent::Accepted => {
 566                                    let handle = window.window_handle();
 567                                    cx.activate(true); // Switch back to the Zed application
 568
 569                                    let workspace_handle = this.workspace.clone();
 570
 571                                    // If there are multiple Zed windows, activate the correct one.
 572                                    cx.defer(move |cx| {
 573                                        handle
 574                                            .update(cx, |_view, window, _cx| {
 575                                                window.activate_window();
 576
 577                                                if let Some(workspace) = workspace_handle.upgrade()
 578                                                {
 579                                                    workspace.update(_cx, |workspace, cx| {
 580                                                        workspace.focus_panel::<AssistantPanel>(
 581                                                            window, cx,
 582                                                        );
 583                                                    });
 584                                                }
 585                                            })
 586                                            .log_err();
 587                                    });
 588
 589                                    this.dismiss_notifications(cx);
 590                                }
 591                                AgentNotificationEvent::Dismissed => {
 592                                    this.dismiss_notifications(cx);
 593                                }
 594                            }
 595                        }));
 596
 597                    self.notifications.push(screen_window);
 598
 599                    // If the user manually refocuses the original window, dismiss the popup.
 600                    self.notification_subscriptions
 601                        .entry(screen_window)
 602                        .or_insert_with(Vec::new)
 603                        .push({
 604                            let pop_up_weak = pop_up.downgrade();
 605
 606                            cx.observe_window_activation(window, move |_, window, cx| {
 607                                if window.is_window_active() {
 608                                    if let Some(pop_up) = pop_up_weak.upgrade() {
 609                                        pop_up.update(cx, |_, cx| {
 610                                            cx.emit(AgentNotificationEvent::Dismissed);
 611                                        });
 612                                    }
 613                                }
 614                            })
 615                        });
 616                }
 617            }
 618        }
 619    }
 620
 621    /// Spawns a task to save the active thread.
 622    ///
 623    /// Only one task to save the thread will be in flight at a time.
 624    fn save_thread(&mut self, cx: &mut Context<Self>) {
 625        let thread = self.thread.clone();
 626        self.save_thread_task = Some(cx.spawn(async move |this, cx| {
 627            let task = this
 628                .update(cx, |this, cx| {
 629                    this.thread_store
 630                        .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
 631                })
 632                .ok();
 633
 634            if let Some(task) = task {
 635                task.await.log_err();
 636            }
 637        }));
 638    }
 639
 640    fn start_editing_message(
 641        &mut self,
 642        message_id: MessageId,
 643        message_segments: &[MessageSegment],
 644        window: &mut Window,
 645        cx: &mut Context<Self>,
 646    ) {
 647        // User message should always consist of a single text segment,
 648        // therefore we can skip returning early if it's not a text segment.
 649        let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
 650            return;
 651        };
 652
 653        let buffer = cx.new(|cx| {
 654            MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
 655        });
 656        let editor = cx.new(|cx| {
 657            let mut editor = Editor::new(
 658                editor::EditorMode::AutoHeight { max_lines: 8 },
 659                buffer,
 660                None,
 661                window,
 662                cx,
 663            );
 664            editor.focus_handle(cx).focus(window);
 665            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
 666            editor
 667        });
 668        self.editing_message = Some((
 669            message_id,
 670            EditMessageState {
 671                editor: editor.clone(),
 672            },
 673        ));
 674        cx.notify();
 675    }
 676
 677    fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
 678        self.editing_message.take();
 679        cx.notify();
 680    }
 681
 682    fn confirm_editing_message(
 683        &mut self,
 684        _: &menu::Confirm,
 685        _: &mut Window,
 686        cx: &mut Context<Self>,
 687    ) {
 688        let Some((message_id, state)) = self.editing_message.take() else {
 689            return;
 690        };
 691        let edited_text = state.editor.read(cx).text(cx);
 692        self.thread.update(cx, |thread, cx| {
 693            thread.edit_message(
 694                message_id,
 695                Role::User,
 696                vec![MessageSegment::Text(edited_text)],
 697                cx,
 698            );
 699            for message_id in self.messages_after(message_id) {
 700                thread.delete_message(*message_id, cx);
 701            }
 702        });
 703
 704        let provider = LanguageModelRegistry::read_global(cx).active_provider();
 705        if provider
 706            .as_ref()
 707            .map_or(false, |provider| provider.must_accept_terms(cx))
 708        {
 709            cx.notify();
 710            return;
 711        }
 712        let model_registry = LanguageModelRegistry::read_global(cx);
 713        let Some(model) = model_registry.active_model() else {
 714            return;
 715        };
 716
 717        self.thread.update(cx, |thread, cx| {
 718            thread.send_to_model(model, RequestKind::Chat, cx)
 719        });
 720        cx.notify();
 721    }
 722
 723    fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
 724        self.messages
 725            .iter()
 726            .rev()
 727            .find(|message_id| {
 728                self.thread
 729                    .read(cx)
 730                    .message(**message_id)
 731                    .map_or(false, |message| message.role == Role::User)
 732            })
 733            .cloned()
 734    }
 735
 736    fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
 737        self.messages
 738            .iter()
 739            .position(|id| *id == message_id)
 740            .map(|index| &self.messages[index + 1..])
 741            .unwrap_or(&[])
 742    }
 743
 744    fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 745        self.cancel_editing_message(&menu::Cancel, window, cx);
 746    }
 747
 748    fn handle_regenerate_click(
 749        &mut self,
 750        _: &ClickEvent,
 751        window: &mut Window,
 752        cx: &mut Context<Self>,
 753    ) {
 754        self.confirm_editing_message(&menu::Confirm, window, cx);
 755    }
 756
 757    fn handle_feedback_click(
 758        &mut self,
 759        feedback: ThreadFeedback,
 760        _window: &mut Window,
 761        cx: &mut Context<Self>,
 762    ) {
 763        let report = self
 764            .thread
 765            .update(cx, |thread, cx| thread.report_feedback(feedback, cx));
 766
 767        let this = cx.entity().downgrade();
 768        cx.spawn(async move |_, cx| {
 769            report.await?;
 770            this.update(cx, |_this, cx| cx.notify())
 771        })
 772        .detach_and_log_err(cx);
 773    }
 774
 775    fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 776        let message_id = self.messages[ix];
 777        let Some(message) = self.thread.read(cx).message(message_id) else {
 778            return Empty.into_any();
 779        };
 780
 781        let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
 782            return Empty.into_any();
 783        };
 784
 785        let context_store = self.context_store.clone();
 786        let workspace = self.workspace.clone();
 787
 788        let thread = self.thread.read(cx);
 789        // Get all the data we need from thread before we start using it in closures
 790        let checkpoint = thread.checkpoint_for_message(message_id);
 791        let context = thread.context_for_message(message_id);
 792        let tool_uses = thread.tool_uses_for_message(message_id, cx);
 793
 794        // Don't render user messages that are just there for returning tool results.
 795        if message.role == Role::User && thread.message_has_tool_results(message_id) {
 796            return Empty.into_any();
 797        }
 798
 799        let allow_editing_message =
 800            message.role == Role::User && self.last_user_message(cx) == Some(message_id);
 801
 802        let edit_message_editor = self
 803            .editing_message
 804            .as_ref()
 805            .filter(|(id, _)| *id == message_id)
 806            .map(|(_, state)| state.editor.clone());
 807
 808        let first_message = ix == 0;
 809        let is_last_message = ix == self.messages.len() - 1;
 810
 811        let colors = cx.theme().colors();
 812        let active_color = colors.element_active;
 813        let editor_bg_color = colors.editor_background;
 814        let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
 815
 816        let feedback_container = h_flex().pt_2().pb_4().px_4().gap_1().justify_between();
 817        let feedback_items = match self.thread.read(cx).feedback() {
 818            Some(feedback) => feedback_container
 819                .child(
 820                    Label::new(match feedback {
 821                        ThreadFeedback::Positive => "Thanks for your feedback!",
 822                        ThreadFeedback::Negative => {
 823                            "We appreciate your feedback and will use it to improve."
 824                        }
 825                    })
 826                    .color(Color::Muted)
 827                    .size(LabelSize::XSmall),
 828                )
 829                .child(
 830                    h_flex()
 831                        .gap_1()
 832                        .child(
 833                            IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
 834                                .icon_size(IconSize::XSmall)
 835                                .icon_color(match feedback {
 836                                    ThreadFeedback::Positive => Color::Accent,
 837                                    ThreadFeedback::Negative => Color::Ignored,
 838                                })
 839                                .shape(ui::IconButtonShape::Square)
 840                                .tooltip(Tooltip::text("Helpful Response"))
 841                                .on_click(cx.listener(move |this, _, window, cx| {
 842                                    this.handle_feedback_click(
 843                                        ThreadFeedback::Positive,
 844                                        window,
 845                                        cx,
 846                                    );
 847                                })),
 848                        )
 849                        .child(
 850                            IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
 851                                .icon_size(IconSize::XSmall)
 852                                .icon_color(match feedback {
 853                                    ThreadFeedback::Positive => Color::Ignored,
 854                                    ThreadFeedback::Negative => Color::Accent,
 855                                })
 856                                .shape(ui::IconButtonShape::Square)
 857                                .tooltip(Tooltip::text("Not Helpful"))
 858                                .on_click(cx.listener(move |this, _, window, cx| {
 859                                    this.handle_feedback_click(
 860                                        ThreadFeedback::Negative,
 861                                        window,
 862                                        cx,
 863                                    );
 864                                })),
 865                        ),
 866                )
 867                .into_any_element(),
 868            None => feedback_container
 869                .child(
 870                    Label::new(
 871                        "Rating the thread sends all of your current conversation to the Zed team.",
 872                    )
 873                    .color(Color::Muted)
 874                    .size(LabelSize::XSmall),
 875                )
 876                .child(
 877                    h_flex()
 878                        .gap_1()
 879                        .child(
 880                            IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
 881                                .icon_size(IconSize::XSmall)
 882                                .icon_color(Color::Ignored)
 883                                .shape(ui::IconButtonShape::Square)
 884                                .tooltip(Tooltip::text("Helpful Response"))
 885                                .on_click(cx.listener(move |this, _, window, cx| {
 886                                    this.handle_feedback_click(
 887                                        ThreadFeedback::Positive,
 888                                        window,
 889                                        cx,
 890                                    );
 891                                })),
 892                        )
 893                        .child(
 894                            IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
 895                                .icon_size(IconSize::XSmall)
 896                                .icon_color(Color::Ignored)
 897                                .shape(ui::IconButtonShape::Square)
 898                                .tooltip(Tooltip::text("Not Helpful"))
 899                                .on_click(cx.listener(move |this, _, window, cx| {
 900                                    this.handle_feedback_click(
 901                                        ThreadFeedback::Negative,
 902                                        window,
 903                                        cx,
 904                                    );
 905                                })),
 906                        ),
 907                )
 908                .into_any_element(),
 909        };
 910
 911        let message_content =
 912            v_flex()
 913                .gap_1p5()
 914                .child(
 915                    if let Some(edit_message_editor) = edit_message_editor.clone() {
 916                        div()
 917                            .key_context("EditMessageEditor")
 918                            .on_action(cx.listener(Self::cancel_editing_message))
 919                            .on_action(cx.listener(Self::confirm_editing_message))
 920                            .min_h_6()
 921                            .child(edit_message_editor)
 922                    } else {
 923                        div()
 924                            .min_h_6()
 925                            .text_ui(cx)
 926                            .child(self.render_message_content(message_id, rendered_message, cx))
 927                    },
 928                )
 929                .when_some(context, |parent, context| {
 930                    if !context.is_empty() {
 931                        parent.child(h_flex().flex_wrap().gap_1().children(
 932                            context.into_iter().map(|context| {
 933                                let context_id = context.id;
 934                                ContextPill::added(context, false, false, None).on_click(Rc::new(
 935                                    cx.listener({
 936                                        let workspace = workspace.clone();
 937                                        let context_store = context_store.clone();
 938                                        move |_, _, window, cx| {
 939                                            if let Some(workspace) = workspace.upgrade() {
 940                                                open_context(
 941                                                    context_id,
 942                                                    context_store.clone(),
 943                                                    workspace,
 944                                                    window,
 945                                                    cx,
 946                                                );
 947                                                cx.notify();
 948                                            }
 949                                        }
 950                                    }),
 951                                ))
 952                            }),
 953                        ))
 954                    } else {
 955                        parent
 956                    }
 957                });
 958
 959        let styled_message = match message.role {
 960            Role::User => v_flex()
 961                .id(("message-container", ix))
 962                .map(|this| {
 963                    if first_message {
 964                        this.pt_2()
 965                    } else {
 966                        this.pt_4()
 967                    }
 968                })
 969                .pb_4()
 970                .pl_2()
 971                .pr_2p5()
 972                .child(
 973                    v_flex()
 974                        .bg(colors.editor_background)
 975                        .rounded_lg()
 976                        .border_1()
 977                        .border_color(colors.border)
 978                        .shadow_md()
 979                        .child(
 980                            h_flex()
 981                                .py_1()
 982                                .pl_2()
 983                                .pr_1()
 984                                .bg(bg_user_message_header)
 985                                .border_b_1()
 986                                .border_color(colors.border)
 987                                .justify_between()
 988                                .rounded_t_md()
 989                                .child(
 990                                    h_flex()
 991                                        .gap_1p5()
 992                                        .child(
 993                                            Icon::new(IconName::PersonCircle)
 994                                                .size(IconSize::XSmall)
 995                                                .color(Color::Muted),
 996                                        )
 997                                        .child(
 998                                            Label::new("You")
 999                                                .size(LabelSize::Small)
1000                                                .color(Color::Muted),
1001                                        ),
1002                                )
1003                                .child(
1004                                    h_flex()
1005                                        // DL: To double-check whether we want to fully remove
1006                                        // the editing feature from meassages. Checkpoint sort of
1007                                        // solve the same problem.
1008                                        .invisible()
1009                                        .gap_1()
1010                                        .when_some(
1011                                            edit_message_editor.clone(),
1012                                            |this, edit_message_editor| {
1013                                                let focus_handle =
1014                                                    edit_message_editor.focus_handle(cx);
1015                                                this.child(
1016                                                    Button::new("cancel-edit-message", "Cancel")
1017                                                        .label_size(LabelSize::Small)
1018                                                        .key_binding(
1019                                                            KeyBinding::for_action_in(
1020                                                                &menu::Cancel,
1021                                                                &focus_handle,
1022                                                                window,
1023                                                                cx,
1024                                                            )
1025                                                            .map(|kb| kb.size(rems_from_px(12.))),
1026                                                        )
1027                                                        .on_click(
1028                                                            cx.listener(Self::handle_cancel_click),
1029                                                        ),
1030                                                )
1031                                                .child(
1032                                                    Button::new(
1033                                                        "confirm-edit-message",
1034                                                        "Regenerate",
1035                                                    )
1036                                                    .label_size(LabelSize::Small)
1037                                                    .key_binding(
1038                                                        KeyBinding::for_action_in(
1039                                                            &menu::Confirm,
1040                                                            &focus_handle,
1041                                                            window,
1042                                                            cx,
1043                                                        )
1044                                                        .map(|kb| kb.size(rems_from_px(12.))),
1045                                                    )
1046                                                    .on_click(
1047                                                        cx.listener(Self::handle_regenerate_click),
1048                                                    ),
1049                                                )
1050                                            },
1051                                        )
1052                                        .when(
1053                                            edit_message_editor.is_none() && allow_editing_message,
1054                                            |this| {
1055                                                this.child(
1056                                                    Button::new("edit-message", "Edit")
1057                                                        .label_size(LabelSize::Small)
1058                                                        .on_click(cx.listener({
1059                                                            let message_segments =
1060                                                                message.segments.clone();
1061                                                            move |this, _, window, cx| {
1062                                                                this.start_editing_message(
1063                                                                    message_id,
1064                                                                    &message_segments,
1065                                                                    window,
1066                                                                    cx,
1067                                                                );
1068                                                            }
1069                                                        })),
1070                                                )
1071                                            },
1072                                        ),
1073                                ),
1074                        )
1075                        .child(div().p_2().child(message_content)),
1076                ),
1077            Role::Assistant => v_flex()
1078                .id(("message-container", ix))
1079                .ml_2()
1080                .pl_2()
1081                .pr_4()
1082                .border_l_1()
1083                .border_color(cx.theme().colors().border_variant)
1084                .child(message_content)
1085                .when(!tool_uses.is_empty(), |parent| {
1086                    parent.child(
1087                        v_flex().children(
1088                            tool_uses
1089                                .into_iter()
1090                                .map(|tool_use| self.render_tool_use(tool_use, cx)),
1091                        ),
1092                    )
1093                }),
1094            Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1095                v_flex()
1096                    .bg(colors.editor_background)
1097                    .rounded_sm()
1098                    .child(div().p_4().child(message_content)),
1099            ),
1100        };
1101
1102        v_flex()
1103            .w_full()
1104            .when(first_message, |parent| {
1105                parent.child(self.render_rules_item(cx))
1106            })
1107            .when_some(checkpoint, |parent, checkpoint| {
1108                let mut is_pending = false;
1109                let mut error = None;
1110                if let Some(last_restore_checkpoint) =
1111                    self.thread.read(cx).last_restore_checkpoint()
1112                {
1113                    if last_restore_checkpoint.message_id() == message_id {
1114                        match last_restore_checkpoint {
1115                            LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1116                            LastRestoreCheckpoint::Error { error: err, .. } => {
1117                                error = Some(err.clone());
1118                            }
1119                        }
1120                    }
1121                }
1122
1123                let restore_checkpoint_button =
1124                    Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1125                        .icon(if error.is_some() {
1126                            IconName::XCircle
1127                        } else {
1128                            IconName::Undo
1129                        })
1130                        .icon_size(IconSize::XSmall)
1131                        .icon_position(IconPosition::Start)
1132                        .icon_color(if error.is_some() {
1133                            Some(Color::Error)
1134                        } else {
1135                            None
1136                        })
1137                        .label_size(LabelSize::XSmall)
1138                        .disabled(is_pending)
1139                        .on_click(cx.listener(move |this, _, _window, cx| {
1140                            this.thread.update(cx, |thread, cx| {
1141                                thread
1142                                    .restore_checkpoint(checkpoint.clone(), cx)
1143                                    .detach_and_log_err(cx);
1144                            });
1145                        }));
1146
1147                let restore_checkpoint_button = if is_pending {
1148                    restore_checkpoint_button
1149                        .with_animation(
1150                            ("pulsating-restore-checkpoint-button", ix),
1151                            Animation::new(Duration::from_secs(2))
1152                                .repeat()
1153                                .with_easing(pulsating_between(0.6, 1.)),
1154                            |label, delta| label.alpha(delta),
1155                        )
1156                        .into_any_element()
1157                } else if let Some(error) = error {
1158                    restore_checkpoint_button
1159                        .tooltip(Tooltip::text(error.to_string()))
1160                        .into_any_element()
1161                } else {
1162                    restore_checkpoint_button.into_any_element()
1163                };
1164
1165                parent.child(
1166                    h_flex()
1167                        .pt_2p5()
1168                        .px_2p5()
1169                        .w_full()
1170                        .gap_1()
1171                        .child(ui::Divider::horizontal())
1172                        .child(restore_checkpoint_button)
1173                        .child(ui::Divider::horizontal()),
1174                )
1175            })
1176            .child(styled_message)
1177            .when(
1178                is_last_message && !self.thread.read(cx).is_generating(),
1179                |parent| parent.child(feedback_items),
1180            )
1181            .into_any()
1182    }
1183
1184    fn render_message_content(
1185        &self,
1186        message_id: MessageId,
1187        rendered_message: &RenderedMessage,
1188        cx: &Context<Self>,
1189    ) -> impl IntoElement {
1190        let pending_thinking_segment_index = rendered_message
1191            .segments
1192            .iter()
1193            .enumerate()
1194            .last()
1195            .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1196            .map(|(index, _)| index);
1197
1198        div()
1199            .text_ui(cx)
1200            .gap_2()
1201            .children(
1202                rendered_message.segments.iter().enumerate().map(
1203                    |(index, segment)| match segment {
1204                        RenderedMessageSegment::Thinking {
1205                            content,
1206                            scroll_handle,
1207                        } => self
1208                            .render_message_thinking_segment(
1209                                message_id,
1210                                index,
1211                                content.clone(),
1212                                &scroll_handle,
1213                                Some(index) == pending_thinking_segment_index,
1214                                cx,
1215                            )
1216                            .into_any_element(),
1217                        RenderedMessageSegment::Text(markdown) => {
1218                            div().child(markdown.clone()).into_any_element()
1219                        }
1220                    },
1221                ),
1222            )
1223    }
1224
1225    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
1226        cx.theme().colors().border.opacity(0.5)
1227    }
1228
1229    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
1230        cx.theme()
1231            .colors()
1232            .element_background
1233            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
1234    }
1235
1236    fn render_message_thinking_segment(
1237        &self,
1238        message_id: MessageId,
1239        ix: usize,
1240        markdown: Entity<Markdown>,
1241        scroll_handle: &ScrollHandle,
1242        pending: bool,
1243        cx: &Context<Self>,
1244    ) -> impl IntoElement {
1245        let is_open = self
1246            .expanded_thinking_segments
1247            .get(&(message_id, ix))
1248            .copied()
1249            .unwrap_or_default();
1250
1251        let editor_bg = cx.theme().colors().editor_background;
1252
1253        div().py_2().child(
1254            v_flex()
1255                .rounded_lg()
1256                .border_1()
1257                .border_color(self.tool_card_border_color(cx))
1258                .child(
1259                    h_flex()
1260                        .group("disclosure-header")
1261                        .justify_between()
1262                        .py_1()
1263                        .px_2()
1264                        .bg(self.tool_card_header_bg(cx))
1265                        .map(|this| {
1266                            if pending || is_open {
1267                                this.rounded_t_md()
1268                                    .border_b_1()
1269                                    .border_color(self.tool_card_border_color(cx))
1270                            } else {
1271                                this.rounded_md()
1272                            }
1273                        })
1274                        .child(
1275                            h_flex()
1276                                .gap_1p5()
1277                                .child(
1278                                    Icon::new(IconName::Brain)
1279                                        .size(IconSize::XSmall)
1280                                        .color(Color::Muted),
1281                                )
1282                                .child({
1283                                    if pending {
1284                                        Label::new("Thinking…")
1285                                            .size(LabelSize::Small)
1286                                            .buffer_font(cx)
1287                                            .with_animation(
1288                                                "pulsating-label",
1289                                                Animation::new(Duration::from_secs(2))
1290                                                    .repeat()
1291                                                    .with_easing(pulsating_between(0.4, 0.8)),
1292                                                |label, delta| label.alpha(delta),
1293                                            )
1294                                            .into_any_element()
1295                                    } else {
1296                                        Label::new("Thought Process")
1297                                            .size(LabelSize::Small)
1298                                            .buffer_font(cx)
1299                                            .into_any_element()
1300                                    }
1301                                }),
1302                        )
1303                        .child(
1304                            h_flex()
1305                                .gap_1()
1306                                .child(
1307                                    div().visible_on_hover("disclosure-header").child(
1308                                        Disclosure::new("thinking-disclosure", is_open)
1309                                            .opened_icon(IconName::ChevronUp)
1310                                            .closed_icon(IconName::ChevronDown)
1311                                            .on_click(cx.listener({
1312                                                move |this, _event, _window, _cx| {
1313                                                    let is_open = this
1314                                                        .expanded_thinking_segments
1315                                                        .entry((message_id, ix))
1316                                                        .or_insert(false);
1317
1318                                                    *is_open = !*is_open;
1319                                                }
1320                                            })),
1321                                    ),
1322                                )
1323                                .child({
1324                                    let (icon_name, color, animated) = if pending {
1325                                        (IconName::ArrowCircle, Color::Accent, true)
1326                                    } else {
1327                                        (IconName::Check, Color::Success, false)
1328                                    };
1329
1330                                    let icon =
1331                                        Icon::new(icon_name).color(color).size(IconSize::Small);
1332
1333                                    if animated {
1334                                        icon.with_animation(
1335                                            "arrow-circle",
1336                                            Animation::new(Duration::from_secs(2)).repeat(),
1337                                            |icon, delta| {
1338                                                icon.transform(Transformation::rotate(percentage(
1339                                                    delta,
1340                                                )))
1341                                            },
1342                                        )
1343                                        .into_any_element()
1344                                    } else {
1345                                        icon.into_any_element()
1346                                    }
1347                                }),
1348                        ),
1349                )
1350                .when(pending && !is_open, |this| {
1351                    let gradient_overlay = div()
1352                        .rounded_b_lg()
1353                        .h_20()
1354                        .absolute()
1355                        .w_full()
1356                        .bottom_0()
1357                        .left_0()
1358                        .bg(linear_gradient(
1359                            180.,
1360                            linear_color_stop(editor_bg, 1.),
1361                            linear_color_stop(editor_bg.opacity(0.2), 0.),
1362                        ));
1363
1364                    this.child(
1365                        div()
1366                            .relative()
1367                            .bg(editor_bg)
1368                            .rounded_b_lg()
1369                            .child(
1370                                div()
1371                                    .id(("thinking-content", ix))
1372                                    .p_2()
1373                                    .h_20()
1374                                    .track_scroll(scroll_handle)
1375                                    .text_ui_sm(cx)
1376                                    .child(markdown.clone())
1377                                    .overflow_hidden(),
1378                            )
1379                            .child(gradient_overlay),
1380                    )
1381                })
1382                .when(is_open, |this| {
1383                    this.child(
1384                        div()
1385                            .id(("thinking-content", ix))
1386                            .h_full()
1387                            .p_2()
1388                            .rounded_b_lg()
1389                            .bg(editor_bg)
1390                            .text_ui_sm(cx)
1391                            .child(markdown.clone()),
1392                    )
1393                }),
1394        )
1395    }
1396
1397    fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
1398        let is_open = self
1399            .expanded_tool_uses
1400            .get(&tool_use.id)
1401            .copied()
1402            .unwrap_or_default();
1403
1404        let status_icons = div().child({
1405            let (icon_name, color, animated) = match &tool_use.status {
1406                ToolUseStatus::Pending | ToolUseStatus::NeedsConfirmation => {
1407                    (IconName::Warning, Color::Warning, false)
1408                }
1409                ToolUseStatus::Running => (IconName::ArrowCircle, Color::Accent, true),
1410                ToolUseStatus::Finished(_) => (IconName::Check, Color::Success, false),
1411                ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
1412            };
1413
1414            let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
1415
1416            if animated {
1417                icon.with_animation(
1418                    "arrow-circle",
1419                    Animation::new(Duration::from_secs(2)).repeat(),
1420                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1421                )
1422                .into_any_element()
1423            } else {
1424                icon.into_any_element()
1425            }
1426        });
1427
1428        let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1429        let results_content = v_flex()
1430            .gap_1()
1431            .child(
1432                content_container()
1433                    .child(
1434                        Label::new("Input")
1435                            .size(LabelSize::XSmall)
1436                            .color(Color::Muted)
1437                            .buffer_font(cx),
1438                    )
1439                    .child(
1440                        Label::new(
1441                            serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1442                        )
1443                        .size(LabelSize::Small)
1444                        .buffer_font(cx),
1445                    ),
1446            )
1447            .map(|container| match tool_use.status {
1448                ToolUseStatus::Finished(output) => container.child(
1449                    content_container()
1450                        .border_t_1()
1451                        .border_color(self.tool_card_border_color(cx))
1452                        .child(
1453                            Label::new("Result")
1454                                .size(LabelSize::XSmall)
1455                                .color(Color::Muted)
1456                                .buffer_font(cx),
1457                        )
1458                        .child(Label::new(output).size(LabelSize::Small).buffer_font(cx)),
1459                ),
1460                ToolUseStatus::Running => container.child(
1461                    content_container().child(
1462                        h_flex()
1463                            .gap_1()
1464                            .pb_1()
1465                            .border_t_1()
1466                            .border_color(self.tool_card_border_color(cx))
1467                            .child(
1468                                Icon::new(IconName::ArrowCircle)
1469                                    .size(IconSize::Small)
1470                                    .color(Color::Accent)
1471                                    .with_animation(
1472                                        "arrow-circle",
1473                                        Animation::new(Duration::from_secs(2)).repeat(),
1474                                        |icon, delta| {
1475                                            icon.transform(Transformation::rotate(percentage(
1476                                                delta,
1477                                            )))
1478                                        },
1479                                    ),
1480                            )
1481                            .child(
1482                                Label::new("Running…")
1483                                    .size(LabelSize::XSmall)
1484                                    .color(Color::Muted)
1485                                    .buffer_font(cx),
1486                            ),
1487                    ),
1488                ),
1489                ToolUseStatus::Error(err) => container.child(
1490                    content_container()
1491                        .border_t_1()
1492                        .border_color(self.tool_card_border_color(cx))
1493                        .child(
1494                            Label::new("Error")
1495                                .size(LabelSize::XSmall)
1496                                .color(Color::Muted)
1497                                .buffer_font(cx),
1498                        )
1499                        .child(Label::new(err).size(LabelSize::Small).buffer_font(cx)),
1500                ),
1501                ToolUseStatus::Pending => container,
1502                ToolUseStatus::NeedsConfirmation => container.child(
1503                    content_container()
1504                        .border_t_1()
1505                        .border_color(self.tool_card_border_color(cx))
1506                        .child(
1507                            Label::new("Asking Permission")
1508                                .size(LabelSize::Small)
1509                                .color(Color::Muted)
1510                                .buffer_font(cx),
1511                        ),
1512                ),
1513            });
1514
1515        fn gradient_overlay(color: Hsla) -> impl IntoElement {
1516            div()
1517                .h_full()
1518                .absolute()
1519                .w_8()
1520                .bottom_0()
1521                .right_12()
1522                .bg(linear_gradient(
1523                    90.,
1524                    linear_color_stop(color, 1.),
1525                    linear_color_stop(color.opacity(0.2), 0.),
1526                ))
1527        }
1528
1529        div().map(|this| {
1530            if !tool_use.needs_confirmation {
1531                this.py_2p5().child(
1532                    v_flex()
1533                        .child(
1534                            h_flex()
1535                                .group("disclosure-header")
1536                                .relative()
1537                                .gap_1p5()
1538                                .justify_between()
1539                                .opacity(0.8)
1540                                .hover(|style| style.opacity(1.))
1541                                .pr_2()
1542                                .child(
1543                                    h_flex()
1544                                        .id("tool-label-container")
1545                                        .gap_1p5()
1546                                        .max_w_full()
1547                                        .overflow_x_scroll()
1548                                        .child(
1549                                            Icon::new(tool_use.icon)
1550                                                .size(IconSize::XSmall)
1551                                                .color(Color::Muted),
1552                                        )
1553                                        .child(
1554                                            h_flex().pr_8().text_ui_sm(cx).children(
1555                                                self.rendered_tool_use_labels
1556                                                    .get(&tool_use.id)
1557                                                    .cloned(),
1558                                            ),
1559                                        ),
1560                                )
1561                                .child(
1562                                    h_flex()
1563                                        .gap_1()
1564                                        .child(
1565                                            div().visible_on_hover("disclosure-header").child(
1566                                                Disclosure::new("tool-use-disclosure", is_open)
1567                                                    .opened_icon(IconName::ChevronUp)
1568                                                    .closed_icon(IconName::ChevronDown)
1569                                                    .on_click(cx.listener({
1570                                                        let tool_use_id = tool_use.id.clone();
1571                                                        move |this, _event, _window, _cx| {
1572                                                            let is_open = this
1573                                                                .expanded_tool_uses
1574                                                                .entry(tool_use_id.clone())
1575                                                                .or_insert(false);
1576
1577                                                            *is_open = !*is_open;
1578                                                        }
1579                                                    })),
1580                                            ),
1581                                        )
1582                                        .child(status_icons),
1583                                )
1584                                .child(gradient_overlay(cx.theme().colors().panel_background)),
1585                        )
1586                        .map(|parent| {
1587                            if !is_open {
1588                                return parent;
1589                            }
1590
1591                            parent.child(
1592                                v_flex()
1593                                    .mt_1()
1594                                    .border_1()
1595                                    .border_color(self.tool_card_border_color(cx))
1596                                    .bg(cx.theme().colors().editor_background)
1597                                    .rounded_lg()
1598                                    .child(results_content),
1599                            )
1600                        }),
1601                )
1602            } else {
1603                this.py_2().child(
1604                    v_flex()
1605                        .rounded_lg()
1606                        .border_1()
1607                        .border_color(self.tool_card_border_color(cx))
1608                        .overflow_hidden()
1609                        .child(
1610                            h_flex()
1611                                .group("disclosure-header")
1612                                .relative()
1613                                .gap_1p5()
1614                                .justify_between()
1615                                .py_1()
1616                                .px_2()
1617                                .bg(self.tool_card_header_bg(cx))
1618                                .map(|element| {
1619                                    if is_open {
1620                                        element.border_b_1().rounded_t_md()
1621                                    } else {
1622                                        element.rounded_md()
1623                                    }
1624                                })
1625                                .border_color(self.tool_card_border_color(cx))
1626                                .child(
1627                                    h_flex()
1628                                        .id("tool-label-container")
1629                                        .gap_1p5()
1630                                        .max_w_full()
1631                                        .overflow_x_scroll()
1632                                        .child(
1633                                            Icon::new(tool_use.icon)
1634                                                .size(IconSize::XSmall)
1635                                                .color(Color::Muted),
1636                                        )
1637                                        .child(
1638                                            h_flex().pr_8().text_ui_sm(cx).children(
1639                                                self.rendered_tool_use_labels
1640                                                    .get(&tool_use.id)
1641                                                    .cloned(),
1642                                            ),
1643                                        ),
1644                                )
1645                                .child(
1646                                    h_flex()
1647                                        .gap_1()
1648                                        .child(
1649                                            div().visible_on_hover("disclosure-header").child(
1650                                                Disclosure::new("tool-use-disclosure", is_open)
1651                                                    .opened_icon(IconName::ChevronUp)
1652                                                    .closed_icon(IconName::ChevronDown)
1653                                                    .on_click(cx.listener({
1654                                                        let tool_use_id = tool_use.id.clone();
1655                                                        move |this, _event, _window, _cx| {
1656                                                            let is_open = this
1657                                                                .expanded_tool_uses
1658                                                                .entry(tool_use_id.clone())
1659                                                                .or_insert(false);
1660
1661                                                            *is_open = !*is_open;
1662                                                        }
1663                                                    })),
1664                                            ),
1665                                        )
1666                                        .child(status_icons),
1667                                )
1668                                .child(gradient_overlay(self.tool_card_header_bg(cx))),
1669                        )
1670                        .map(|parent| {
1671                            if !is_open {
1672                                return parent;
1673                            }
1674
1675                            parent.child(
1676                                v_flex()
1677                                    .bg(cx.theme().colors().editor_background)
1678                                    .rounded_b_lg()
1679                                    .child(results_content),
1680                            )
1681                        }),
1682                )
1683            }
1684        })
1685    }
1686
1687    fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1688        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1689        else {
1690            return div().into_any();
1691        };
1692
1693        let rules_files = system_prompt_context
1694            .worktrees
1695            .iter()
1696            .filter_map(|worktree| worktree.rules_file.as_ref())
1697            .collect::<Vec<_>>();
1698
1699        let label_text = match rules_files.as_slice() {
1700            &[] => return div().into_any(),
1701            &[rules_file] => {
1702                format!("Using {:?} file", rules_file.rel_path)
1703            }
1704            rules_files => {
1705                format!("Using {} rules files", rules_files.len())
1706            }
1707        };
1708
1709        div()
1710            .pt_1()
1711            .px_2p5()
1712            .child(
1713                h_flex()
1714                    .w_full()
1715                    .gap_0p5()
1716                    .child(
1717                        h_flex()
1718                            .gap_1p5()
1719                            .child(
1720                                Icon::new(IconName::File)
1721                                    .size(IconSize::XSmall)
1722                                    .color(Color::Disabled),
1723                            )
1724                            .child(
1725                                Label::new(label_text)
1726                                    .size(LabelSize::XSmall)
1727                                    .color(Color::Muted)
1728                                    .buffer_font(cx),
1729                            ),
1730                    )
1731                    .child(
1732                        IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1733                            .shape(ui::IconButtonShape::Square)
1734                            .icon_size(IconSize::XSmall)
1735                            .icon_color(Color::Ignored)
1736                            .on_click(cx.listener(Self::handle_open_rules))
1737                            .tooltip(Tooltip::text("View Rules")),
1738                    ),
1739            )
1740            .into_any()
1741    }
1742
1743    fn handle_allow_tool(
1744        &mut self,
1745        tool_use_id: LanguageModelToolUseId,
1746        _: &ClickEvent,
1747        _window: &mut Window,
1748        cx: &mut Context<Self>,
1749    ) {
1750        if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
1751            .thread
1752            .read(cx)
1753            .pending_tool(&tool_use_id)
1754            .map(|tool_use| tool_use.status.clone())
1755        {
1756            self.thread.update(cx, |thread, cx| {
1757                thread.run_tool(
1758                    c.tool_use_id.clone(),
1759                    c.ui_text.clone(),
1760                    c.input.clone(),
1761                    &c.messages,
1762                    c.tool.clone(),
1763                    cx,
1764                );
1765            });
1766        }
1767    }
1768
1769    fn handle_deny_tool(
1770        &mut self,
1771        tool_use_id: LanguageModelToolUseId,
1772        _: &ClickEvent,
1773        _window: &mut Window,
1774        cx: &mut Context<Self>,
1775    ) {
1776        self.thread.update(cx, |thread, cx| {
1777            thread.deny_tool_use(tool_use_id, cx);
1778        });
1779    }
1780
1781    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1782        let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1783        else {
1784            return;
1785        };
1786
1787        let abs_paths = system_prompt_context
1788            .worktrees
1789            .iter()
1790            .flat_map(|worktree| worktree.rules_file.as_ref())
1791            .map(|rules_file| rules_file.abs_path.to_path_buf())
1792            .collect::<Vec<_>>();
1793
1794        if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1795            // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1796            // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1797            // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1798            workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1799        }) {
1800            task.detach();
1801        }
1802    }
1803
1804    fn render_confirmations<'a>(
1805        &'a mut self,
1806        cx: &'a mut Context<Self>,
1807    ) -> impl Iterator<Item = AnyElement> + 'a {
1808        let thread = self.thread.read(cx);
1809
1810        thread
1811            .tools_needing_confirmation()
1812            .map(|tool| {
1813                div()
1814                    .m_3()
1815                    .p_2()
1816                    .bg(cx.theme().colors().editor_background)
1817                    .border_1()
1818                    .border_color(cx.theme().colors().border)
1819                    .rounded_lg()
1820                    .child(
1821                        v_flex()
1822                            .gap_1()
1823                            .child(
1824                                v_flex()
1825                                    .gap_0p5()
1826                                    .child(
1827                                        Label::new("The agent wants to run this action:")
1828                                            .color(Color::Muted),
1829                                    )
1830                                    .child(div().p_3().child(Label::new(&tool.ui_text))),
1831                            )
1832                            .child(
1833                                h_flex()
1834                                    .gap_1()
1835                                    .child({
1836                                        let tool_id = tool.id.clone();
1837                                        Button::new("allow-tool-action", "Allow").on_click(
1838                                            cx.listener(move |this, event, window, cx| {
1839                                                this.handle_allow_tool(
1840                                                    tool_id.clone(),
1841                                                    event,
1842                                                    window,
1843                                                    cx,
1844                                                )
1845                                            }),
1846                                        )
1847                                    })
1848                                    .child({
1849                                        let tool_id = tool.id.clone();
1850                                        Button::new("deny-tool", "Deny").on_click(cx.listener(
1851                                            move |this, event, window, cx| {
1852                                                this.handle_deny_tool(
1853                                                    tool_id.clone(),
1854                                                    event,
1855                                                    window,
1856                                                    cx,
1857                                                )
1858                                            },
1859                                        ))
1860                                    }),
1861                            )
1862                            .child(
1863                                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.")
1864                                    .color(Color::Muted)
1865                                    .size(LabelSize::Small),
1866                            ),
1867                    )
1868                    .into_any()
1869            })
1870    }
1871
1872    fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
1873        for window in self.notifications.drain(..) {
1874            window
1875                .update(cx, |_, window, _| {
1876                    window.remove_window();
1877                })
1878                .ok();
1879
1880            self.notification_subscriptions.remove(&window);
1881        }
1882    }
1883
1884    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
1885        div()
1886            .occlude()
1887            .id("active-thread-scrollbar")
1888            .on_mouse_move(cx.listener(|_, _, _, cx| {
1889                cx.notify();
1890                cx.stop_propagation()
1891            }))
1892            .on_hover(|_, _, cx| {
1893                cx.stop_propagation();
1894            })
1895            .on_any_mouse_down(|_, _, cx| {
1896                cx.stop_propagation();
1897            })
1898            .on_mouse_up(
1899                MouseButton::Left,
1900                cx.listener(|_, _, _, cx| {
1901                    cx.stop_propagation();
1902                }),
1903            )
1904            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
1905                cx.notify();
1906            }))
1907            .h_full()
1908            .absolute()
1909            .right_1()
1910            .top_1()
1911            .bottom_0()
1912            .w(px(12.))
1913            .cursor_default()
1914            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
1915    }
1916}
1917
1918impl Render for ActiveThread {
1919    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1920        v_flex()
1921            .size_full()
1922            .relative()
1923            .child(list(self.list_state.clone()).flex_grow())
1924            .children(self.render_confirmations(cx))
1925            .child(self.render_vertical_scrollbar(cx))
1926    }
1927}
1928
1929pub(crate) fn open_context(
1930    id: ContextId,
1931    context_store: Entity<ContextStore>,
1932    workspace: Entity<Workspace>,
1933    window: &mut Window,
1934    cx: &mut App,
1935) {
1936    let Some(context) = context_store.read(cx).context_for_id(id) else {
1937        return;
1938    };
1939
1940    match context {
1941        AssistantContext::File(file_context) => {
1942            if let Some(project_path) = file_context.context_buffer.buffer.read(cx).project_path(cx)
1943            {
1944                workspace.update(cx, |workspace, cx| {
1945                    workspace
1946                        .open_path(project_path, None, true, window, cx)
1947                        .detach_and_log_err(cx);
1948                });
1949            }
1950        }
1951        AssistantContext::Directory(directory_context) => {
1952            let path = directory_context.path.clone();
1953            workspace.update(cx, |workspace, cx| {
1954                workspace.project().update(cx, |project, cx| {
1955                    if let Some(entry) = project.entry_for_path(&path, cx) {
1956                        cx.emit(project::Event::RevealInProjectPanel(entry.id));
1957                    }
1958                })
1959            })
1960        }
1961        AssistantContext::Symbol(symbol_context) => {
1962            if let Some(project_path) = symbol_context
1963                .context_symbol
1964                .buffer
1965                .read(cx)
1966                .project_path(cx)
1967            {
1968                let snapshot = symbol_context.context_symbol.buffer.read(cx).snapshot();
1969                let target_position = symbol_context
1970                    .context_symbol
1971                    .id
1972                    .range
1973                    .start
1974                    .to_point(&snapshot);
1975
1976                let open_task = workspace.update(cx, |workspace, cx| {
1977                    workspace.open_path(project_path, None, true, window, cx)
1978                });
1979                window
1980                    .spawn(cx, async move |cx| {
1981                        if let Some(active_editor) = open_task
1982                            .await
1983                            .log_err()
1984                            .and_then(|item| item.downcast::<Editor>())
1985                        {
1986                            active_editor
1987                                .downgrade()
1988                                .update_in(cx, |editor, window, cx| {
1989                                    editor.go_to_singleton_buffer_point(
1990                                        target_position,
1991                                        window,
1992                                        cx,
1993                                    );
1994                                })
1995                                .log_err();
1996                        }
1997                    })
1998                    .detach();
1999            }
2000        }
2001        AssistantContext::FetchedUrl(fetched_url_context) => {
2002            cx.open_url(&fetched_url_context.url);
2003        }
2004        AssistantContext::Thread(thread_context) => {
2005            let thread_id = thread_context.thread.read(cx).id().clone();
2006            workspace.update(cx, |workspace, cx| {
2007                if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
2008                    panel.update(cx, |panel, cx| {
2009                        panel
2010                            .open_thread(&thread_id, window, cx)
2011                            .detach_and_log_err(cx)
2012                    });
2013                }
2014            })
2015        }
2016    }
2017}