active_thread.rs

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