active_thread.rs

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