active_thread.rs

   1use crate::context::{AgentContextHandle, RULES_ICON};
   2use crate::context_picker::MentionLink;
   3use crate::thread::{
   4    LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
   5    ThreadFeedback,
   6};
   7use crate::thread_store::{RulesLoadingError, ThreadStore};
   8use crate::tool_use::{PendingToolUseStatus, ToolUse};
   9use crate::ui::{
  10    AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
  11};
  12use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
  13use anyhow::Context as _;
  14use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
  15use assistant_tool::ToolUseStatus;
  16use collections::{HashMap, HashSet};
  17use editor::scroll::Autoscroll;
  18use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
  19use gpui::{
  20    AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardItem,
  21    DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, ListAlignment,
  22    ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription,
  23    Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
  24    linear_color_stop, linear_gradient, list, percentage, pulsating_between,
  25};
  26use language::{Buffer, Language, LanguageRegistry};
  27use language_model::{
  28    LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, RequestUsage, Role,
  29    StopReason,
  30};
  31use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
  32use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
  33use project::{ProjectEntryId, ProjectItem as _};
  34use rope::Point;
  35use settings::{Settings as _, update_settings_file};
  36use std::ffi::OsStr;
  37use std::path::Path;
  38use std::rc::Rc;
  39use std::sync::Arc;
  40use std::time::Duration;
  41use text::ToPoint;
  42use theme::ThemeSettings;
  43use ui::{
  44    Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, TextSize, Tooltip, prelude::*,
  45};
  46use util::ResultExt as _;
  47use util::markdown::MarkdownCodeBlock;
  48use workspace::Workspace;
  49use zed_actions::assistant::OpenRulesLibrary;
  50
  51pub struct ActiveThread {
  52    language_registry: Arc<LanguageRegistry>,
  53    thread_store: Entity<ThreadStore>,
  54    thread: Entity<Thread>,
  55    workspace: WeakEntity<Workspace>,
  56    save_thread_task: Option<Task<()>>,
  57    messages: Vec<MessageId>,
  58    list_state: ListState,
  59    scrollbar_state: ScrollbarState,
  60    show_scrollbar: bool,
  61    hide_scrollbar_task: Option<Task<()>>,
  62    rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
  63    rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,
  64    editing_message: Option<(MessageId, EditMessageState)>,
  65    expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
  66    expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
  67    expanded_code_blocks: HashMap<(MessageId, usize), bool>,
  68    last_error: Option<ThreadError>,
  69    last_usage: Option<RequestUsage>,
  70    notifications: Vec<WindowHandle<AgentNotification>>,
  71    copied_code_block_ids: HashSet<(MessageId, usize)>,
  72    _subscriptions: Vec<Subscription>,
  73    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
  74    open_feedback_editors: HashMap<MessageId, Entity<Editor>>,
  75}
  76
  77struct RenderedMessage {
  78    language_registry: Arc<LanguageRegistry>,
  79    segments: Vec<RenderedMessageSegment>,
  80}
  81
  82#[derive(Clone)]
  83struct RenderedToolUse {
  84    label: Entity<Markdown>,
  85    input: Entity<Markdown>,
  86    output: Entity<Markdown>,
  87}
  88
  89impl RenderedMessage {
  90    fn from_segments(
  91        segments: &[MessageSegment],
  92        language_registry: Arc<LanguageRegistry>,
  93        cx: &mut App,
  94    ) -> Self {
  95        let mut this = Self {
  96            language_registry,
  97            segments: Vec::with_capacity(segments.len()),
  98        };
  99        for segment in segments {
 100            this.push_segment(segment, cx);
 101        }
 102        this
 103    }
 104
 105    fn append_thinking(&mut self, text: &String, cx: &mut App) {
 106        if let Some(RenderedMessageSegment::Thinking {
 107            content,
 108            scroll_handle,
 109        }) = self.segments.last_mut()
 110        {
 111            content.update(cx, |markdown, cx| {
 112                markdown.append(text, cx);
 113            });
 114            scroll_handle.scroll_to_bottom();
 115        } else {
 116            self.segments.push(RenderedMessageSegment::Thinking {
 117                content: parse_markdown(text.into(), self.language_registry.clone(), cx),
 118                scroll_handle: ScrollHandle::default(),
 119            });
 120        }
 121    }
 122
 123    fn append_text(&mut self, text: &String, cx: &mut App) {
 124        if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {
 125            markdown.update(cx, |markdown, cx| markdown.append(text, cx));
 126        } else {
 127            self.segments
 128                .push(RenderedMessageSegment::Text(parse_markdown(
 129                    SharedString::from(text),
 130                    self.language_registry.clone(),
 131                    cx,
 132                )));
 133        }
 134    }
 135
 136    fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {
 137        match segment {
 138            MessageSegment::Thinking { text, .. } => {
 139                self.segments.push(RenderedMessageSegment::Thinking {
 140                    content: parse_markdown(text.into(), self.language_registry.clone(), cx),
 141                    scroll_handle: ScrollHandle::default(),
 142                })
 143            }
 144            MessageSegment::Text(text) => {
 145                self.segments
 146                    .push(RenderedMessageSegment::Text(parse_markdown(
 147                        text.into(),
 148                        self.language_registry.clone(),
 149                        cx,
 150                    )))
 151            }
 152            MessageSegment::RedactedThinking(_) => {}
 153        };
 154    }
 155}
 156
 157enum RenderedMessageSegment {
 158    Thinking {
 159        content: Entity<Markdown>,
 160        scroll_handle: ScrollHandle,
 161    },
 162    Text(Entity<Markdown>),
 163}
 164
 165fn parse_markdown(
 166    text: SharedString,
 167    language_registry: Arc<LanguageRegistry>,
 168    cx: &mut App,
 169) -> Entity<Markdown> {
 170    cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))
 171}
 172
 173fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 174    let theme_settings = ThemeSettings::get_global(cx);
 175    let colors = cx.theme().colors();
 176    let ui_font_size = TextSize::Default.rems(cx);
 177    let buffer_font_size = TextSize::Small.rems(cx);
 178    let mut text_style = window.text_style();
 179
 180    text_style.refine(&TextStyleRefinement {
 181        font_family: Some(theme_settings.ui_font.family.clone()),
 182        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
 183        font_features: Some(theme_settings.ui_font.features.clone()),
 184        font_size: Some(ui_font_size.into()),
 185        color: Some(cx.theme().colors().text),
 186        ..Default::default()
 187    });
 188
 189    MarkdownStyle {
 190        base_text_style: text_style.clone(),
 191        syntax: cx.theme().syntax().clone(),
 192        selection_background_color: cx.theme().players().local().selection,
 193        code_block_overflow_x_scroll: true,
 194        table_overflow_x_scroll: true,
 195        heading_level_styles: Some(HeadingLevelStyles {
 196            h1: Some(TextStyleRefinement {
 197                font_size: Some(rems(1.15).into()),
 198                ..Default::default()
 199            }),
 200            h2: Some(TextStyleRefinement {
 201                font_size: Some(rems(1.1).into()),
 202                ..Default::default()
 203            }),
 204            h3: Some(TextStyleRefinement {
 205                font_size: Some(rems(1.05).into()),
 206                ..Default::default()
 207            }),
 208            h4: Some(TextStyleRefinement {
 209                font_size: Some(rems(1.).into()),
 210                ..Default::default()
 211            }),
 212            h5: Some(TextStyleRefinement {
 213                font_size: Some(rems(0.95).into()),
 214                ..Default::default()
 215            }),
 216            h6: Some(TextStyleRefinement {
 217                font_size: Some(rems(0.875).into()),
 218                ..Default::default()
 219            }),
 220        }),
 221        code_block: StyleRefinement {
 222            padding: EdgesRefinement {
 223                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 224                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 225                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 226                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
 227            },
 228            background: Some(colors.editor_background.into()),
 229            text: Some(TextStyleRefinement {
 230                font_family: Some(theme_settings.buffer_font.family.clone()),
 231                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 232                font_features: Some(theme_settings.buffer_font.features.clone()),
 233                font_size: Some(buffer_font_size.into()),
 234                ..Default::default()
 235            }),
 236            ..Default::default()
 237        },
 238        inline_code: TextStyleRefinement {
 239            font_family: Some(theme_settings.buffer_font.family.clone()),
 240            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 241            font_features: Some(theme_settings.buffer_font.features.clone()),
 242            font_size: Some(buffer_font_size.into()),
 243            background_color: Some(colors.editor_foreground.opacity(0.08)),
 244            ..Default::default()
 245        },
 246        link: TextStyleRefinement {
 247            background_color: Some(colors.editor_foreground.opacity(0.025)),
 248            underline: Some(UnderlineStyle {
 249                color: Some(colors.text_accent.opacity(0.5)),
 250                thickness: px(1.),
 251                ..Default::default()
 252            }),
 253            ..Default::default()
 254        },
 255        link_callback: Some(Rc::new(move |url, cx| {
 256            if MentionLink::is_valid(url) {
 257                let colors = cx.theme().colors();
 258                Some(TextStyleRefinement {
 259                    background_color: Some(colors.element_background),
 260                    ..Default::default()
 261                })
 262            } else {
 263                None
 264            }
 265        })),
 266        ..Default::default()
 267    }
 268}
 269
 270fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
 271    let theme_settings = ThemeSettings::get_global(cx);
 272    let colors = cx.theme().colors();
 273    let ui_font_size = TextSize::Default.rems(cx);
 274    let buffer_font_size = TextSize::Small.rems(cx);
 275    let mut text_style = window.text_style();
 276
 277    text_style.refine(&TextStyleRefinement {
 278        font_family: Some(theme_settings.ui_font.family.clone()),
 279        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
 280        font_features: Some(theme_settings.ui_font.features.clone()),
 281        font_size: Some(ui_font_size.into()),
 282        color: Some(cx.theme().colors().text),
 283        ..Default::default()
 284    });
 285
 286    MarkdownStyle {
 287        base_text_style: text_style,
 288        syntax: cx.theme().syntax().clone(),
 289        selection_background_color: cx.theme().players().local().selection,
 290        code_block_overflow_x_scroll: true,
 291        code_block: StyleRefinement {
 292            margin: EdgesRefinement::default(),
 293            padding: EdgesRefinement::default(),
 294            background: Some(colors.editor_background.into()),
 295            border_color: None,
 296            border_widths: EdgesRefinement::default(),
 297            text: Some(TextStyleRefinement {
 298                font_family: Some(theme_settings.buffer_font.family.clone()),
 299                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 300                font_features: Some(theme_settings.buffer_font.features.clone()),
 301                font_size: Some(buffer_font_size.into()),
 302                ..Default::default()
 303            }),
 304            ..Default::default()
 305        },
 306        inline_code: TextStyleRefinement {
 307            font_family: Some(theme_settings.buffer_font.family.clone()),
 308            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 309            font_features: Some(theme_settings.buffer_font.features.clone()),
 310            font_size: Some(TextSize::XSmall.rems(cx).into()),
 311            ..Default::default()
 312        },
 313        heading: StyleRefinement {
 314            text: Some(TextStyleRefinement {
 315                font_size: Some(ui_font_size.into()),
 316                ..Default::default()
 317            }),
 318            ..Default::default()
 319        },
 320        ..Default::default()
 321    }
 322}
 323
 324const MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK: usize = 10;
 325
 326fn render_markdown_code_block(
 327    message_id: MessageId,
 328    ix: usize,
 329    kind: &CodeBlockKind,
 330    parsed_markdown: &ParsedMarkdown,
 331    metadata: CodeBlockMetadata,
 332    active_thread: Entity<ActiveThread>,
 333    workspace: WeakEntity<Workspace>,
 334    _window: &Window,
 335    cx: &App,
 336) -> Div {
 337    let label = match kind {
 338        CodeBlockKind::Indented => None,
 339        CodeBlockKind::Fenced => Some(
 340            h_flex()
 341                .gap_1()
 342                .child(
 343                    Icon::new(IconName::Code)
 344                        .color(Color::Muted)
 345                        .size(IconSize::XSmall),
 346                )
 347                .child(Label::new("untitled").size(LabelSize::Small))
 348                .into_any_element(),
 349        ),
 350        CodeBlockKind::FencedLang(raw_language_name) => Some(render_code_language(
 351            parsed_markdown.languages_by_name.get(raw_language_name),
 352            raw_language_name.clone(),
 353            cx,
 354        )),
 355        CodeBlockKind::FencedSrc(path_range) => path_range.path.file_name().map(|file_name| {
 356            // We tell the model to use /dev/null for the path instead of using ```language
 357            // because otherwise it consistently fails to use code citations.
 358            if path_range.path.starts_with("/dev/null") {
 359                let ext = path_range
 360                    .path
 361                    .extension()
 362                    .and_then(OsStr::to_str)
 363                    .map(|str| SharedString::new(str.to_string()))
 364                    .unwrap_or_default();
 365
 366                render_code_language(
 367                    parsed_markdown
 368                        .languages_by_path
 369                        .get(&path_range.path)
 370                        .or_else(|| parsed_markdown.languages_by_name.get(&ext)),
 371                    ext,
 372                    cx,
 373                )
 374            } else {
 375                let content = if let Some(parent) = path_range.path.parent() {
 376                    h_flex()
 377                        .ml_1()
 378                        .gap_1()
 379                        .child(
 380                            Label::new(file_name.to_string_lossy().to_string())
 381                                .size(LabelSize::Small),
 382                        )
 383                        .child(
 384                            Label::new(parent.to_string_lossy().to_string())
 385                                .color(Color::Muted)
 386                                .size(LabelSize::Small),
 387                        )
 388                        .into_any_element()
 389                } else {
 390                    Label::new(path_range.path.to_string_lossy().to_string())
 391                        .size(LabelSize::Small)
 392                        .ml_1()
 393                        .into_any_element()
 394                };
 395
 396                h_flex()
 397                    .id(("code-block-header-label", ix))
 398                    .w_full()
 399                    .max_w_full()
 400                    .px_1()
 401                    .gap_0p5()
 402                    .cursor_pointer()
 403                    .rounded_sm()
 404                    .hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
 405                    .tooltip(Tooltip::text("Jump to File"))
 406                    .child(
 407                        h_flex()
 408                            .gap_0p5()
 409                            .children(
 410                                file_icons::FileIcons::get_icon(&path_range.path, cx)
 411                                    .map(Icon::from_path)
 412                                    .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
 413                            )
 414                            .child(content)
 415                            .child(
 416                                Icon::new(IconName::ArrowUpRight)
 417                                    .size(IconSize::XSmall)
 418                                    .color(Color::Ignored),
 419                            ),
 420                    )
 421                    .on_click({
 422                        let path_range = path_range.clone();
 423                        move |_, window, cx| {
 424                            workspace
 425                                .update(cx, {
 426                                    |workspace, cx| {
 427                                        let Some(project_path) = workspace
 428                                            .project()
 429                                            .read(cx)
 430                                            .find_project_path(&path_range.path, cx)
 431                                        else {
 432                                            return;
 433                                        };
 434                                        let Some(target) = path_range.range.as_ref().map(|range| {
 435                                            Point::new(
 436                                                // Line number is 1-based
 437                                                range.start.line.saturating_sub(1),
 438                                                range.start.col.unwrap_or(0),
 439                                            )
 440                                        }) else {
 441                                            return;
 442                                        };
 443                                        let open_task = workspace.open_path(
 444                                            project_path,
 445                                            None,
 446                                            true,
 447                                            window,
 448                                            cx,
 449                                        );
 450                                        window
 451                                            .spawn(cx, async move |cx| {
 452                                                let item = open_task.await?;
 453                                                if let Some(active_editor) =
 454                                                    item.downcast::<Editor>()
 455                                                {
 456                                                    active_editor
 457                                                        .update_in(cx, |editor, window, cx| {
 458                                                            editor.go_to_singleton_buffer_point(
 459                                                                target, window, cx,
 460                                                            );
 461                                                        })
 462                                                        .ok();
 463                                                }
 464                                                anyhow::Ok(())
 465                                            })
 466                                            .detach_and_log_err(cx);
 467                                    }
 468                                })
 469                                .ok();
 470                        }
 471                    })
 472                    .into_any_element()
 473            }
 474        }),
 475    };
 476
 477    let codeblock_was_copied = active_thread
 478        .read(cx)
 479        .copied_code_block_ids
 480        .contains(&(message_id, ix));
 481
 482    let is_expanded = active_thread
 483        .read(cx)
 484        .expanded_code_blocks
 485        .get(&(message_id, ix))
 486        .copied()
 487        .unwrap_or(true);
 488
 489    let codeblock_header_bg = cx
 490        .theme()
 491        .colors()
 492        .element_background
 493        .blend(cx.theme().colors().editor_foreground.opacity(0.01));
 494
 495    let codeblock_header = h_flex()
 496        .py_1()
 497        .pl_1p5()
 498        .pr_1()
 499        .gap_1()
 500        .justify_between()
 501        .border_b_1()
 502        .border_color(cx.theme().colors().border.opacity(0.6))
 503        .bg(codeblock_header_bg)
 504        .rounded_t_md()
 505        .children(label)
 506        .child(
 507            h_flex()
 508                .visible_on_hover("codeblock_container")
 509                .gap_1()
 510                .child(
 511                    IconButton::new(
 512                        ("copy-markdown-code", ix),
 513                        if codeblock_was_copied {
 514                            IconName::Check
 515                        } else {
 516                            IconName::Copy
 517                        },
 518                    )
 519                    .icon_color(Color::Muted)
 520                    .shape(ui::IconButtonShape::Square)
 521                    .tooltip(Tooltip::text("Copy Code"))
 522                    .on_click({
 523                        let active_thread = active_thread.clone();
 524                        let parsed_markdown = parsed_markdown.clone();
 525                        let code_block_range = metadata.content_range.clone();
 526                        move |_event, _window, cx| {
 527                            active_thread.update(cx, |this, cx| {
 528                                this.copied_code_block_ids.insert((message_id, ix));
 529
 530                                let code =
 531                                    parsed_markdown.source()[code_block_range.clone()].to_string();
 532                                cx.write_to_clipboard(ClipboardItem::new_string(code));
 533
 534                                cx.spawn(async move |this, cx| {
 535                                    cx.background_executor().timer(Duration::from_secs(2)).await;
 536
 537                                    cx.update(|cx| {
 538                                        this.update(cx, |this, cx| {
 539                                            this.copied_code_block_ids.remove(&(message_id, ix));
 540                                            cx.notify();
 541                                        })
 542                                    })
 543                                    .ok();
 544                                })
 545                                .detach();
 546                            });
 547                        }
 548                    }),
 549                )
 550                .when(
 551                    metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK,
 552                    |header| {
 553                        header.child(
 554                            IconButton::new(
 555                                ("expand-collapse-code", ix),
 556                                if is_expanded {
 557                                    IconName::ChevronUp
 558                                } else {
 559                                    IconName::ChevronDown
 560                                },
 561                            )
 562                            .icon_color(Color::Muted)
 563                            .shape(ui::IconButtonShape::Square)
 564                            .tooltip(Tooltip::text(if is_expanded {
 565                                "Collapse Code"
 566                            } else {
 567                                "Expand Code"
 568                            }))
 569                            .on_click({
 570                                let active_thread = active_thread.clone();
 571                                move |_event, _window, cx| {
 572                                    active_thread.update(cx, |this, cx| {
 573                                        let is_expanded = this
 574                                            .expanded_code_blocks
 575                                            .entry((message_id, ix))
 576                                            .or_insert(true);
 577                                        *is_expanded = !*is_expanded;
 578                                        cx.notify();
 579                                    });
 580                                }
 581                            }),
 582                        )
 583                    },
 584                ),
 585        );
 586
 587    v_flex()
 588        .group("codeblock_container")
 589        .my_2()
 590        .overflow_hidden()
 591        .rounded_lg()
 592        .border_1()
 593        .border_color(cx.theme().colors().border.opacity(0.6))
 594        .bg(cx.theme().colors().editor_background)
 595        .child(codeblock_header)
 596        .when(
 597            metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK,
 598            |this| {
 599                if is_expanded {
 600                    this.h_full()
 601                } else {
 602                    this.max_h_80()
 603                }
 604            },
 605        )
 606}
 607
 608fn render_code_language(
 609    language: Option<&Arc<Language>>,
 610    name_fallback: SharedString,
 611    cx: &App,
 612) -> AnyElement {
 613    let icon_path = language.and_then(|language| {
 614        language
 615            .config()
 616            .matcher
 617            .path_suffixes
 618            .iter()
 619            .find_map(|extension| file_icons::FileIcons::get_icon(Path::new(extension), cx))
 620            .map(Icon::from_path)
 621    });
 622
 623    let language_label = language
 624        .map(|language| language.name().into())
 625        .unwrap_or(name_fallback);
 626
 627    h_flex()
 628        .gap_1()
 629        .children(icon_path.map(|icon| icon.color(Color::Muted).size(IconSize::Small)))
 630        .child(Label::new(language_label).size(LabelSize::Small))
 631        .into_any_element()
 632}
 633
 634fn open_markdown_link(
 635    text: SharedString,
 636    workspace: WeakEntity<Workspace>,
 637    window: &mut Window,
 638    cx: &mut App,
 639) {
 640    let Some(workspace) = workspace.upgrade() else {
 641        cx.open_url(&text);
 642        return;
 643    };
 644
 645    match MentionLink::try_parse(&text, &workspace, cx) {
 646        Some(MentionLink::File(path, entry)) => workspace.update(cx, |workspace, cx| {
 647            if entry.is_dir() {
 648                workspace.project().update(cx, |_, cx| {
 649                    cx.emit(project::Event::RevealInProjectPanel(entry.id));
 650                })
 651            } else {
 652                workspace
 653                    .open_path(path, None, true, window, cx)
 654                    .detach_and_log_err(cx);
 655            }
 656        }),
 657        Some(MentionLink::Symbol(path, symbol_name)) => {
 658            let open_task = workspace.update(cx, |workspace, cx| {
 659                workspace.open_path(path, None, true, window, cx)
 660            });
 661            window
 662                .spawn(cx, async move |cx| {
 663                    let active_editor = open_task
 664                        .await?
 665                        .downcast::<Editor>()
 666                        .context("Item is not an editor")?;
 667                    active_editor.update_in(cx, |editor, window, cx| {
 668                        let symbol_range = editor
 669                            .buffer()
 670                            .read(cx)
 671                            .snapshot(cx)
 672                            .outline(None)
 673                            .and_then(|outline| {
 674                                outline
 675                                    .find_most_similar(&symbol_name)
 676                                    .map(|(_, item)| item.range.clone())
 677                            })
 678                            .context("Could not find matching symbol")?;
 679
 680                        editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
 681                            s.select_anchor_ranges([symbol_range.start..symbol_range.start])
 682                        });
 683                        anyhow::Ok(())
 684                    })
 685                })
 686                .detach_and_log_err(cx);
 687        }
 688        Some(MentionLink::Selection(path, line_range)) => {
 689            let open_task = workspace.update(cx, |workspace, cx| {
 690                workspace.open_path(path, None, true, window, cx)
 691            });
 692            window
 693                .spawn(cx, async move |cx| {
 694                    let active_editor = open_task
 695                        .await?
 696                        .downcast::<Editor>()
 697                        .context("Item is not an editor")?;
 698                    active_editor.update_in(cx, |editor, window, cx| {
 699                        editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
 700                            s.select_ranges([Point::new(line_range.start as u32, 0)
 701                                ..Point::new(line_range.start as u32, 0)])
 702                        });
 703                        anyhow::Ok(())
 704                    })
 705                })
 706                .detach_and_log_err(cx);
 707        }
 708        Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
 709            if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 710                panel.update(cx, |panel, cx| {
 711                    panel
 712                        .open_thread_by_id(&thread_id, window, cx)
 713                        .detach_and_log_err(cx)
 714                });
 715            }
 716        }),
 717        Some(MentionLink::Fetch(url)) => cx.open_url(&url),
 718        Some(MentionLink::Rules(prompt_id)) => window.dispatch_action(
 719            Box::new(OpenRulesLibrary {
 720                prompt_to_select: Some(prompt_id.0),
 721            }),
 722            cx,
 723        ),
 724        None => cx.open_url(&text),
 725    }
 726}
 727
 728struct EditMessageState {
 729    editor: Entity<Editor>,
 730    last_estimated_token_count: Option<usize>,
 731    _subscription: Subscription,
 732    _update_token_count_task: Option<Task<()>>,
 733}
 734
 735impl ActiveThread {
 736    pub fn new(
 737        thread: Entity<Thread>,
 738        thread_store: Entity<ThreadStore>,
 739        language_registry: Arc<LanguageRegistry>,
 740        workspace: WeakEntity<Workspace>,
 741        window: &mut Window,
 742        cx: &mut Context<Self>,
 743    ) -> Self {
 744        let subscriptions = vec![
 745            cx.observe(&thread, |_, _, cx| cx.notify()),
 746            cx.subscribe_in(&thread, window, Self::handle_thread_event),
 747            cx.subscribe(&thread_store, Self::handle_rules_loading_error),
 748        ];
 749
 750        let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
 751            let this = cx.entity().downgrade();
 752            move |ix, window: &mut Window, cx: &mut App| {
 753                this.update(cx, |this, cx| this.render_message(ix, window, cx))
 754                    .unwrap()
 755            }
 756        });
 757
 758        let mut this = Self {
 759            language_registry,
 760            thread_store,
 761            thread: thread.clone(),
 762            workspace,
 763            save_thread_task: None,
 764            messages: Vec::new(),
 765            rendered_messages_by_id: HashMap::default(),
 766            rendered_tool_uses: HashMap::default(),
 767            expanded_tool_uses: HashMap::default(),
 768            expanded_thinking_segments: HashMap::default(),
 769            expanded_code_blocks: HashMap::default(),
 770            list_state: list_state.clone(),
 771            scrollbar_state: ScrollbarState::new(list_state),
 772            show_scrollbar: false,
 773            hide_scrollbar_task: None,
 774            editing_message: None,
 775            last_error: None,
 776            last_usage: None,
 777            copied_code_block_ids: HashSet::default(),
 778            notifications: Vec::new(),
 779            _subscriptions: subscriptions,
 780            notification_subscriptions: HashMap::default(),
 781            open_feedback_editors: HashMap::default(),
 782        };
 783
 784        for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
 785            this.push_message(&message.id, &message.segments, window, cx);
 786
 787            for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
 788                this.render_tool_use_markdown(
 789                    tool_use.id.clone(),
 790                    tool_use.ui_text.clone(),
 791                    &serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
 792                    tool_use.status.text(),
 793                    cx,
 794                );
 795            }
 796        }
 797
 798        this
 799    }
 800
 801    pub fn thread(&self) -> &Entity<Thread> {
 802        &self.thread
 803    }
 804
 805    pub fn is_empty(&self) -> bool {
 806        self.messages.is_empty()
 807    }
 808
 809    pub fn summary(&self, cx: &App) -> Option<SharedString> {
 810        self.thread.read(cx).summary()
 811    }
 812
 813    pub fn summary_or_default(&self, cx: &App) -> SharedString {
 814        self.thread.read(cx).summary_or_default()
 815    }
 816
 817    pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool {
 818        self.last_error.take();
 819        self.thread.update(cx, |thread, cx| {
 820            thread.cancel_last_completion(Some(window.window_handle()), cx)
 821        })
 822    }
 823
 824    pub fn last_error(&self) -> Option<ThreadError> {
 825        self.last_error.clone()
 826    }
 827
 828    pub fn clear_last_error(&mut self) {
 829        self.last_error.take();
 830    }
 831
 832    pub fn last_usage(&self) -> Option<RequestUsage> {
 833        self.last_usage
 834    }
 835
 836    /// Returns the editing message id and the estimated token count in the content
 837    pub fn editing_message_id(&self) -> Option<(MessageId, usize)> {
 838        self.editing_message
 839            .as_ref()
 840            .map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
 841    }
 842
 843    fn push_message(
 844        &mut self,
 845        id: &MessageId,
 846        segments: &[MessageSegment],
 847        _window: &mut Window,
 848        cx: &mut Context<Self>,
 849    ) {
 850        let old_len = self.messages.len();
 851        self.messages.push(*id);
 852        self.list_state.splice(old_len..old_len, 1);
 853
 854        let rendered_message =
 855            RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
 856        self.rendered_messages_by_id.insert(*id, rendered_message);
 857    }
 858
 859    fn edited_message(
 860        &mut self,
 861        id: &MessageId,
 862        segments: &[MessageSegment],
 863        _window: &mut Window,
 864        cx: &mut Context<Self>,
 865    ) {
 866        let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
 867            return;
 868        };
 869        self.list_state.splice(index..index + 1, 1);
 870        let rendered_message =
 871            RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
 872        self.rendered_messages_by_id.insert(*id, rendered_message);
 873    }
 874
 875    fn deleted_message(&mut self, id: &MessageId) {
 876        let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
 877            return;
 878        };
 879        self.messages.remove(index);
 880        self.list_state.splice(index..index + 1, 0);
 881        self.rendered_messages_by_id.remove(id);
 882    }
 883
 884    fn render_tool_use_markdown(
 885        &mut self,
 886        tool_use_id: LanguageModelToolUseId,
 887        tool_label: impl Into<SharedString>,
 888        tool_input: &str,
 889        tool_output: SharedString,
 890        cx: &mut Context<Self>,
 891    ) {
 892        let rendered = self
 893            .rendered_tool_uses
 894            .entry(tool_use_id.clone())
 895            .or_insert_with(|| RenderedToolUse {
 896                label: cx.new(|cx| {
 897                    Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
 898                }),
 899                input: cx.new(|cx| {
 900                    Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
 901                }),
 902                output: cx.new(|cx| {
 903                    Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
 904                }),
 905            });
 906
 907        rendered.label.update(cx, |this, cx| {
 908            this.replace(tool_label, cx);
 909        });
 910        rendered.input.update(cx, |this, cx| {
 911            this.replace(
 912                MarkdownCodeBlock {
 913                    tag: "json",
 914                    text: tool_input,
 915                }
 916                .to_string(),
 917                cx,
 918            );
 919        });
 920        rendered.output.update(cx, |this, cx| {
 921            this.replace(tool_output, cx);
 922        });
 923    }
 924
 925    fn handle_thread_event(
 926        &mut self,
 927        _thread: &Entity<Thread>,
 928        event: &ThreadEvent,
 929        window: &mut Window,
 930        cx: &mut Context<Self>,
 931    ) {
 932        match event {
 933            ThreadEvent::CancelEditing => {
 934                if self.editing_message.is_some() {
 935                    self.cancel_editing_message(&menu::Cancel, window, cx);
 936                }
 937            }
 938            ThreadEvent::ShowError(error) => {
 939                self.last_error = Some(error.clone());
 940            }
 941            ThreadEvent::UsageUpdated(usage) => {
 942                self.last_usage = Some(*usage);
 943            }
 944            ThreadEvent::StreamedCompletion
 945            | ThreadEvent::SummaryGenerated
 946            | ThreadEvent::SummaryChanged => {
 947                self.save_thread(cx);
 948            }
 949            ThreadEvent::Stopped(reason) => match reason {
 950                Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
 951                    let thread = self.thread.read(cx);
 952                    self.show_notification(
 953                        if thread.used_tools_since_last_user_message() {
 954                            "Finished running tools"
 955                        } else {
 956                            "New message"
 957                        },
 958                        IconName::ZedAssistant,
 959                        window,
 960                        cx,
 961                    );
 962                }
 963                _ => {}
 964            },
 965            ThreadEvent::ToolConfirmationNeeded => {
 966                self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx);
 967            }
 968            ThreadEvent::StreamedAssistantText(message_id, text) => {
 969                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
 970                    rendered_message.append_text(text, cx);
 971                }
 972            }
 973            ThreadEvent::StreamedAssistantThinking(message_id, text) => {
 974                if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
 975                    rendered_message.append_thinking(text, cx);
 976                }
 977            }
 978            ThreadEvent::MessageAdded(message_id) => {
 979                if let Some(message_segments) = self
 980                    .thread
 981                    .read(cx)
 982                    .message(*message_id)
 983                    .map(|message| message.segments.clone())
 984                {
 985                    self.push_message(message_id, &message_segments, window, cx);
 986                }
 987
 988                self.save_thread(cx);
 989                cx.notify();
 990            }
 991            ThreadEvent::MessageEdited(message_id) => {
 992                if let Some(message_segments) = self
 993                    .thread
 994                    .read(cx)
 995                    .message(*message_id)
 996                    .map(|message| message.segments.clone())
 997                {
 998                    self.edited_message(message_id, &message_segments, window, cx);
 999                }
1000
1001                self.save_thread(cx);
1002                cx.notify();
1003            }
1004            ThreadEvent::MessageDeleted(message_id) => {
1005                self.deleted_message(message_id);
1006                self.save_thread(cx);
1007                cx.notify();
1008            }
1009            ThreadEvent::UsePendingTools { tool_uses } => {
1010                for tool_use in tool_uses {
1011                    self.render_tool_use_markdown(
1012                        tool_use.id.clone(),
1013                        tool_use.ui_text.clone(),
1014                        &serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1015                        "".into(),
1016                        cx,
1017                    );
1018                }
1019            }
1020            ThreadEvent::StreamedToolUse {
1021                tool_use_id,
1022                ui_text,
1023                input,
1024            } => {
1025                self.render_tool_use_markdown(
1026                    tool_use_id.clone(),
1027                    ui_text.clone(),
1028                    &serde_json::to_string_pretty(&input).unwrap_or_default(),
1029                    "".into(),
1030                    cx,
1031                );
1032            }
1033            ThreadEvent::ToolFinished {
1034                pending_tool_use, ..
1035            } => {
1036                if let Some(tool_use) = pending_tool_use {
1037                    self.render_tool_use_markdown(
1038                        tool_use.id.clone(),
1039                        tool_use.ui_text.clone(),
1040                        &serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
1041                        self.thread
1042                            .read(cx)
1043                            .output_for_tool(&tool_use.id)
1044                            .map(|output| output.clone().into())
1045                            .unwrap_or("".into()),
1046                        cx,
1047                    );
1048                }
1049            }
1050            ThreadEvent::CheckpointChanged => cx.notify(),
1051            ThreadEvent::ReceivedTextChunk => {}
1052            ThreadEvent::InvalidToolInput {
1053                tool_use_id,
1054                ui_text,
1055                invalid_input_json,
1056            } => {
1057                self.render_tool_use_markdown(
1058                    tool_use_id.clone(),
1059                    ui_text,
1060                    invalid_input_json,
1061                    self.thread
1062                        .read(cx)
1063                        .output_for_tool(tool_use_id)
1064                        .map(|output| output.clone().into())
1065                        .unwrap_or("".into()),
1066                    cx,
1067                );
1068            }
1069        }
1070    }
1071
1072    fn handle_rules_loading_error(
1073        &mut self,
1074        _thread_store: Entity<ThreadStore>,
1075        error: &RulesLoadingError,
1076        cx: &mut Context<Self>,
1077    ) {
1078        self.last_error = Some(ThreadError::Message {
1079            header: "Error loading rules file".into(),
1080            message: error.message.clone(),
1081        });
1082        cx.notify();
1083    }
1084
1085    fn show_notification(
1086        &mut self,
1087        caption: impl Into<SharedString>,
1088        icon: IconName,
1089        window: &mut Window,
1090        cx: &mut Context<ActiveThread>,
1091    ) {
1092        if window.is_window_active() || !self.notifications.is_empty() {
1093            return;
1094        }
1095
1096        let title = self
1097            .thread
1098            .read(cx)
1099            .summary()
1100            .unwrap_or("Agent Panel".into());
1101
1102        match AssistantSettings::get_global(cx).notify_when_agent_waiting {
1103            NotifyWhenAgentWaiting::PrimaryScreen => {
1104                if let Some(primary) = cx.primary_display() {
1105                    self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
1106                }
1107            }
1108            NotifyWhenAgentWaiting::AllScreens => {
1109                let caption = caption.into();
1110                for screen in cx.displays() {
1111                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
1112                }
1113            }
1114            NotifyWhenAgentWaiting::Never => {
1115                // Don't show anything
1116            }
1117        }
1118    }
1119
1120    fn pop_up(
1121        &mut self,
1122        icon: IconName,
1123        caption: SharedString,
1124        title: SharedString,
1125        window: &mut Window,
1126        screen: Rc<dyn PlatformDisplay>,
1127        cx: &mut Context<'_, ActiveThread>,
1128    ) {
1129        let options = AgentNotification::window_options(screen, cx);
1130
1131        let project_name = self.workspace.upgrade().and_then(|workspace| {
1132            workspace
1133                .read(cx)
1134                .project()
1135                .read(cx)
1136                .visible_worktrees(cx)
1137                .next()
1138                .map(|worktree| worktree.read(cx).root_name().to_string())
1139        });
1140
1141        if let Some(screen_window) = cx
1142            .open_window(options, |_, cx| {
1143                cx.new(|_| {
1144                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
1145                })
1146            })
1147            .log_err()
1148        {
1149            if let Some(pop_up) = screen_window.entity(cx).log_err() {
1150                self.notification_subscriptions
1151                    .entry(screen_window)
1152                    .or_insert_with(Vec::new)
1153                    .push(cx.subscribe_in(&pop_up, window, {
1154                        |this, _, event, window, cx| match event {
1155                            AgentNotificationEvent::Accepted => {
1156                                let handle = window.window_handle();
1157                                cx.activate(true);
1158
1159                                let workspace_handle = this.workspace.clone();
1160
1161                                // If there are multiple Zed windows, activate the correct one.
1162                                cx.defer(move |cx| {
1163                                    handle
1164                                        .update(cx, |_view, window, _cx| {
1165                                            window.activate_window();
1166
1167                                            if let Some(workspace) = workspace_handle.upgrade() {
1168                                                workspace.update(_cx, |workspace, cx| {
1169                                                    workspace
1170                                                        .focus_panel::<AssistantPanel>(window, cx);
1171                                                });
1172                                            }
1173                                        })
1174                                        .log_err();
1175                                });
1176
1177                                this.dismiss_notifications(cx);
1178                            }
1179                            AgentNotificationEvent::Dismissed => {
1180                                this.dismiss_notifications(cx);
1181                            }
1182                        }
1183                    }));
1184
1185                self.notifications.push(screen_window);
1186
1187                // If the user manually refocuses the original window, dismiss the popup.
1188                self.notification_subscriptions
1189                    .entry(screen_window)
1190                    .or_insert_with(Vec::new)
1191                    .push({
1192                        let pop_up_weak = pop_up.downgrade();
1193
1194                        cx.observe_window_activation(window, move |_, window, cx| {
1195                            if window.is_window_active() {
1196                                if let Some(pop_up) = pop_up_weak.upgrade() {
1197                                    pop_up.update(cx, |_, cx| {
1198                                        cx.emit(AgentNotificationEvent::Dismissed);
1199                                    });
1200                                }
1201                            }
1202                        })
1203                    });
1204            }
1205        }
1206    }
1207
1208    /// Spawns a task to save the active thread.
1209    ///
1210    /// Only one task to save the thread will be in flight at a time.
1211    fn save_thread(&mut self, cx: &mut Context<Self>) {
1212        let thread = self.thread.clone();
1213        self.save_thread_task = Some(cx.spawn(async move |this, cx| {
1214            let task = this
1215                .update(cx, |this, cx| {
1216                    this.thread_store
1217                        .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
1218                })
1219                .ok();
1220
1221            if let Some(task) = task {
1222                task.await.log_err();
1223            }
1224        }));
1225    }
1226
1227    fn start_editing_message(
1228        &mut self,
1229        message_id: MessageId,
1230        message_segments: &[MessageSegment],
1231        window: &mut Window,
1232        cx: &mut Context<Self>,
1233    ) {
1234        // User message should always consist of a single text segment,
1235        // therefore we can skip returning early if it's not a text segment.
1236        let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
1237            return;
1238        };
1239
1240        let buffer = cx.new(|cx| {
1241            MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
1242        });
1243        let editor = cx.new(|cx| {
1244            let mut editor = Editor::new(
1245                editor::EditorMode::AutoHeight { max_lines: 8 },
1246                buffer,
1247                None,
1248                window,
1249                cx,
1250            );
1251            editor.focus_handle(cx).focus(window);
1252            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
1253            editor
1254        });
1255        let subscription = cx.subscribe(&editor, |this, _, event, cx| match event {
1256            EditorEvent::BufferEdited => {
1257                this.update_editing_message_token_count(true, cx);
1258            }
1259            _ => {}
1260        });
1261        self.editing_message = Some((
1262            message_id,
1263            EditMessageState {
1264                editor: editor.clone(),
1265                last_estimated_token_count: None,
1266                _subscription: subscription,
1267                _update_token_count_task: None,
1268            },
1269        ));
1270        self.update_editing_message_token_count(false, cx);
1271        cx.notify();
1272    }
1273
1274    fn update_editing_message_token_count(&mut self, debounce: bool, cx: &mut Context<Self>) {
1275        let Some((message_id, state)) = self.editing_message.as_mut() else {
1276            return;
1277        };
1278
1279        cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
1280        state._update_token_count_task.take();
1281
1282        let Some(configured_model) = self.thread.read(cx).configured_model() else {
1283            state.last_estimated_token_count.take();
1284            return;
1285        };
1286
1287        let editor = state.editor.clone();
1288        let thread = self.thread.clone();
1289        let message_id = *message_id;
1290
1291        state._update_token_count_task = Some(cx.spawn(async move |this, cx| {
1292            if debounce {
1293                cx.background_executor()
1294                    .timer(Duration::from_millis(200))
1295                    .await;
1296            }
1297
1298            let token_count = if let Some(task) = cx
1299                .update(|cx| {
1300                    let Some(message) = thread.read(cx).message(message_id) else {
1301                        log::error!("Message that was being edited no longer exists");
1302                        return None;
1303                    };
1304                    let message_text = editor.read(cx).text(cx);
1305
1306                    if message_text.is_empty() && message.loaded_context.is_empty() {
1307                        return None;
1308                    }
1309
1310                    let mut request_message = LanguageModelRequestMessage {
1311                        role: language_model::Role::User,
1312                        content: Vec::new(),
1313                        cache: false,
1314                    };
1315
1316                    message
1317                        .loaded_context
1318                        .add_to_request_message(&mut request_message);
1319
1320                    if !message_text.is_empty() {
1321                        request_message
1322                            .content
1323                            .push(MessageContent::Text(message_text));
1324                    }
1325
1326                    let request = language_model::LanguageModelRequest {
1327                        thread_id: None,
1328                        prompt_id: None,
1329                        mode: None,
1330                        messages: vec![request_message],
1331                        tools: vec![],
1332                        stop: vec![],
1333                        temperature: None,
1334                    };
1335
1336                    Some(configured_model.model.count_tokens(request, cx))
1337                })
1338                .ok()
1339                .flatten()
1340            {
1341                task.await.log_err()
1342            } else {
1343                Some(0)
1344            };
1345
1346            if let Some(token_count) = token_count {
1347                this.update(cx, |this, cx| {
1348                    let Some((_message_id, state)) = this.editing_message.as_mut() else {
1349                        return;
1350                    };
1351
1352                    state.last_estimated_token_count = Some(token_count);
1353                    cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
1354                })
1355                .ok();
1356            };
1357        }));
1358    }
1359
1360    fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
1361        self.editing_message.take();
1362        cx.notify();
1363    }
1364
1365    fn confirm_editing_message(
1366        &mut self,
1367        _: &menu::Confirm,
1368        window: &mut Window,
1369        cx: &mut Context<Self>,
1370    ) {
1371        let Some((message_id, state)) = self.editing_message.take() else {
1372            return;
1373        };
1374        let edited_text = state.editor.read(cx).text(cx);
1375        let thread_model = self.thread.update(cx, |thread, cx| {
1376            thread.edit_message(
1377                message_id,
1378                Role::User,
1379                vec![MessageSegment::Text(edited_text)],
1380                cx,
1381            );
1382            for message_id in self.messages_after(message_id) {
1383                thread.delete_message(*message_id, cx);
1384            }
1385            thread.get_or_init_configured_model(cx)
1386        });
1387
1388        let Some(model) = thread_model else {
1389            return;
1390        };
1391
1392        if model.provider.must_accept_terms(cx) {
1393            cx.notify();
1394            return;
1395        }
1396
1397        self.thread.update(cx, |thread, cx| {
1398            thread.advance_prompt_id();
1399            thread.send_to_model(model.model, Some(window.window_handle()), cx);
1400        });
1401        cx.notify();
1402    }
1403
1404    fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
1405        self.messages
1406            .iter()
1407            .position(|id| *id == message_id)
1408            .map(|index| &self.messages[index + 1..])
1409            .unwrap_or(&[])
1410    }
1411
1412    fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1413        self.cancel_editing_message(&menu::Cancel, window, cx);
1414    }
1415
1416    fn handle_regenerate_click(
1417        &mut self,
1418        _: &ClickEvent,
1419        window: &mut Window,
1420        cx: &mut Context<Self>,
1421    ) {
1422        self.confirm_editing_message(&menu::Confirm, window, cx);
1423    }
1424
1425    fn handle_feedback_click(
1426        &mut self,
1427        message_id: MessageId,
1428        feedback: ThreadFeedback,
1429        window: &mut Window,
1430        cx: &mut Context<Self>,
1431    ) {
1432        let report = self.thread.update(cx, |thread, cx| {
1433            thread.report_message_feedback(message_id, feedback, cx)
1434        });
1435
1436        cx.spawn(async move |this, cx| {
1437            report.await?;
1438            this.update(cx, |_this, cx| cx.notify())
1439        })
1440        .detach_and_log_err(cx);
1441
1442        match feedback {
1443            ThreadFeedback::Positive => {
1444                self.open_feedback_editors.remove(&message_id);
1445            }
1446            ThreadFeedback::Negative => {
1447                self.handle_show_feedback_comments(message_id, window, cx);
1448            }
1449        }
1450    }
1451
1452    fn handle_show_feedback_comments(
1453        &mut self,
1454        message_id: MessageId,
1455        window: &mut Window,
1456        cx: &mut Context<Self>,
1457    ) {
1458        let buffer = cx.new(|cx| {
1459            let empty_string = String::new();
1460            MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
1461        });
1462
1463        let editor = cx.new(|cx| {
1464            let mut editor = Editor::new(
1465                editor::EditorMode::AutoHeight { max_lines: 4 },
1466                buffer,
1467                None,
1468                window,
1469                cx,
1470            );
1471            editor.set_placeholder_text(
1472                "What went wrong? Share your feedback so we can improve.",
1473                cx,
1474            );
1475            editor
1476        });
1477
1478        editor.read(cx).focus_handle(cx).focus(window);
1479        self.open_feedback_editors.insert(message_id, editor);
1480        cx.notify();
1481    }
1482
1483    fn submit_feedback_message(&mut self, message_id: MessageId, cx: &mut Context<Self>) {
1484        let Some(editor) = self.open_feedback_editors.get(&message_id) else {
1485            return;
1486        };
1487
1488        let report_task = self.thread.update(cx, |thread, cx| {
1489            thread.report_message_feedback(message_id, ThreadFeedback::Negative, cx)
1490        });
1491
1492        let comments = editor.read(cx).text(cx);
1493        if !comments.is_empty() {
1494            let thread_id = self.thread.read(cx).id().clone();
1495            let comments_value = String::from(comments.as_str());
1496
1497            let message_content = self
1498                .thread
1499                .read(cx)
1500                .message(message_id)
1501                .map(|msg| msg.to_string())
1502                .unwrap_or_default();
1503
1504            telemetry::event!(
1505                "Assistant Thread Feedback Comments",
1506                thread_id,
1507                message_id = message_id.0,
1508                message_content,
1509                comments = comments_value
1510            );
1511
1512            self.open_feedback_editors.remove(&message_id);
1513
1514            cx.spawn(async move |this, cx| {
1515                report_task.await?;
1516                this.update(cx, |_this, cx| cx.notify())
1517            })
1518            .detach_and_log_err(cx);
1519        }
1520    }
1521
1522    fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1523        let message_id = self.messages[ix];
1524        let Some(message) = self.thread.read(cx).message(message_id) else {
1525            return Empty.into_any();
1526        };
1527
1528        let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
1529            return Empty.into_any();
1530        };
1531
1532        let workspace = self.workspace.clone();
1533        let thread = self.thread.read(cx);
1534
1535        // Get all the data we need from thread before we start using it in closures
1536        let checkpoint = thread.checkpoint_for_message(message_id);
1537        let added_context = thread
1538            .context_for_message(message_id)
1539            .map(|context| AddedContext::new_attached(context, cx))
1540            .collect::<Vec<_>>();
1541
1542        let tool_uses = thread.tool_uses_for_message(message_id, cx);
1543        let has_tool_uses = !tool_uses.is_empty();
1544        let is_generating = thread.is_generating();
1545
1546        let is_first_message = ix == 0;
1547        let is_last_message = ix == self.messages.len() - 1;
1548
1549        let show_feedback = thread.is_turn_end(ix);
1550
1551        let generating_label = (is_generating && is_last_message)
1552            .then(|| AnimatedLabel::new("Generating").size(LabelSize::Small));
1553
1554        let edit_message_editor = self
1555            .editing_message
1556            .as_ref()
1557            .filter(|(id, _)| *id == message_id)
1558            .map(|(_, state)| state.editor.clone());
1559
1560        let colors = cx.theme().colors();
1561        let editor_bg_color = colors.editor_background;
1562
1563        let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
1564            .shape(ui::IconButtonShape::Square)
1565            .icon_size(IconSize::XSmall)
1566            .icon_color(Color::Ignored)
1567            .tooltip(Tooltip::text("Open Thread as Markdown"))
1568            .on_click(|_, window, cx| {
1569                window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
1570            });
1571
1572        // For all items that should be aligned with the LLM's response.
1573        const RESPONSE_PADDING_X: Pixels = px(18.);
1574
1575        let feedback_container = h_flex()
1576            .group("feedback_container")
1577            .py_2()
1578            .px(RESPONSE_PADDING_X)
1579            .gap_1()
1580            .flex_wrap()
1581            .justify_end();
1582        let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
1583            Some(feedback) => feedback_container
1584                .child(
1585                    div().mr_1().visible_on_hover("feedback_container").child(
1586                        Label::new(match feedback {
1587                            ThreadFeedback::Positive => "Thanks for your feedback!",
1588                            ThreadFeedback::Negative => {
1589                                "We appreciate your feedback and will use it to improve."
1590                            }
1591                        })
1592                    .color(Color::Muted)
1593                    .size(LabelSize::XSmall)
1594                    .truncate())
1595                )
1596                .child(
1597                    h_flex()
1598                        .pr_1()
1599                        .gap_1()
1600                        .child(
1601                            IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
1602                                .shape(ui::IconButtonShape::Square)
1603                                .icon_size(IconSize::XSmall)
1604                                .icon_color(match feedback {
1605                                    ThreadFeedback::Positive => Color::Accent,
1606                                    ThreadFeedback::Negative => Color::Ignored,
1607                                })
1608                                .tooltip(Tooltip::text("Helpful Response"))
1609                                .on_click(cx.listener(move |this, _, window, cx| {
1610                                    this.handle_feedback_click(
1611                                        message_id,
1612                                        ThreadFeedback::Positive,
1613                                        window,
1614                                        cx,
1615                                    );
1616                                })),
1617                        )
1618                        .child(
1619                            IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
1620                                .shape(ui::IconButtonShape::Square)
1621                                .icon_size(IconSize::XSmall)
1622                                .icon_color(match feedback {
1623                                    ThreadFeedback::Positive => Color::Ignored,
1624                                    ThreadFeedback::Negative => Color::Accent,
1625                                })
1626                                .tooltip(Tooltip::text("Not Helpful"))
1627                                .on_click(cx.listener(move |this, _, window, cx| {
1628                                    this.handle_feedback_click(
1629                                        message_id,
1630                                        ThreadFeedback::Negative,
1631                                        window,
1632                                        cx,
1633                                    );
1634                                })),
1635                        )
1636                        .child(open_as_markdown),
1637                )
1638                .into_any_element(),
1639            None => feedback_container
1640                .child(
1641                    div().mr_1().visible_on_hover("feedback_container").child(
1642                        Label::new(
1643                            "Rating the thread sends all of your current conversation to the Zed team.",
1644                        )
1645                        .color(Color::Muted)
1646                    .size(LabelSize::XSmall)
1647                    .truncate())
1648                )
1649                .child(
1650                    h_flex()
1651                        .pr_1()
1652                        .gap_1()
1653                        .child(
1654                            IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
1655                                .icon_size(IconSize::XSmall)
1656                                .icon_color(Color::Ignored)
1657                                .shape(ui::IconButtonShape::Square)
1658                                .tooltip(Tooltip::text("Helpful Response"))
1659                                .on_click(cx.listener(move |this, _, window, cx| {
1660                                    this.handle_feedback_click(
1661                                        message_id,
1662                                        ThreadFeedback::Positive,
1663                                        window,
1664                                        cx,
1665                                    );
1666                                })),
1667                        )
1668                        .child(
1669                            IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
1670                                .icon_size(IconSize::XSmall)
1671                                .icon_color(Color::Ignored)
1672                                .shape(ui::IconButtonShape::Square)
1673                                .tooltip(Tooltip::text("Not Helpful"))
1674                                .on_click(cx.listener(move |this, _, window, cx| {
1675                                    this.handle_feedback_click(
1676                                        message_id,
1677                                        ThreadFeedback::Negative,
1678                                        window,
1679                                        cx,
1680                                    );
1681                                })),
1682                        )
1683                        .child(open_as_markdown),
1684                )
1685                .into_any_element(),
1686        };
1687
1688        let message_is_empty = message.should_display_content();
1689        let has_content = !message_is_empty || !added_context.is_empty();
1690
1691        let message_content = has_content.then(|| {
1692            v_flex()
1693                .w_full()
1694                .gap_1()
1695                .when(!message_is_empty, |parent| {
1696                    parent.child(
1697                        if let Some(edit_message_editor) = edit_message_editor.clone() {
1698                            let settings = ThemeSettings::get_global(cx);
1699                            let font_size = TextSize::Small.rems(cx);
1700                            let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
1701
1702                            let text_style = TextStyle {
1703                                color: cx.theme().colors().text,
1704                                font_family: settings.buffer_font.family.clone(),
1705                                font_fallbacks: settings.buffer_font.fallbacks.clone(),
1706                                font_features: settings.buffer_font.features.clone(),
1707                                font_size: font_size.into(),
1708                                line_height: line_height.into(),
1709                                ..Default::default()
1710                            };
1711
1712                            div()
1713                                .key_context("EditMessageEditor")
1714                                .on_action(cx.listener(Self::cancel_editing_message))
1715                                .on_action(cx.listener(Self::confirm_editing_message))
1716                                .min_h_6()
1717                                .flex_grow()
1718                                .w_full()
1719                                .child(EditorElement::new(
1720                                    &edit_message_editor,
1721                                    EditorStyle {
1722                                        background: colors.editor_background,
1723                                        local_player: cx.theme().players().local(),
1724                                        text: text_style,
1725                                        syntax: cx.theme().syntax().clone(),
1726                                        ..Default::default()
1727                                    },
1728                                ))
1729                                .into_any()
1730                        } else {
1731                            div()
1732                                .min_h_6()
1733                                .child(self.render_message_content(
1734                                    message_id,
1735                                    rendered_message,
1736                                    has_tool_uses,
1737                                    workspace.clone(),
1738                                    window,
1739                                    cx,
1740                                ))
1741                                .into_any()
1742                        },
1743                    )
1744                })
1745                .when(!added_context.is_empty(), |parent| {
1746                    parent.child(h_flex().flex_wrap().gap_1().children(
1747                        added_context.into_iter().map(|added_context| {
1748                            let context = added_context.handle.clone();
1749                            ContextPill::added(added_context, false, false, None).on_click(Rc::new(
1750                                cx.listener({
1751                                    let workspace = workspace.clone();
1752                                    move |_, _, window, cx| {
1753                                        if let Some(workspace) = workspace.upgrade() {
1754                                            open_context(&context, workspace, window, cx);
1755                                            cx.notify();
1756                                        }
1757                                    }
1758                                }),
1759                            ))
1760                        }),
1761                    ))
1762                })
1763        });
1764
1765        let styled_message = match message.role {
1766            Role::User => v_flex()
1767                .id(("message-container", ix))
1768                .pt_2()
1769                .pl_2()
1770                .pr_2p5()
1771                .pb_4()
1772                .child(
1773                    v_flex()
1774                        .id(("user-message", ix))
1775                        .bg(editor_bg_color)
1776                        .rounded_lg()
1777                        .shadow_md()
1778                        .border_1()
1779                        .border_color(colors.border)
1780                        .hover(|hover| hover.border_color(colors.text_accent.opacity(0.5)))
1781                        .cursor_pointer()
1782                        .child(
1783                            h_flex()
1784                                .p_2()
1785                                .pt_3()
1786                                .gap_1()
1787                                .children(message_content)
1788                                .when_some(edit_message_editor.clone(), |this, edit_editor| {
1789                                    let edit_editor_clone = edit_editor.clone();
1790                                    this.w_full().justify_between().child(
1791                                        h_flex()
1792                                            .gap_0p5()
1793                                            .child(
1794                                                IconButton::new(
1795                                                    "cancel-edit-message",
1796                                                    IconName::Close,
1797                                                )
1798                                                .shape(ui::IconButtonShape::Square)
1799                                                .icon_color(Color::Error)
1800                                                .tooltip(move |window, cx| {
1801                                                    let focus_handle =
1802                                                        edit_editor_clone.focus_handle(cx);
1803                                                    Tooltip::for_action_in(
1804                                                        "Cancel Edit",
1805                                                        &menu::Cancel,
1806                                                        &focus_handle,
1807                                                        window,
1808                                                        cx,
1809                                                    )
1810                                                })
1811                                                .on_click(cx.listener(Self::handle_cancel_click)),
1812                                            )
1813                                            .child(
1814                                                IconButton::new(
1815                                                    "confirm-edit-message",
1816                                                    IconName::Check,
1817                                                )
1818                                                .disabled(edit_editor.read(cx).is_empty(cx))
1819                                                .shape(ui::IconButtonShape::Square)
1820                                                .icon_color(Color::Success)
1821                                                .tooltip(move |window, cx| {
1822                                                    let focus_handle = edit_editor.focus_handle(cx);
1823                                                    Tooltip::for_action_in(
1824                                                        "Regenerate",
1825                                                        &menu::Confirm,
1826                                                        &focus_handle,
1827                                                        window,
1828                                                        cx,
1829                                                    )
1830                                                })
1831                                                .on_click(
1832                                                    cx.listener(Self::handle_regenerate_click),
1833                                                ),
1834                                            ),
1835                                    )
1836                                }),
1837                        )
1838                        .when(edit_message_editor.is_none(), |this| {
1839                            this.tooltip(Tooltip::text("Click To Edit"))
1840                        })
1841                        .on_click(cx.listener({
1842                            let message_segments = message.segments.clone();
1843                            move |this, _, window, cx| {
1844                                this.start_editing_message(
1845                                    message_id,
1846                                    &message_segments,
1847                                    window,
1848                                    cx,
1849                                );
1850                            }
1851                        })),
1852                ),
1853            Role::Assistant => v_flex()
1854                .id(("message-container", ix))
1855                .px(RESPONSE_PADDING_X)
1856                .gap_2()
1857                .children(message_content)
1858                .when(has_tool_uses, |parent| {
1859                    parent.children(tool_uses.into_iter().map(|tool_use| {
1860                        self.render_tool_use(tool_use, window, workspace.clone(), cx)
1861                    }))
1862                }),
1863            Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1864                v_flex()
1865                    .bg(colors.editor_background)
1866                    .rounded_sm()
1867                    .child(div().p_4().children(message_content)),
1868            ),
1869        };
1870
1871        let after_editing_message = self
1872            .editing_message
1873            .as_ref()
1874            .map_or(false, |(editing_message_id, _)| {
1875                message_id > *editing_message_id
1876            });
1877
1878        let panel_background = cx.theme().colors().panel_background;
1879
1880        v_flex()
1881            .w_full()
1882            .when_some(checkpoint, |parent, checkpoint| {
1883                let mut is_pending = false;
1884                let mut error = None;
1885                if let Some(last_restore_checkpoint) =
1886                    self.thread.read(cx).last_restore_checkpoint()
1887                {
1888                    if last_restore_checkpoint.message_id() == message_id {
1889                        match last_restore_checkpoint {
1890                            LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1891                            LastRestoreCheckpoint::Error { error: err, .. } => {
1892                                error = Some(err.clone());
1893                            }
1894                        }
1895                    }
1896                }
1897
1898                let restore_checkpoint_button =
1899                    Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1900                        .icon(if error.is_some() {
1901                            IconName::XCircle
1902                        } else {
1903                            IconName::Undo
1904                        })
1905                        .icon_size(IconSize::XSmall)
1906                        .icon_position(IconPosition::Start)
1907                        .icon_color(if error.is_some() {
1908                            Some(Color::Error)
1909                        } else {
1910                            None
1911                        })
1912                        .label_size(LabelSize::XSmall)
1913                        .disabled(is_pending)
1914                        .on_click(cx.listener(move |this, _, _window, cx| {
1915                            this.thread.update(cx, |thread, cx| {
1916                                thread
1917                                    .restore_checkpoint(checkpoint.clone(), cx)
1918                                    .detach_and_log_err(cx);
1919                            });
1920                        }));
1921
1922                let restore_checkpoint_button = if is_pending {
1923                    restore_checkpoint_button
1924                        .with_animation(
1925                            ("pulsating-restore-checkpoint-button", ix),
1926                            Animation::new(Duration::from_secs(2))
1927                                .repeat()
1928                                .with_easing(pulsating_between(0.6, 1.)),
1929                            |label, delta| label.alpha(delta),
1930                        )
1931                        .into_any_element()
1932                } else if let Some(error) = error {
1933                    restore_checkpoint_button
1934                        .tooltip(Tooltip::text(error.to_string()))
1935                        .into_any_element()
1936                } else {
1937                    restore_checkpoint_button.into_any_element()
1938                };
1939
1940                parent.child(
1941                    h_flex()
1942                        .pt_2p5()
1943                        .px_2p5()
1944                        .w_full()
1945                        .gap_1()
1946                        .child(ui::Divider::horizontal())
1947                        .child(restore_checkpoint_button)
1948                        .child(ui::Divider::horizontal()),
1949                )
1950            })
1951            .when(is_first_message, |parent| {
1952                parent.child(self.render_rules_item(cx))
1953            })
1954            .child(styled_message)
1955            .when(generating_label.is_some(), |this| {
1956                this.child(
1957                    h_flex()
1958                        .h_8()
1959                        .mt_2()
1960                        .mb_4()
1961                        .ml_4()
1962                        .py_1p5()
1963                        .child(generating_label.unwrap()),
1964                )
1965            })
1966            .when(show_feedback, move |parent| {
1967                parent.child(feedback_items).when_some(
1968                    self.open_feedback_editors.get(&message_id),
1969                    move |parent, feedback_editor| {
1970                        let focus_handle = feedback_editor.focus_handle(cx);
1971                        parent.child(
1972                            v_flex()
1973                                .key_context("AgentFeedbackMessageEditor")
1974                                .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
1975                                    this.open_feedback_editors.remove(&message_id);
1976                                    cx.notify();
1977                                }))
1978                                .on_action(cx.listener(move |this, _: &menu::Confirm, _, cx| {
1979                                    this.submit_feedback_message(message_id, cx);
1980                                    cx.notify();
1981                                }))
1982                                .on_action(cx.listener(Self::confirm_editing_message))
1983                                .mb_2()
1984                                .mx_4()
1985                                .p_2()
1986                                .rounded_md()
1987                                .border_1()
1988                                .border_color(cx.theme().colors().border)
1989                                .bg(cx.theme().colors().editor_background)
1990                                .child(feedback_editor.clone())
1991                                .child(
1992                                    h_flex()
1993                                        .gap_1()
1994                                        .justify_end()
1995                                        .child(
1996                                            Button::new("dismiss-feedback-message", "Cancel")
1997                                                .label_size(LabelSize::Small)
1998                                                .key_binding(
1999                                                    KeyBinding::for_action_in(
2000                                                        &menu::Cancel,
2001                                                        &focus_handle,
2002                                                        window,
2003                                                        cx,
2004                                                    )
2005                                                    .map(|kb| kb.size(rems_from_px(10.))),
2006                                                )
2007                                                .on_click(cx.listener(
2008                                                    move |this, _, _window, cx| {
2009                                                        this.open_feedback_editors
2010                                                            .remove(&message_id);
2011                                                        cx.notify();
2012                                                    },
2013                                                )),
2014                                        )
2015                                        .child(
2016                                            Button::new(
2017                                                "submit-feedback-message",
2018                                                "Share Feedback",
2019                                            )
2020                                            .style(ButtonStyle::Tinted(ui::TintColor::Accent))
2021                                            .label_size(LabelSize::Small)
2022                                            .key_binding(
2023                                                KeyBinding::for_action_in(
2024                                                    &menu::Confirm,
2025                                                    &focus_handle,
2026                                                    window,
2027                                                    cx,
2028                                                )
2029                                                .map(|kb| kb.size(rems_from_px(10.))),
2030                                            )
2031                                            .on_click(
2032                                                cx.listener(move |this, _, _window, cx| {
2033                                                    this.submit_feedback_message(message_id, cx);
2034                                                    cx.notify()
2035                                                }),
2036                                            ),
2037                                        ),
2038                                ),
2039                        )
2040                    },
2041                )
2042            })
2043            .when(after_editing_message, |parent| {
2044                // Backdrop to dim out the whole thread below the editing user message
2045                parent.relative().child(
2046                    div()
2047                        .occlude()
2048                        .absolute()
2049                        .inset_0()
2050                        .size_full()
2051                        .bg(panel_background)
2052                        .opacity(0.8),
2053                )
2054            })
2055            .into_any()
2056    }
2057
2058    fn render_message_content(
2059        &self,
2060        message_id: MessageId,
2061        rendered_message: &RenderedMessage,
2062        has_tool_uses: bool,
2063        workspace: WeakEntity<Workspace>,
2064        window: &Window,
2065        cx: &Context<Self>,
2066    ) -> impl IntoElement {
2067        let is_last_message = self.messages.last() == Some(&message_id);
2068        let is_generating = self.thread.read(cx).is_generating();
2069        let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses {
2070            rendered_message
2071                .segments
2072                .iter()
2073                .enumerate()
2074                .next_back()
2075                .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
2076                .map(|(index, _)| index)
2077        } else {
2078            None
2079        };
2080
2081        let message_role = self
2082            .thread
2083            .read(cx)
2084            .message(message_id)
2085            .map(|m| m.role)
2086            .unwrap_or(Role::User);
2087
2088        let is_assistant_message = message_role == Role::Assistant;
2089        let is_user_message = message_role == Role::User;
2090
2091        v_flex()
2092            .text_ui(cx)
2093            .gap_2()
2094            .when(is_user_message, |this| this.text_xs())
2095            .children(
2096                rendered_message.segments.iter().enumerate().map(
2097                    |(index, segment)| match segment {
2098                        RenderedMessageSegment::Thinking {
2099                            content,
2100                            scroll_handle,
2101                        } => self
2102                            .render_message_thinking_segment(
2103                                message_id,
2104                                index,
2105                                content.clone(),
2106                                &scroll_handle,
2107                                Some(index) == pending_thinking_segment_index,
2108                                window,
2109                                cx,
2110                            )
2111                            .into_any_element(),
2112                        RenderedMessageSegment::Text(markdown) => {
2113                            let markdown_element = MarkdownElement::new(
2114                                markdown.clone(),
2115                                if is_user_message {
2116                                    let mut style = default_markdown_style(window, cx);
2117                                    let mut text_style = window.text_style();
2118                                    let theme_settings = ThemeSettings::get_global(cx);
2119
2120                                    let buffer_font = theme_settings.buffer_font.family.clone();
2121                                    let buffer_font_size = TextSize::Small.rems(cx);
2122
2123                                    text_style.refine(&TextStyleRefinement {
2124                                        font_family: Some(buffer_font),
2125                                        font_size: Some(buffer_font_size.into()),
2126                                        ..Default::default()
2127                                    });
2128
2129                                    style.base_text_style = text_style;
2130                                    style
2131                                } else {
2132                                    default_markdown_style(window, cx)
2133                                },
2134                            );
2135
2136                            let markdown_element = if is_assistant_message {
2137                                markdown_element.code_block_renderer(
2138                                    markdown::CodeBlockRenderer::Custom {
2139                                        render: Arc::new({
2140                                            let workspace = workspace.clone();
2141                                            let active_thread = cx.entity();
2142                                            move |kind,
2143                                                  parsed_markdown,
2144                                                  range,
2145                                                  metadata,
2146                                                  window,
2147                                                  cx| {
2148                                                render_markdown_code_block(
2149                                                    message_id,
2150                                                    range.start,
2151                                                    kind,
2152                                                    parsed_markdown,
2153                                                    metadata,
2154                                                    active_thread.clone(),
2155                                                    workspace.clone(),
2156                                                    window,
2157                                                    cx,
2158                                                )
2159                                            }
2160                                        }),
2161                                        transform: Some(Arc::new({
2162                                            let active_thread = cx.entity();
2163                                            let editor_bg = cx.theme().colors().editor_background;
2164
2165                                            move |el, range, metadata, _, cx| {
2166                                                let is_expanded = active_thread
2167                                                    .read(cx)
2168                                                    .expanded_code_blocks
2169                                                    .get(&(message_id, range.start))
2170                                                    .copied()
2171                                                    .unwrap_or(true);
2172
2173                                                if is_expanded
2174                                                    || metadata.line_count
2175                                                        <= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
2176                                                {
2177                                                    return el;
2178                                                }
2179                                                el.child(
2180                                                    div()
2181                                                        .absolute()
2182                                                        .bottom_0()
2183                                                        .left_0()
2184                                                        .w_full()
2185                                                        .h_1_4()
2186                                                        .rounded_b_lg()
2187                                                        .bg(linear_gradient(
2188                                                            0.,
2189                                                            linear_color_stop(editor_bg, 0.),
2190                                                            linear_color_stop(
2191                                                                editor_bg.opacity(0.),
2192                                                                1.,
2193                                                            ),
2194                                                        )),
2195                                                )
2196                                            }
2197                                        })),
2198                                    },
2199                                )
2200                            } else {
2201                                markdown_element.code_block_renderer(
2202                                    markdown::CodeBlockRenderer::Default {
2203                                        copy_button: false,
2204                                        border: true,
2205                                    },
2206                                )
2207                            };
2208
2209                            div()
2210                                .child(markdown_element.on_url_click({
2211                                    let workspace = self.workspace.clone();
2212                                    move |text, window, cx| {
2213                                        open_markdown_link(text, workspace.clone(), window, cx);
2214                                    }
2215                                }))
2216                                .into_any_element()
2217                        }
2218                    },
2219                ),
2220            )
2221    }
2222
2223    fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
2224        cx.theme().colors().border.opacity(0.5)
2225    }
2226
2227    fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
2228        cx.theme()
2229            .colors()
2230            .element_background
2231            .blend(cx.theme().colors().editor_foreground.opacity(0.025))
2232    }
2233
2234    fn render_message_thinking_segment(
2235        &self,
2236        message_id: MessageId,
2237        ix: usize,
2238        markdown: Entity<Markdown>,
2239        scroll_handle: &ScrollHandle,
2240        pending: bool,
2241        window: &Window,
2242        cx: &Context<Self>,
2243    ) -> impl IntoElement {
2244        let is_open = self
2245            .expanded_thinking_segments
2246            .get(&(message_id, ix))
2247            .copied()
2248            .unwrap_or_default();
2249
2250        let editor_bg = cx.theme().colors().panel_background;
2251
2252        div().map(|this| {
2253            if pending {
2254                this.v_flex()
2255                    .mt_neg_2()
2256                    .mb_1p5()
2257                    .child(
2258                        h_flex()
2259                            .group("disclosure-header")
2260                            .justify_between()
2261                            .child(
2262                                h_flex()
2263                                    .gap_1p5()
2264                                    .child(
2265                                        Icon::new(IconName::LightBulb)
2266                                            .size(IconSize::XSmall)
2267                                            .color(Color::Muted),
2268                                    )
2269                                    .child(AnimatedLabel::new("Thinking").size(LabelSize::Small)),
2270                            )
2271                            .child(
2272                                h_flex()
2273                                    .gap_1()
2274                                    .child(
2275                                        div().visible_on_hover("disclosure-header").child(
2276                                            Disclosure::new("thinking-disclosure", is_open)
2277                                                .opened_icon(IconName::ChevronUp)
2278                                                .closed_icon(IconName::ChevronDown)
2279                                                .on_click(cx.listener({
2280                                                    move |this, _event, _window, _cx| {
2281                                                        let is_open = this
2282                                                            .expanded_thinking_segments
2283                                                            .entry((message_id, ix))
2284                                                            .or_insert(false);
2285
2286                                                        *is_open = !*is_open;
2287                                                    }
2288                                                })),
2289                                        ),
2290                                    )
2291                                    .child({
2292                                        Icon::new(IconName::ArrowCircle)
2293                                            .color(Color::Accent)
2294                                            .size(IconSize::Small)
2295                                            .with_animation(
2296                                                "arrow-circle",
2297                                                Animation::new(Duration::from_secs(2)).repeat(),
2298                                                |icon, delta| {
2299                                                    icon.transform(Transformation::rotate(
2300                                                        percentage(delta),
2301                                                    ))
2302                                                },
2303                                            )
2304                                    }),
2305                            ),
2306                    )
2307                    .when(!is_open, |this| {
2308                        let gradient_overlay = div()
2309                            .rounded_b_lg()
2310                            .h_full()
2311                            .absolute()
2312                            .w_full()
2313                            .bottom_0()
2314                            .left_0()
2315                            .bg(linear_gradient(
2316                                180.,
2317                                linear_color_stop(editor_bg, 1.),
2318                                linear_color_stop(editor_bg.opacity(0.2), 0.),
2319                            ));
2320
2321                        this.child(
2322                            div()
2323                                .relative()
2324                                .bg(editor_bg)
2325                                .rounded_b_lg()
2326                                .mt_2()
2327                                .pl_4()
2328                                .child(
2329                                    div()
2330                                        .id(("thinking-content", ix))
2331                                        .max_h_20()
2332                                        .track_scroll(scroll_handle)
2333                                        .text_ui_sm(cx)
2334                                        .overflow_hidden()
2335                                        .child(
2336                                            MarkdownElement::new(
2337                                                markdown.clone(),
2338                                                default_markdown_style(window, cx),
2339                                            )
2340                                            .on_url_click({
2341                                                let workspace = self.workspace.clone();
2342                                                move |text, window, cx| {
2343                                                    open_markdown_link(
2344                                                        text,
2345                                                        workspace.clone(),
2346                                                        window,
2347                                                        cx,
2348                                                    );
2349                                                }
2350                                            }),
2351                                        ),
2352                                )
2353                                .child(gradient_overlay),
2354                        )
2355                    })
2356                    .when(is_open, |this| {
2357                        this.child(
2358                            div()
2359                                .id(("thinking-content", ix))
2360                                .h_full()
2361                                .bg(editor_bg)
2362                                .text_ui_sm(cx)
2363                                .child(
2364                                    MarkdownElement::new(
2365                                        markdown.clone(),
2366                                        default_markdown_style(window, cx),
2367                                    )
2368                                    .on_url_click({
2369                                        let workspace = self.workspace.clone();
2370                                        move |text, window, cx| {
2371                                            open_markdown_link(text, workspace.clone(), window, cx);
2372                                        }
2373                                    }),
2374                                ),
2375                        )
2376                    })
2377            } else {
2378                this.v_flex()
2379                    .mt_neg_2()
2380                    .child(
2381                        h_flex()
2382                            .group("disclosure-header")
2383                            .pr_1()
2384                            .justify_between()
2385                            .opacity(0.8)
2386                            .hover(|style| style.opacity(1.))
2387                            .child(
2388                                h_flex()
2389                                    .gap_1p5()
2390                                    .child(
2391                                        Icon::new(IconName::LightBulb)
2392                                            .size(IconSize::XSmall)
2393                                            .color(Color::Muted),
2394                                    )
2395                                    .child(Label::new("Thought Process").size(LabelSize::Small)),
2396                            )
2397                            .child(
2398                                div().visible_on_hover("disclosure-header").child(
2399                                    Disclosure::new("thinking-disclosure", is_open)
2400                                        .opened_icon(IconName::ChevronUp)
2401                                        .closed_icon(IconName::ChevronDown)
2402                                        .on_click(cx.listener({
2403                                            move |this, _event, _window, _cx| {
2404                                                let is_open = this
2405                                                    .expanded_thinking_segments
2406                                                    .entry((message_id, ix))
2407                                                    .or_insert(false);
2408
2409                                                *is_open = !*is_open;
2410                                            }
2411                                        })),
2412                                ),
2413                            ),
2414                    )
2415                    .child(
2416                        div()
2417                            .id(("thinking-content", ix))
2418                            .relative()
2419                            .mt_1p5()
2420                            .ml_1p5()
2421                            .pl_2p5()
2422                            .border_l_1()
2423                            .border_color(cx.theme().colors().border_variant)
2424                            .text_ui_sm(cx)
2425                            .when(is_open, |this| {
2426                                this.child(
2427                                    MarkdownElement::new(
2428                                        markdown.clone(),
2429                                        default_markdown_style(window, cx),
2430                                    )
2431                                    .on_url_click({
2432                                        let workspace = self.workspace.clone();
2433                                        move |text, window, cx| {
2434                                            open_markdown_link(text, workspace.clone(), window, cx);
2435                                        }
2436                                    }),
2437                                )
2438                            }),
2439                    )
2440            }
2441        })
2442    }
2443
2444    fn render_tool_use(
2445        &self,
2446        tool_use: ToolUse,
2447        window: &mut Window,
2448        workspace: WeakEntity<Workspace>,
2449        cx: &mut Context<Self>,
2450    ) -> impl IntoElement + use<> {
2451        if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
2452            return card.render(&tool_use.status, window, workspace, cx);
2453        }
2454
2455        let is_open = self
2456            .expanded_tool_uses
2457            .get(&tool_use.id)
2458            .copied()
2459            .unwrap_or_default();
2460
2461        let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
2462
2463        let fs = self
2464            .workspace
2465            .upgrade()
2466            .map(|workspace| workspace.read(cx).app_state().fs.clone());
2467        let needs_confirmation = matches!(&tool_use.status, ToolUseStatus::NeedsConfirmation);
2468        let needs_confirmation_tools = tool_use.needs_confirmation;
2469
2470        let status_icons = div().child(match &tool_use.status {
2471            ToolUseStatus::NeedsConfirmation => {
2472                let icon = Icon::new(IconName::Warning)
2473                    .color(Color::Warning)
2474                    .size(IconSize::Small);
2475                icon.into_any_element()
2476            }
2477            ToolUseStatus::Pending
2478            | ToolUseStatus::InputStillStreaming
2479            | ToolUseStatus::Running => {
2480                let icon = Icon::new(IconName::ArrowCircle)
2481                    .color(Color::Accent)
2482                    .size(IconSize::Small);
2483                icon.with_animation(
2484                    "arrow-circle",
2485                    Animation::new(Duration::from_secs(2)).repeat(),
2486                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
2487                )
2488                .into_any_element()
2489            }
2490            ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
2491            ToolUseStatus::Error(_) => {
2492                let icon = Icon::new(IconName::Close)
2493                    .color(Color::Error)
2494                    .size(IconSize::Small);
2495                icon.into_any_element()
2496            }
2497        });
2498
2499        let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
2500        let results_content_container = || v_flex().p_2().gap_0p5();
2501
2502        let results_content = v_flex()
2503            .gap_1()
2504            .child(
2505                results_content_container()
2506                    .child(
2507                        Label::new("Input")
2508                            .size(LabelSize::XSmall)
2509                            .color(Color::Muted)
2510                            .buffer_font(cx),
2511                    )
2512                    .child(
2513                        div()
2514                            .w_full()
2515                            .text_ui_sm(cx)
2516                            .children(rendered_tool_use.as_ref().map(|rendered| {
2517                                MarkdownElement::new(
2518                                    rendered.input.clone(),
2519                                    tool_use_markdown_style(window, cx),
2520                                )
2521                                .code_block_renderer(markdown::CodeBlockRenderer::Default {
2522                                    copy_button: false,
2523                                    border: false,
2524                                })
2525                                .on_url_click({
2526                                    let workspace = self.workspace.clone();
2527                                    move |text, window, cx| {
2528                                        open_markdown_link(text, workspace.clone(), window, cx);
2529                                    }
2530                                })
2531                            })),
2532                    ),
2533            )
2534            .map(|container| match tool_use.status {
2535                ToolUseStatus::Finished(_) => container.child(
2536                    results_content_container()
2537                        .border_t_1()
2538                        .border_color(self.tool_card_border_color(cx))
2539                        .child(
2540                            Label::new("Result")
2541                                .size(LabelSize::XSmall)
2542                                .color(Color::Muted)
2543                                .buffer_font(cx),
2544                        )
2545                        .child(div().w_full().text_ui_sm(cx).children(
2546                            rendered_tool_use.as_ref().map(|rendered| {
2547                                MarkdownElement::new(
2548                                    rendered.output.clone(),
2549                                    tool_use_markdown_style(window, cx),
2550                                )
2551                                .code_block_renderer(markdown::CodeBlockRenderer::Default {
2552                                    copy_button: false,
2553                                    border: false,
2554                                })
2555                                .on_url_click({
2556                                    let workspace = self.workspace.clone();
2557                                    move |text, window, cx| {
2558                                        open_markdown_link(text, workspace.clone(), window, cx);
2559                                    }
2560                                })
2561                                .into_any_element()
2562                            }),
2563                        )),
2564                ),
2565                ToolUseStatus::InputStillStreaming | ToolUseStatus::Running => container.child(
2566                    results_content_container()
2567                        .border_t_1()
2568                        .border_color(self.tool_card_border_color(cx))
2569                        .child(
2570                            h_flex()
2571                                .gap_1()
2572                                .child(
2573                                    Icon::new(IconName::ArrowCircle)
2574                                        .size(IconSize::Small)
2575                                        .color(Color::Accent)
2576                                        .with_animation(
2577                                            "arrow-circle",
2578                                            Animation::new(Duration::from_secs(2)).repeat(),
2579                                            |icon, delta| {
2580                                                icon.transform(Transformation::rotate(percentage(
2581                                                    delta,
2582                                                )))
2583                                            },
2584                                        ),
2585                                )
2586                                .child(
2587                                    Label::new("Running…")
2588                                        .size(LabelSize::XSmall)
2589                                        .color(Color::Muted)
2590                                        .buffer_font(cx),
2591                                ),
2592                        ),
2593                ),
2594                ToolUseStatus::Error(_) => container.child(
2595                    results_content_container()
2596                        .border_t_1()
2597                        .border_color(self.tool_card_border_color(cx))
2598                        .child(
2599                            Label::new("Error")
2600                                .size(LabelSize::XSmall)
2601                                .color(Color::Muted)
2602                                .buffer_font(cx),
2603                        )
2604                        .child(
2605                            div()
2606                                .text_ui_sm(cx)
2607                                .children(rendered_tool_use.as_ref().map(|rendered| {
2608                                    MarkdownElement::new(
2609                                        rendered.output.clone(),
2610                                        tool_use_markdown_style(window, cx),
2611                                    )
2612                                    .on_url_click({
2613                                        let workspace = self.workspace.clone();
2614                                        move |text, window, cx| {
2615                                            open_markdown_link(text, workspace.clone(), window, cx);
2616                                        }
2617                                    })
2618                                    .into_any_element()
2619                                })),
2620                        ),
2621                ),
2622                ToolUseStatus::Pending => container,
2623                ToolUseStatus::NeedsConfirmation => container.child(
2624                    results_content_container()
2625                        .border_t_1()
2626                        .border_color(self.tool_card_border_color(cx))
2627                        .child(
2628                            Label::new("Asking Permission")
2629                                .size(LabelSize::Small)
2630                                .color(Color::Muted)
2631                                .buffer_font(cx),
2632                        ),
2633                ),
2634            });
2635
2636        let gradient_overlay = |color: Hsla| {
2637            div()
2638                .h_full()
2639                .absolute()
2640                .w_12()
2641                .bottom_0()
2642                .map(|element| {
2643                    if is_status_finished {
2644                        element.right_6()
2645                    } else {
2646                        element.right(px(44.))
2647                    }
2648                })
2649                .bg(linear_gradient(
2650                    90.,
2651                    linear_color_stop(color, 1.),
2652                    linear_color_stop(color.opacity(0.2), 0.),
2653                ))
2654        };
2655
2656        v_flex().gap_1().mb_2().map(|element| {
2657            if !needs_confirmation_tools {
2658                element.child(
2659                    v_flex()
2660                        .child(
2661                            h_flex()
2662                                .group("disclosure-header")
2663                                .relative()
2664                                .gap_1p5()
2665                                .justify_between()
2666                                .opacity(0.8)
2667                                .hover(|style| style.opacity(1.))
2668                                .when(!is_status_finished, |this| this.pr_2())
2669                                .child(
2670                                    h_flex()
2671                                        .id("tool-label-container")
2672                                        .gap_1p5()
2673                                        .max_w_full()
2674                                        .overflow_x_scroll()
2675                                        .child(
2676                                            Icon::new(tool_use.icon)
2677                                                .size(IconSize::XSmall)
2678                                                .color(Color::Muted),
2679                                        )
2680                                        .child(
2681                                            h_flex().pr_8().text_size(rems(0.8125)).children(
2682                                                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| {
2683                                                    open_markdown_link(text, workspace.clone(), window, cx);
2684                                                }}))
2685                                            ),
2686                                        ),
2687                                )
2688                                .child(
2689                                    h_flex()
2690                                        .gap_1()
2691                                        .child(
2692                                            div().visible_on_hover("disclosure-header").child(
2693                                                Disclosure::new("tool-use-disclosure", is_open)
2694                                                    .opened_icon(IconName::ChevronUp)
2695                                                    .closed_icon(IconName::ChevronDown)
2696                                                    .on_click(cx.listener({
2697                                                        let tool_use_id = tool_use.id.clone();
2698                                                        move |this, _event, _window, _cx| {
2699                                                            let is_open = this
2700                                                                .expanded_tool_uses
2701                                                                .entry(tool_use_id.clone())
2702                                                                .or_insert(false);
2703
2704                                                            *is_open = !*is_open;
2705                                                        }
2706                                                    })),
2707                                            ),
2708                                        )
2709                                        .child(status_icons),
2710                                )
2711                                .child(gradient_overlay(cx.theme().colors().panel_background)),
2712                        )
2713                        .map(|parent| {
2714                            if !is_open {
2715                                return parent;
2716                            }
2717
2718                            parent.child(
2719                                v_flex()
2720                                    .mt_1()
2721                                    .border_1()
2722                                    .border_color(self.tool_card_border_color(cx))
2723                                    .bg(cx.theme().colors().editor_background)
2724                                    .rounded_lg()
2725                                    .child(results_content),
2726                            )
2727                        }),
2728                )
2729            } else {
2730                v_flex()
2731                    .mb_2()
2732                    .rounded_lg()
2733                    .border_1()
2734                    .border_color(self.tool_card_border_color(cx))
2735                    .overflow_hidden()
2736                    .child(
2737                        h_flex()
2738                            .group("disclosure-header")
2739                            .relative()
2740                            .justify_between()
2741                            .py_1()
2742                            .map(|element| {
2743                                if is_status_finished {
2744                                    element.pl_2().pr_0p5()
2745                                } else {
2746                                    element.px_2()
2747                                }
2748                            })
2749                            .bg(self.tool_card_header_bg(cx))
2750                            .map(|element| {
2751                                if is_open {
2752                                    element.border_b_1().rounded_t_md()
2753                                } else if needs_confirmation {
2754                                    element.rounded_t_md()
2755                                } else {
2756                                    element.rounded_md()
2757                                }
2758                            })
2759                            .border_color(self.tool_card_border_color(cx))
2760                            .child(
2761                                h_flex()
2762                                    .id("tool-label-container")
2763                                    .gap_1p5()
2764                                    .max_w_full()
2765                                    .overflow_x_scroll()
2766                                    .child(
2767                                        Icon::new(tool_use.icon)
2768                                            .size(IconSize::XSmall)
2769                                            .color(Color::Muted),
2770                                    )
2771                                    .child(
2772                                        h_flex().pr_8().text_ui_sm(cx).children(
2773                                            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| {
2774                                                open_markdown_link(text, workspace.clone(), window, cx);
2775                                            }}))
2776                                        ),
2777                                    ),
2778                            )
2779                            .child(
2780                                h_flex()
2781                                    .gap_1()
2782                                    .child(
2783                                        div().visible_on_hover("disclosure-header").child(
2784                                            Disclosure::new("tool-use-disclosure", is_open)
2785                                                .opened_icon(IconName::ChevronUp)
2786                                                .closed_icon(IconName::ChevronDown)
2787                                                .on_click(cx.listener({
2788                                                    let tool_use_id = tool_use.id.clone();
2789                                                    move |this, _event, _window, _cx| {
2790                                                        let is_open = this
2791                                                            .expanded_tool_uses
2792                                                            .entry(tool_use_id.clone())
2793                                                            .or_insert(false);
2794
2795                                                        *is_open = !*is_open;
2796                                                    }
2797                                                })),
2798                                        ),
2799                                    )
2800                                    .child(status_icons),
2801                            )
2802                            .child(gradient_overlay(self.tool_card_header_bg(cx))),
2803                    )
2804                    .map(|parent| {
2805                        if !is_open {
2806                            return parent;
2807                        }
2808
2809                        parent.child(
2810                            v_flex()
2811                                .bg(cx.theme().colors().editor_background)
2812                                .map(|element| {
2813                                    if  needs_confirmation {
2814                                        element.rounded_none()
2815                                    } else {
2816                                        element.rounded_b_lg()
2817                                    }
2818                                })
2819                                .child(results_content),
2820                        )
2821                    })
2822                    .when(needs_confirmation, |this| {
2823                        this.child(
2824                            h_flex()
2825                                .py_1()
2826                                .pl_2()
2827                                .pr_1()
2828                                .gap_1()
2829                                .justify_between()
2830                                .bg(cx.theme().colors().editor_background)
2831                                .border_t_1()
2832                                .border_color(self.tool_card_border_color(cx))
2833                                .rounded_b_lg()
2834                                .child(
2835                                    AnimatedLabel::new("Waiting for Confirmation").size(LabelSize::Small)
2836                                )
2837                                .child(
2838                                    h_flex()
2839                                        .gap_0p5()
2840                                        .child({
2841                                            let tool_id = tool_use.id.clone();
2842                                            Button::new(
2843                                                "always-allow-tool-action",
2844                                                "Always Allow",
2845                                            )
2846                                            .label_size(LabelSize::Small)
2847                                            .icon(IconName::CheckDouble)
2848                                            .icon_position(IconPosition::Start)
2849                                            .icon_size(IconSize::Small)
2850                                            .icon_color(Color::Success)
2851                                            .tooltip(move |window, cx|  {
2852                                                Tooltip::with_meta(
2853                                                    "Never ask for permission",
2854                                                    None,
2855                                                    "Restore the original behavior in your Agent Panel settings",
2856                                                    window,
2857                                                    cx,
2858                                                )
2859                                            })
2860                                            .on_click(cx.listener(
2861                                                move |this, event, window, cx| {
2862                                                    if let Some(fs) = fs.clone() {
2863                                                        update_settings_file::<AssistantSettings>(
2864                                                            fs.clone(),
2865                                                            cx,
2866                                                            |settings, _| {
2867                                                                settings.set_always_allow_tool_actions(true);
2868                                                            },
2869                                                        );
2870                                                    }
2871                                                    this.handle_allow_tool(
2872                                                        tool_id.clone(),
2873                                                        event,
2874                                                        window,
2875                                                        cx,
2876                                                    )
2877                                                },
2878                                            ))
2879                                        })
2880                                        .child(ui::Divider::vertical())
2881                                        .child({
2882                                            let tool_id = tool_use.id.clone();
2883                                            Button::new("allow-tool-action", "Allow")
2884                                                .label_size(LabelSize::Small)
2885                                                .icon(IconName::Check)
2886                                                .icon_position(IconPosition::Start)
2887                                                .icon_size(IconSize::Small)
2888                                                .icon_color(Color::Success)
2889                                                .on_click(cx.listener(
2890                                                    move |this, event, window, cx| {
2891                                                        this.handle_allow_tool(
2892                                                            tool_id.clone(),
2893                                                            event,
2894                                                            window,
2895                                                            cx,
2896                                                        )
2897                                                    },
2898                                                ))
2899                                        })
2900                                        .child({
2901                                            let tool_id = tool_use.id.clone();
2902                                            let tool_name: Arc<str> = tool_use.name.into();
2903                                            Button::new("deny-tool", "Deny")
2904                                                .label_size(LabelSize::Small)
2905                                                .icon(IconName::Close)
2906                                                .icon_position(IconPosition::Start)
2907                                                .icon_size(IconSize::Small)
2908                                                .icon_color(Color::Error)
2909                                                .on_click(cx.listener(
2910                                                    move |this, event, window, cx| {
2911                                                        this.handle_deny_tool(
2912                                                            tool_id.clone(),
2913                                                            tool_name.clone(),
2914                                                            event,
2915                                                            window,
2916                                                            cx,
2917                                                        )
2918                                                    },
2919                                                ))
2920                                        }),
2921                                ),
2922                        )
2923                    })
2924            }
2925        }).into_any_element()
2926    }
2927
2928    fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
2929        let project_context = self.thread.read(cx).project_context();
2930        let project_context = project_context.borrow();
2931        let Some(project_context) = project_context.as_ref() else {
2932            return div().into_any();
2933        };
2934
2935        let user_rules_text = if project_context.user_rules.is_empty() {
2936            None
2937        } else if project_context.user_rules.len() == 1 {
2938            let user_rules = &project_context.user_rules[0];
2939
2940            match user_rules.title.as_ref() {
2941                Some(title) => Some(format!("Using \"{title}\" user rule")),
2942                None => Some("Using user rule".into()),
2943            }
2944        } else {
2945            Some(format!(
2946                "Using {} user rules",
2947                project_context.user_rules.len()
2948            ))
2949        };
2950
2951        let first_user_rules_id = project_context
2952            .user_rules
2953            .first()
2954            .map(|user_rules| user_rules.uuid.0);
2955
2956        let rules_files = project_context
2957            .worktrees
2958            .iter()
2959            .filter_map(|worktree| worktree.rules_file.as_ref())
2960            .collect::<Vec<_>>();
2961
2962        let rules_file_text = match rules_files.as_slice() {
2963            &[] => None,
2964            &[rules_file] => Some(format!(
2965                "Using project {:?} file",
2966                rules_file.path_in_worktree
2967            )),
2968            rules_files => Some(format!("Using {} project rules files", rules_files.len())),
2969        };
2970
2971        if user_rules_text.is_none() && rules_file_text.is_none() {
2972            return div().into_any();
2973        }
2974
2975        v_flex()
2976            .pt_2()
2977            .px_2p5()
2978            .gap_1()
2979            .when_some(user_rules_text, |parent, user_rules_text| {
2980                parent.child(
2981                    h_flex()
2982                        .w_full()
2983                        .child(
2984                            Icon::new(RULES_ICON)
2985                                .size(IconSize::XSmall)
2986                                .color(Color::Disabled),
2987                        )
2988                        .child(
2989                            Label::new(user_rules_text)
2990                                .size(LabelSize::XSmall)
2991                                .color(Color::Muted)
2992                                .truncate()
2993                                .buffer_font(cx)
2994                                .ml_1p5()
2995                                .mr_0p5(),
2996                        )
2997                        .child(
2998                            IconButton::new("open-prompt-library", IconName::ArrowUpRightAlt)
2999                                .shape(ui::IconButtonShape::Square)
3000                                .icon_size(IconSize::XSmall)
3001                                .icon_color(Color::Ignored)
3002                                // TODO: Figure out a way to pass focus handle here so we can display the `OpenRulesLibrary`  keybinding
3003                                .tooltip(Tooltip::text("View User Rules"))
3004                                .on_click(move |_event, window, cx| {
3005                                    window.dispatch_action(
3006                                        Box::new(OpenRulesLibrary {
3007                                            prompt_to_select: first_user_rules_id,
3008                                        }),
3009                                        cx,
3010                                    )
3011                                }),
3012                        ),
3013                )
3014            })
3015            .when_some(rules_file_text, |parent, rules_file_text| {
3016                parent.child(
3017                    h_flex()
3018                        .w_full()
3019                        .child(
3020                            Icon::new(IconName::File)
3021                                .size(IconSize::XSmall)
3022                                .color(Color::Disabled),
3023                        )
3024                        .child(
3025                            Label::new(rules_file_text)
3026                                .size(LabelSize::XSmall)
3027                                .color(Color::Muted)
3028                                .buffer_font(cx)
3029                                .ml_1p5()
3030                                .mr_0p5(),
3031                        )
3032                        .child(
3033                            IconButton::new("open-rule", IconName::ArrowUpRightAlt)
3034                                .shape(ui::IconButtonShape::Square)
3035                                .icon_size(IconSize::XSmall)
3036                                .icon_color(Color::Ignored)
3037                                .on_click(cx.listener(Self::handle_open_rules))
3038                                .tooltip(Tooltip::text("View Rules")),
3039                        ),
3040                )
3041            })
3042            .into_any()
3043    }
3044
3045    fn handle_allow_tool(
3046        &mut self,
3047        tool_use_id: LanguageModelToolUseId,
3048        _: &ClickEvent,
3049        window: &mut Window,
3050        cx: &mut Context<Self>,
3051    ) {
3052        if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
3053            .thread
3054            .read(cx)
3055            .pending_tool(&tool_use_id)
3056            .map(|tool_use| tool_use.status.clone())
3057        {
3058            self.thread.update(cx, |thread, cx| {
3059                thread.run_tool(
3060                    c.tool_use_id.clone(),
3061                    c.ui_text.clone(),
3062                    c.input.clone(),
3063                    &c.messages,
3064                    c.tool.clone(),
3065                    Some(window.window_handle()),
3066                    cx,
3067                );
3068            });
3069        }
3070    }
3071
3072    fn handle_deny_tool(
3073        &mut self,
3074        tool_use_id: LanguageModelToolUseId,
3075        tool_name: Arc<str>,
3076        _: &ClickEvent,
3077        window: &mut Window,
3078        cx: &mut Context<Self>,
3079    ) {
3080        let window_handle = window.window_handle();
3081        self.thread.update(cx, |thread, cx| {
3082            thread.deny_tool_use(tool_use_id, tool_name, Some(window_handle), cx);
3083        });
3084    }
3085
3086    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
3087        let project_context = self.thread.read(cx).project_context();
3088        let project_context = project_context.borrow();
3089        let Some(project_context) = project_context.as_ref() else {
3090            return;
3091        };
3092
3093        let project_entry_ids = project_context
3094            .worktrees
3095            .iter()
3096            .flat_map(|worktree| worktree.rules_file.as_ref())
3097            .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id))
3098            .collect::<Vec<_>>();
3099
3100        self.workspace
3101            .update(cx, move |workspace, cx| {
3102                // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
3103                // files clear. For example, if rules file 1 is already open but rules file 2 is not,
3104                // this would open and focus rules file 2 in a tab that is not next to rules file 1.
3105                let project = workspace.project().read(cx);
3106                let project_paths = project_entry_ids
3107                    .into_iter()
3108                    .flat_map(|entry_id| project.path_for_entry(entry_id, cx))
3109                    .collect::<Vec<_>>();
3110                for project_path in project_paths {
3111                    workspace
3112                        .open_path(project_path, None, true, window, cx)
3113                        .detach_and_log_err(cx);
3114                }
3115            })
3116            .ok();
3117    }
3118
3119    fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {
3120        for window in self.notifications.drain(..) {
3121            window
3122                .update(cx, |_, window, _| {
3123                    window.remove_window();
3124                })
3125                .ok();
3126
3127            self.notification_subscriptions.remove(&window);
3128        }
3129    }
3130
3131    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
3132        if !self.show_scrollbar && !self.scrollbar_state.is_dragging() {
3133            return None;
3134        }
3135
3136        Some(
3137            div()
3138                .occlude()
3139                .id("active-thread-scrollbar")
3140                .on_mouse_move(cx.listener(|_, _, _, cx| {
3141                    cx.notify();
3142                    cx.stop_propagation()
3143                }))
3144                .on_hover(|_, _, cx| {
3145                    cx.stop_propagation();
3146                })
3147                .on_any_mouse_down(|_, _, cx| {
3148                    cx.stop_propagation();
3149                })
3150                .on_mouse_up(
3151                    MouseButton::Left,
3152                    cx.listener(|_, _, _, cx| {
3153                        cx.stop_propagation();
3154                    }),
3155                )
3156                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
3157                    cx.notify();
3158                }))
3159                .h_full()
3160                .absolute()
3161                .right_1()
3162                .top_1()
3163                .bottom_0()
3164                .w(px(12.))
3165                .cursor_default()
3166                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
3167        )
3168    }
3169
3170    fn hide_scrollbar_later(&mut self, cx: &mut Context<Self>) {
3171        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
3172        self.hide_scrollbar_task = Some(cx.spawn(async move |thread, cx| {
3173            cx.background_executor()
3174                .timer(SCROLLBAR_SHOW_INTERVAL)
3175                .await;
3176            thread
3177                .update(cx, |thread, cx| {
3178                    if !thread.scrollbar_state.is_dragging() {
3179                        thread.show_scrollbar = false;
3180                        cx.notify();
3181                    }
3182                })
3183                .log_err();
3184        }))
3185    }
3186}
3187
3188pub enum ActiveThreadEvent {
3189    EditingMessageTokenCountChanged,
3190}
3191
3192impl EventEmitter<ActiveThreadEvent> for ActiveThread {}
3193
3194impl Render for ActiveThread {
3195    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3196        v_flex()
3197            .size_full()
3198            .relative()
3199            .on_mouse_move(cx.listener(|this, _, _, cx| {
3200                this.show_scrollbar = true;
3201                this.hide_scrollbar_later(cx);
3202                cx.notify();
3203            }))
3204            .on_scroll_wheel(cx.listener(|this, _, _, cx| {
3205                this.show_scrollbar = true;
3206                this.hide_scrollbar_later(cx);
3207                cx.notify();
3208            }))
3209            .on_mouse_up(
3210                MouseButton::Left,
3211                cx.listener(|this, _, _, cx| {
3212                    this.hide_scrollbar_later(cx);
3213                }),
3214            )
3215            .child(list(self.list_state.clone()).flex_grow())
3216            .when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
3217                this.child(scrollbar)
3218            })
3219    }
3220}
3221
3222pub(crate) fn open_context(
3223    context: &AgentContextHandle,
3224    workspace: Entity<Workspace>,
3225    window: &mut Window,
3226    cx: &mut App,
3227) {
3228    match context {
3229        AgentContextHandle::File(file_context) => {
3230            if let Some(project_path) = file_context.project_path(cx) {
3231                workspace.update(cx, |workspace, cx| {
3232                    workspace
3233                        .open_path(project_path, None, true, window, cx)
3234                        .detach_and_log_err(cx);
3235                });
3236            }
3237        }
3238
3239        AgentContextHandle::Directory(directory_context) => {
3240            let entry_id = directory_context.entry_id;
3241            workspace.update(cx, |workspace, cx| {
3242                workspace.project().update(cx, |_project, cx| {
3243                    cx.emit(project::Event::RevealInProjectPanel(entry_id));
3244                })
3245            })
3246        }
3247
3248        AgentContextHandle::Symbol(symbol_context) => {
3249            let buffer = symbol_context.buffer.read(cx);
3250            if let Some(project_path) = buffer.project_path(cx) {
3251                let snapshot = buffer.snapshot();
3252                let target_position = symbol_context.range.start.to_point(&snapshot);
3253                open_editor_at_position(project_path, target_position, &workspace, window, cx)
3254                    .detach();
3255            }
3256        }
3257
3258        AgentContextHandle::Selection(selection_context) => {
3259            let buffer = selection_context.buffer.read(cx);
3260            if let Some(project_path) = buffer.project_path(cx) {
3261                let snapshot = buffer.snapshot();
3262                let target_position = selection_context.range.start.to_point(&snapshot);
3263
3264                open_editor_at_position(project_path, target_position, &workspace, window, cx)
3265                    .detach();
3266            }
3267        }
3268
3269        AgentContextHandle::FetchedUrl(fetched_url_context) => {
3270            cx.open_url(&fetched_url_context.url);
3271        }
3272
3273        AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
3274            if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
3275                panel.update(cx, |panel, cx| {
3276                    let thread_id = thread_context.thread.read(cx).id().clone();
3277                    panel
3278                        .open_thread_by_id(&thread_id, window, cx)
3279                        .detach_and_log_err(cx)
3280                });
3281            }
3282        }),
3283
3284        AgentContextHandle::Rules(rules_context) => window.dispatch_action(
3285            Box::new(OpenRulesLibrary {
3286                prompt_to_select: Some(rules_context.prompt_id.0),
3287            }),
3288            cx,
3289        ),
3290
3291        AgentContextHandle::Image(_) => {}
3292    }
3293}
3294
3295fn open_editor_at_position(
3296    project_path: project::ProjectPath,
3297    target_position: Point,
3298    workspace: &Entity<Workspace>,
3299    window: &mut Window,
3300    cx: &mut App,
3301) -> Task<()> {
3302    let open_task = workspace.update(cx, |workspace, cx| {
3303        workspace.open_path(project_path, None, true, window, cx)
3304    });
3305    window.spawn(cx, async move |cx| {
3306        if let Some(active_editor) = open_task
3307            .await
3308            .log_err()
3309            .and_then(|item| item.downcast::<Editor>())
3310        {
3311            active_editor
3312                .downgrade()
3313                .update_in(cx, |editor, window, cx| {
3314                    editor.go_to_singleton_buffer_point(target_position, window, cx);
3315                })
3316                .log_err();
3317        }
3318    })
3319}