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