active_thread.rs

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