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