active_thread.rs

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