text_thread_editor.rs

   1use crate::{
   2    language_model_selector::{LanguageModelSelector, language_model_selector},
   3    ui::ModelSelectorTooltip,
   4};
   5use anyhow::Result;
   6use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
   7use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
   8use client::{proto, zed_urls};
   9use collections::{BTreeSet, HashMap, HashSet, hash_map};
  10use editor::{
  11    Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset,
  12    MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint as _,
  13    actions::{MoveToEndOfLine, Newline, ShowCompletions},
  14    display_map::{
  15        BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
  16        RenderBlock, ToDisplayPoint,
  17    },
  18    scroll::ScrollOffset,
  19};
  20use editor::{FoldPlaceholder, display_map::CreaseId};
  21use fs::Fs;
  22use futures::FutureExt;
  23use gpui::{
  24    Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity,
  25    EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement,
  26    ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement,
  27    Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*,
  28    pulsating_between, size,
  29};
  30use language::{
  31    BufferSnapshot, LspAdapterDelegate, ToOffset,
  32    language_settings::{SoftWrap, all_language_settings},
  33};
  34use language_model::{
  35    ConfigurationError, IconOrSvg, LanguageModelImage, LanguageModelRegistry, Role,
  36};
  37use multi_buffer::MultiBufferRow;
  38use picker::{Picker, popover_menu::PickerPopoverMenu};
  39use project::{Project, Worktree};
  40use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
  41use rope::Point;
  42use serde::{Deserialize, Serialize};
  43use settings::{
  44    LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore,
  45    update_settings_file,
  46};
  47use std::{
  48    any::{Any, TypeId},
  49    cmp,
  50    ops::Range,
  51    path::{Path, PathBuf},
  52    rc::Rc,
  53    sync::Arc,
  54    time::Duration,
  55};
  56use text::SelectionGoal;
  57use ui::{
  58    ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,
  59    TintColor, Tooltip, prelude::*,
  60};
  61use util::{ResultExt, maybe};
  62use workspace::{
  63    CollaboratorId,
  64    searchable::{Direction, SearchableItemHandle},
  65};
  66
  67use workspace::{
  68    Save, Toast, Workspace,
  69    item::{self, FollowableItem, Item},
  70    notifications::NotificationId,
  71    pane,
  72    searchable::{SearchEvent, SearchableItem},
  73};
  74use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
  75
  76use crate::CycleFavoriteModels;
  77
  78use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
  79use assistant_text_thread::{
  80    CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
  81    MessageMetadata, MessageStatus, PendingSlashCommandStatus, TextThread, TextThreadEvent,
  82    TextThreadId, ThoughtProcessOutputSection,
  83};
  84
  85actions!(
  86    assistant,
  87    [
  88        /// Sends the current message to the assistant.
  89        Assist,
  90        /// Confirms and executes the entered slash command.
  91        ConfirmCommand,
  92        /// Copies code from the assistant's response to the clipboard.
  93        CopyCode,
  94        /// Cycles between user and assistant message roles.
  95        CycleMessageRole,
  96        /// Inserts the selected text into the active editor.
  97        InsertIntoEditor,
  98        /// Splits the conversation at the current cursor position.
  99        Split,
 100    ]
 101);
 102
 103/// Inserts files that were dragged and dropped into the assistant conversation.
 104#[derive(PartialEq, Clone, Action)]
 105#[action(namespace = assistant, no_json, no_register)]
 106pub enum InsertDraggedFiles {
 107    ProjectPaths(Vec<ProjectPath>),
 108    ExternalFiles(Vec<PathBuf>),
 109}
 110
 111#[derive(Copy, Clone, Debug, PartialEq)]
 112struct ScrollPosition {
 113    offset_before_cursor: gpui::Point<ScrollOffset>,
 114    cursor: Anchor,
 115}
 116
 117type MessageHeader = MessageMetadata;
 118
 119#[derive(Clone)]
 120enum AssistError {
 121    PaymentRequired,
 122    Message(SharedString),
 123}
 124
 125pub enum ThoughtProcessStatus {
 126    Pending,
 127    Completed,
 128}
 129
 130pub trait AgentPanelDelegate {
 131    fn active_text_thread_editor(
 132        &self,
 133        workspace: &mut Workspace,
 134        window: &mut Window,
 135        cx: &mut Context<Workspace>,
 136    ) -> Option<Entity<TextThreadEditor>>;
 137
 138    fn open_local_text_thread(
 139        &self,
 140        workspace: &mut Workspace,
 141        path: Arc<Path>,
 142        window: &mut Window,
 143        cx: &mut Context<Workspace>,
 144    ) -> Task<Result<()>>;
 145
 146    fn open_remote_text_thread(
 147        &self,
 148        workspace: &mut Workspace,
 149        text_thread_id: TextThreadId,
 150        window: &mut Window,
 151        cx: &mut Context<Workspace>,
 152    ) -> Task<Result<Entity<TextThreadEditor>>>;
 153
 154    fn quote_selection(
 155        &self,
 156        workspace: &mut Workspace,
 157        selection_ranges: Vec<Range<Anchor>>,
 158        buffer: Entity<MultiBuffer>,
 159        window: &mut Window,
 160        cx: &mut Context<Workspace>,
 161    );
 162
 163    fn quote_terminal_text(
 164        &self,
 165        workspace: &mut Workspace,
 166        text: String,
 167        window: &mut Window,
 168        cx: &mut Context<Workspace>,
 169    );
 170}
 171
 172impl dyn AgentPanelDelegate {
 173    /// Returns the global [`AssistantPanelDelegate`], if it exists.
 174    pub fn try_global(cx: &App) -> Option<Arc<Self>> {
 175        cx.try_global::<GlobalAssistantPanelDelegate>()
 176            .map(|global| global.0.clone())
 177    }
 178
 179    /// Sets the global [`AssistantPanelDelegate`].
 180    pub fn set_global(delegate: Arc<Self>, cx: &mut App) {
 181        cx.set_global(GlobalAssistantPanelDelegate(delegate));
 182    }
 183}
 184
 185struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
 186
 187impl Global for GlobalAssistantPanelDelegate {}
 188
 189pub struct TextThreadEditor {
 190    text_thread: Entity<TextThread>,
 191    fs: Arc<dyn Fs>,
 192    slash_commands: Arc<SlashCommandWorkingSet>,
 193    workspace: WeakEntity<Workspace>,
 194    project: Entity<Project>,
 195    lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
 196    editor: Entity<Editor>,
 197    pending_thought_process: Option<(CreaseId, language::Anchor)>,
 198    blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
 199    image_blocks: HashSet<CustomBlockId>,
 200    scroll_position: Option<ScrollPosition>,
 201    remote_id: Option<workspace::ViewId>,
 202    pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
 203    invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
 204    _subscriptions: Vec<Subscription>,
 205    last_error: Option<AssistError>,
 206    pub(crate) slash_menu_handle:
 207        PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
 208    // dragged_file_worktrees is used to keep references to worktrees that were added
 209    // when the user drag/dropped an external file onto the context editor. Since
 210    // the worktree is not part of the project panel, it would be dropped as soon as
 211    // the file is opened. In order to keep the worktree alive for the duration of the
 212    // context editor, we keep a reference here.
 213    dragged_file_worktrees: Vec<Entity<Worktree>>,
 214    language_model_selector: Entity<LanguageModelSelector>,
 215    language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
 216}
 217
 218const MAX_TAB_TITLE_LEN: usize = 16;
 219
 220impl TextThreadEditor {
 221    pub fn init(cx: &mut App) {
 222        workspace::FollowableViewRegistry::register::<TextThreadEditor>(cx);
 223
 224        cx.observe_new(
 225            |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
 226                workspace
 227                    .register_action(TextThreadEditor::quote_selection)
 228                    .register_action(TextThreadEditor::insert_selection)
 229                    .register_action(TextThreadEditor::copy_code)
 230                    .register_action(TextThreadEditor::handle_insert_dragged_files);
 231            },
 232        )
 233        .detach();
 234    }
 235
 236    pub fn for_text_thread(
 237        text_thread: Entity<TextThread>,
 238        fs: Arc<dyn Fs>,
 239        workspace: WeakEntity<Workspace>,
 240        project: Entity<Project>,
 241        lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
 242        window: &mut Window,
 243        cx: &mut Context<Self>,
 244    ) -> Self {
 245        let completion_provider = SlashCommandCompletionProvider::new(
 246            text_thread.read(cx).slash_commands().clone(),
 247            Some(cx.entity().downgrade()),
 248            Some(workspace.clone()),
 249        );
 250
 251        let editor = cx.new(|cx| {
 252            let mut editor =
 253                Editor::for_buffer(text_thread.read(cx).buffer().clone(), None, window, cx);
 254            editor.disable_scrollbars_and_minimap(window, cx);
 255            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
 256            editor.set_show_line_numbers(false, cx);
 257            editor.set_show_git_diff_gutter(false, cx);
 258            editor.set_show_code_actions(false, cx);
 259            editor.set_show_runnables(false, cx);
 260            editor.set_show_breakpoints(false, cx);
 261            editor.set_show_wrap_guides(false, cx);
 262            editor.set_show_indent_guides(false, cx);
 263            editor.set_completion_provider(Some(Rc::new(completion_provider)));
 264            editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never);
 265            editor.set_collaboration_hub(Box::new(project.clone()));
 266
 267            let show_edit_predictions = all_language_settings(None, cx)
 268                .edit_predictions
 269                .enabled_in_text_threads;
 270
 271            editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
 272
 273            editor
 274        });
 275
 276        let _subscriptions = vec![
 277            cx.observe(&text_thread, |_, _, cx| cx.notify()),
 278            cx.subscribe_in(&text_thread, window, Self::handle_text_thread_event),
 279            cx.subscribe_in(&editor, window, Self::handle_editor_event),
 280            cx.subscribe_in(&editor, window, Self::handle_editor_search_event),
 281            cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
 282        ];
 283
 284        let slash_command_sections = text_thread
 285            .read(cx)
 286            .slash_command_output_sections()
 287            .to_vec();
 288        let thought_process_sections = text_thread
 289            .read(cx)
 290            .thought_process_output_sections()
 291            .to_vec();
 292        let slash_commands = text_thread.read(cx).slash_commands().clone();
 293        let focus_handle = editor.read(cx).focus_handle(cx);
 294
 295        let mut this = Self {
 296            text_thread,
 297            slash_commands,
 298            editor,
 299            lsp_adapter_delegate,
 300            blocks: Default::default(),
 301            image_blocks: Default::default(),
 302            scroll_position: None,
 303            remote_id: None,
 304            pending_thought_process: None,
 305            fs: fs.clone(),
 306            workspace,
 307            project,
 308            pending_slash_command_creases: HashMap::default(),
 309            invoked_slash_command_creases: HashMap::default(),
 310            _subscriptions,
 311            last_error: None,
 312            slash_menu_handle: Default::default(),
 313            dragged_file_worktrees: Vec::new(),
 314            language_model_selector: cx.new(|cx| {
 315                language_model_selector(
 316                    |cx| LanguageModelRegistry::read_global(cx).default_model(),
 317                    {
 318                        let fs = fs.clone();
 319                        move |model, cx| {
 320                            update_settings_file(fs.clone(), cx, move |settings, _| {
 321                                let provider = model.provider_id().0.to_string();
 322                                let model = model.id().0.to_string();
 323                                settings.agent.get_or_insert_default().set_model(
 324                                    LanguageModelSelection {
 325                                        provider: LanguageModelProviderSetting(provider),
 326                                        model,
 327                                    },
 328                                )
 329                            });
 330                        }
 331                    },
 332                    {
 333                        let fs = fs.clone();
 334                        move |model, should_be_favorite, cx| {
 335                            crate::favorite_models::toggle_in_settings(
 336                                model,
 337                                should_be_favorite,
 338                                fs.clone(),
 339                                cx,
 340                            );
 341                        }
 342                    },
 343                    true, // Use popover styles for picker
 344                    focus_handle,
 345                    window,
 346                    cx,
 347                )
 348            }),
 349            language_model_selector_menu_handle: PopoverMenuHandle::default(),
 350        };
 351        this.update_message_headers(cx);
 352        this.update_image_blocks(cx);
 353        this.insert_slash_command_output_sections(slash_command_sections, false, window, cx);
 354        this.insert_thought_process_output_sections(
 355            thought_process_sections
 356                .into_iter()
 357                .map(|section| (section, ThoughtProcessStatus::Completed)),
 358            window,
 359            cx,
 360        );
 361        this
 362    }
 363
 364    fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 365        self.editor.update(cx, |editor, cx| {
 366            let show_edit_predictions = all_language_settings(None, cx)
 367                .edit_predictions
 368                .enabled_in_text_threads;
 369
 370            editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
 371        });
 372    }
 373
 374    pub fn text_thread(&self) -> &Entity<TextThread> {
 375        &self.text_thread
 376    }
 377
 378    pub fn editor(&self) -> &Entity<Editor> {
 379        &self.editor
 380    }
 381
 382    pub fn insert_default_prompt(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 383        let command_name = DefaultSlashCommand.name();
 384        self.editor.update(cx, |editor, cx| {
 385            editor.insert(&format!("/{command_name}\n\n"), window, cx)
 386        });
 387        let command = self.text_thread.update(cx, |text_thread, cx| {
 388            text_thread.reparse(cx);
 389            text_thread.parsed_slash_commands()[0].clone()
 390        });
 391        self.run_command(
 392            command.source_range,
 393            &command.name,
 394            &command.arguments,
 395            false,
 396            self.workspace.clone(),
 397            window,
 398            cx,
 399        );
 400    }
 401
 402    fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {
 403        if self.sending_disabled(cx) {
 404            return;
 405        }
 406        telemetry::event!("Agent Message Sent", agent = "zed-text");
 407        self.send_to_model(window, cx);
 408    }
 409
 410    fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 411        self.last_error = None;
 412        if let Some(user_message) = self
 413            .text_thread
 414            .update(cx, |text_thread, cx| text_thread.assist(cx))
 415        {
 416            let new_selection = {
 417                let cursor = user_message
 418                    .start
 419                    .to_offset(self.text_thread.read(cx).buffer().read(cx));
 420                MultiBufferOffset(cursor)..MultiBufferOffset(cursor)
 421            };
 422            self.editor.update(cx, |editor, cx| {
 423                editor.change_selections(Default::default(), window, cx, |selections| {
 424                    selections.select_ranges([new_selection])
 425                });
 426            });
 427            // Avoid scrolling to the new cursor position so the assistant's output is stable.
 428            cx.defer_in(window, |this, _, _| this.scroll_position = None);
 429        }
 430
 431        cx.notify();
 432    }
 433
 434    fn cancel(
 435        &mut self,
 436        _: &editor::actions::Cancel,
 437        _window: &mut Window,
 438        cx: &mut Context<Self>,
 439    ) {
 440        self.last_error = None;
 441
 442        if self
 443            .text_thread
 444            .update(cx, |text_thread, cx| text_thread.cancel_last_assist(cx))
 445        {
 446            return;
 447        }
 448
 449        cx.propagate();
 450    }
 451
 452    fn cycle_message_role(
 453        &mut self,
 454        _: &CycleMessageRole,
 455        _window: &mut Window,
 456        cx: &mut Context<Self>,
 457    ) {
 458        let cursors = self.cursors(cx);
 459        self.text_thread.update(cx, |text_thread, cx| {
 460            let messages = text_thread
 461                .messages_for_offsets(cursors.into_iter().map(|cursor| cursor.0), cx)
 462                .into_iter()
 463                .map(|message| message.id)
 464                .collect();
 465            text_thread.cycle_message_roles(messages, cx)
 466        });
 467    }
 468
 469    fn cursors(&self, cx: &mut App) -> Vec<MultiBufferOffset> {
 470        let selections = self.editor.update(cx, |editor, cx| {
 471            editor
 472                .selections
 473                .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
 474        });
 475        selections
 476            .into_iter()
 477            .map(|selection| selection.head())
 478            .collect()
 479    }
 480
 481    pub fn insert_command(&mut self, name: &str, window: &mut Window, cx: &mut Context<Self>) {
 482        if let Some(command) = self.slash_commands.command(name, cx) {
 483            self.editor.update(cx, |editor, cx| {
 484                editor.transact(window, cx, |editor, window, cx| {
 485                    editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
 486                    let snapshot = editor.buffer().read(cx).snapshot(cx);
 487                    let newest_cursor = editor
 488                        .selections
 489                        .newest::<Point>(&editor.display_snapshot(cx))
 490                        .head();
 491                    if newest_cursor.column > 0
 492                        || snapshot
 493                            .chars_at(newest_cursor)
 494                            .next()
 495                            .is_some_and(|ch| ch != '\n')
 496                    {
 497                        editor.move_to_end_of_line(
 498                            &MoveToEndOfLine {
 499                                stop_at_soft_wraps: false,
 500                            },
 501                            window,
 502                            cx,
 503                        );
 504                        editor.newline(&Newline, window, cx);
 505                    }
 506
 507                    editor.insert(&format!("/{name}"), window, cx);
 508                    if command.accepts_arguments() {
 509                        editor.insert(" ", window, cx);
 510                        editor.show_completions(&ShowCompletions, window, cx);
 511                    }
 512                });
 513            });
 514            if !command.requires_argument() {
 515                self.confirm_command(&ConfirmCommand, window, cx);
 516            }
 517        }
 518    }
 519
 520    pub fn confirm_command(
 521        &mut self,
 522        _: &ConfirmCommand,
 523        window: &mut Window,
 524        cx: &mut Context<Self>,
 525    ) {
 526        if self.editor.read(cx).has_visible_completions_menu() {
 527            return;
 528        }
 529
 530        let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
 531        let mut commands_by_range = HashMap::default();
 532        let workspace = self.workspace.clone();
 533        self.text_thread.update(cx, |text_thread, cx| {
 534            text_thread.reparse(cx);
 535            for selection in selections.iter() {
 536                if let Some(command) =
 537                    text_thread.pending_command_for_position(selection.head().text_anchor, cx)
 538                {
 539                    commands_by_range
 540                        .entry(command.source_range.clone())
 541                        .or_insert_with(|| command.clone());
 542                }
 543            }
 544        });
 545
 546        if commands_by_range.is_empty() {
 547            cx.propagate();
 548        } else {
 549            for command in commands_by_range.into_values() {
 550                self.run_command(
 551                    command.source_range,
 552                    &command.name,
 553                    &command.arguments,
 554                    true,
 555                    workspace.clone(),
 556                    window,
 557                    cx,
 558                );
 559            }
 560            cx.stop_propagation();
 561        }
 562    }
 563
 564    pub fn run_command(
 565        &mut self,
 566        command_range: Range<language::Anchor>,
 567        name: &str,
 568        arguments: &[String],
 569        ensure_trailing_newline: bool,
 570        workspace: WeakEntity<Workspace>,
 571        window: &mut Window,
 572        cx: &mut Context<Self>,
 573    ) {
 574        if let Some(command) = self.slash_commands.command(name, cx) {
 575            let text_thread = self.text_thread.read(cx);
 576            let sections = text_thread
 577                .slash_command_output_sections()
 578                .iter()
 579                .filter(|section| section.is_valid(text_thread.buffer().read(cx)))
 580                .cloned()
 581                .collect::<Vec<_>>();
 582            let snapshot = text_thread.buffer().read(cx).snapshot();
 583            let output = command.run(
 584                arguments,
 585                &sections,
 586                snapshot,
 587                workspace,
 588                self.lsp_adapter_delegate.clone(),
 589                window,
 590                cx,
 591            );
 592            self.text_thread.update(cx, |text_thread, cx| {
 593                text_thread.insert_command_output(
 594                    command_range,
 595                    name,
 596                    output,
 597                    ensure_trailing_newline,
 598                    cx,
 599                )
 600            });
 601        }
 602    }
 603
 604    fn handle_text_thread_event(
 605        &mut self,
 606        _: &Entity<TextThread>,
 607        event: &TextThreadEvent,
 608        window: &mut Window,
 609        cx: &mut Context<Self>,
 610    ) {
 611        let text_thread_editor = cx.entity().downgrade();
 612
 613        match event {
 614            TextThreadEvent::MessagesEdited => {
 615                self.update_message_headers(cx);
 616                self.update_image_blocks(cx);
 617                self.text_thread.update(cx, |text_thread, cx| {
 618                    text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
 619                });
 620            }
 621            TextThreadEvent::SummaryChanged => {
 622                cx.emit(EditorEvent::TitleChanged);
 623                self.text_thread.update(cx, |text_thread, cx| {
 624                    text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
 625                });
 626            }
 627            TextThreadEvent::SummaryGenerated => {}
 628            TextThreadEvent::PathChanged { .. } => {}
 629            TextThreadEvent::StartedThoughtProcess(range) => {
 630                let creases = self.insert_thought_process_output_sections(
 631                    [(
 632                        ThoughtProcessOutputSection {
 633                            range: range.clone(),
 634                        },
 635                        ThoughtProcessStatus::Pending,
 636                    )],
 637                    window,
 638                    cx,
 639                );
 640                self.pending_thought_process = Some((creases[0], range.start));
 641            }
 642            TextThreadEvent::EndedThoughtProcess(end) => {
 643                if let Some((crease_id, start)) = self.pending_thought_process.take() {
 644                    self.editor.update(cx, |editor, cx| {
 645                        let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
 646                        let start_anchor =
 647                            multi_buffer_snapshot.as_singleton_anchor(start).unwrap();
 648
 649                        editor.display_map.update(cx, |display_map, cx| {
 650                            display_map.unfold_intersecting(
 651                                vec![start_anchor..start_anchor],
 652                                true,
 653                                cx,
 654                            );
 655                        });
 656                        editor.remove_creases(vec![crease_id], cx);
 657                    });
 658                    self.insert_thought_process_output_sections(
 659                        [(
 660                            ThoughtProcessOutputSection { range: start..*end },
 661                            ThoughtProcessStatus::Completed,
 662                        )],
 663                        window,
 664                        cx,
 665                    );
 666                }
 667            }
 668            TextThreadEvent::StreamedCompletion => {
 669                self.editor.update(cx, |editor, cx| {
 670                    if let Some(scroll_position) = self.scroll_position {
 671                        let snapshot = editor.snapshot(window, cx);
 672                        let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
 673                        let scroll_top =
 674                            cursor_point.row().as_f64() - scroll_position.offset_before_cursor.y;
 675                        editor.set_scroll_position(
 676                            point(scroll_position.offset_before_cursor.x, scroll_top),
 677                            window,
 678                            cx,
 679                        );
 680                    }
 681                });
 682            }
 683            TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => {
 684                self.editor.update(cx, |editor, cx| {
 685                    let buffer = editor.buffer().read(cx).snapshot(cx);
 686                    let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
 687
 688                    editor.remove_creases(
 689                        removed
 690                            .iter()
 691                            .filter_map(|range| self.pending_slash_command_creases.remove(range)),
 692                        cx,
 693                    );
 694
 695                    let crease_ids = editor.insert_creases(
 696                        updated.iter().map(|command| {
 697                            let workspace = self.workspace.clone();
 698                            let confirm_command = Arc::new({
 699                                let text_thread_editor = text_thread_editor.clone();
 700                                let command = command.clone();
 701                                move |window: &mut Window, cx: &mut App| {
 702                                    text_thread_editor
 703                                        .update(cx, |text_thread_editor, cx| {
 704                                            text_thread_editor.run_command(
 705                                                command.source_range.clone(),
 706                                                &command.name,
 707                                                &command.arguments,
 708                                                false,
 709                                                workspace.clone(),
 710                                                window,
 711                                                cx,
 712                                            );
 713                                        })
 714                                        .ok();
 715                                }
 716                            });
 717                            let placeholder = FoldPlaceholder {
 718                                render: Arc::new(move |_, _, _| Empty.into_any()),
 719                                ..Default::default()
 720                            };
 721                            let render_toggle = {
 722                                let confirm_command = confirm_command.clone();
 723                                let command = command.clone();
 724                                move |row, _, _, _window: &mut Window, _cx: &mut App| {
 725                                    render_pending_slash_command_gutter_decoration(
 726                                        row,
 727                                        &command.status,
 728                                        confirm_command.clone(),
 729                                    )
 730                                }
 731                            };
 732                            let render_trailer = {
 733                                move |_row, _unfold, _window: &mut Window, _cx: &mut App| {
 734                                    Empty.into_any()
 735                                }
 736                            };
 737
 738                            let range = buffer
 739                                .anchor_range_in_excerpt(excerpt_id, command.source_range.clone())
 740                                .unwrap();
 741                            Crease::inline(range, placeholder, render_toggle, render_trailer)
 742                        }),
 743                        cx,
 744                    );
 745
 746                    self.pending_slash_command_creases.extend(
 747                        updated
 748                            .iter()
 749                            .map(|command| command.source_range.clone())
 750                            .zip(crease_ids),
 751                    );
 752                })
 753            }
 754            TextThreadEvent::InvokedSlashCommandChanged { command_id } => {
 755                self.update_invoked_slash_command(*command_id, window, cx);
 756            }
 757            TextThreadEvent::SlashCommandOutputSectionAdded { section } => {
 758                self.insert_slash_command_output_sections([section.clone()], false, window, cx);
 759            }
 760            TextThreadEvent::Operation(_) => {}
 761            TextThreadEvent::ShowAssistError(error_message) => {
 762                self.last_error = Some(AssistError::Message(error_message.clone()));
 763            }
 764            TextThreadEvent::ShowPaymentRequiredError => {
 765                self.last_error = Some(AssistError::PaymentRequired);
 766            }
 767        }
 768    }
 769
 770    fn update_invoked_slash_command(
 771        &mut self,
 772        command_id: InvokedSlashCommandId,
 773        window: &mut Window,
 774        cx: &mut Context<Self>,
 775    ) {
 776        if let Some(invoked_slash_command) =
 777            self.text_thread.read(cx).invoked_slash_command(&command_id)
 778            && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status
 779        {
 780            let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone();
 781            for range in run_commands_in_ranges {
 782                let commands = self.text_thread.update(cx, |text_thread, cx| {
 783                    text_thread.reparse(cx);
 784                    text_thread
 785                        .pending_commands_for_range(range.clone(), cx)
 786                        .to_vec()
 787                });
 788
 789                for command in commands {
 790                    self.run_command(
 791                        command.source_range,
 792                        &command.name,
 793                        &command.arguments,
 794                        false,
 795                        self.workspace.clone(),
 796                        window,
 797                        cx,
 798                    );
 799                }
 800            }
 801        }
 802
 803        self.editor.update(cx, |editor, cx| {
 804            if let Some(invoked_slash_command) =
 805                self.text_thread.read(cx).invoked_slash_command(&command_id)
 806            {
 807                if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
 808                    let buffer = editor.buffer().read(cx).snapshot(cx);
 809                    let (&excerpt_id, _buffer_id, _buffer_snapshot) =
 810                        buffer.as_singleton().unwrap();
 811
 812                    let range = buffer
 813                        .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
 814                        .unwrap();
 815                    editor.remove_folds_with_type(
 816                        &[range],
 817                        TypeId::of::<PendingSlashCommand>(),
 818                        false,
 819                        cx,
 820                    );
 821
 822                    editor.remove_creases(
 823                        HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
 824                        cx,
 825                    );
 826                } else if let hash_map::Entry::Vacant(entry) =
 827                    self.invoked_slash_command_creases.entry(command_id)
 828                {
 829                    let buffer = editor.buffer().read(cx).snapshot(cx);
 830                    let (&excerpt_id, _buffer_id, _buffer_snapshot) =
 831                        buffer.as_singleton().unwrap();
 832                    let context = self.text_thread.downgrade();
 833                    let range = buffer
 834                        .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
 835                        .unwrap();
 836                    let crease = Crease::inline(
 837                        range,
 838                        invoked_slash_command_fold_placeholder(command_id, context),
 839                        fold_toggle("invoked-slash-command"),
 840                        |_row, _folded, _window, _cx| Empty.into_any(),
 841                    );
 842                    let crease_ids = editor.insert_creases([crease.clone()], cx);
 843                    editor.fold_creases(vec![crease], false, window, cx);
 844                    entry.insert(crease_ids[0]);
 845                } else {
 846                    cx.notify()
 847                }
 848            } else {
 849                editor.remove_creases(
 850                    HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
 851                    cx,
 852                );
 853                cx.notify();
 854            };
 855        });
 856    }
 857
 858    fn insert_thought_process_output_sections(
 859        &mut self,
 860        sections: impl IntoIterator<
 861            Item = (
 862                ThoughtProcessOutputSection<language::Anchor>,
 863                ThoughtProcessStatus,
 864            ),
 865        >,
 866        window: &mut Window,
 867        cx: &mut Context<Self>,
 868    ) -> Vec<CreaseId> {
 869        self.editor.update(cx, |editor, cx| {
 870            let buffer = editor.buffer().read(cx).snapshot(cx);
 871            let excerpt_id = *buffer.as_singleton().unwrap().0;
 872            let mut buffer_rows_to_fold = BTreeSet::new();
 873            let mut creases = Vec::new();
 874            for (section, status) in sections {
 875                let range = buffer
 876                    .anchor_range_in_excerpt(excerpt_id, section.range)
 877                    .unwrap();
 878                let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
 879                buffer_rows_to_fold.insert(buffer_row);
 880                creases.push(
 881                    Crease::inline(
 882                        range,
 883                        FoldPlaceholder {
 884                            render: render_thought_process_fold_icon_button(
 885                                cx.entity().downgrade(),
 886                                status,
 887                            ),
 888                            merge_adjacent: false,
 889                            ..Default::default()
 890                        },
 891                        render_slash_command_output_toggle,
 892                        |_, _, _, _| Empty.into_any_element(),
 893                    )
 894                    .with_metadata(CreaseMetadata {
 895                        icon_path: SharedString::from(IconName::Ai.path()),
 896                        label: "Thinking Process".into(),
 897                    }),
 898                );
 899            }
 900
 901            let creases = editor.insert_creases(creases, cx);
 902
 903            for buffer_row in buffer_rows_to_fold.into_iter().rev() {
 904                editor.fold_at(buffer_row, window, cx);
 905            }
 906
 907            creases
 908        })
 909    }
 910
 911    fn insert_slash_command_output_sections(
 912        &mut self,
 913        sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
 914        expand_result: bool,
 915        window: &mut Window,
 916        cx: &mut Context<Self>,
 917    ) {
 918        self.editor.update(cx, |editor, cx| {
 919            let buffer = editor.buffer().read(cx).snapshot(cx);
 920            let excerpt_id = *buffer.as_singleton().unwrap().0;
 921            let mut buffer_rows_to_fold = BTreeSet::new();
 922            let mut creases = Vec::new();
 923            for section in sections {
 924                let range = buffer
 925                    .anchor_range_in_excerpt(excerpt_id, section.range)
 926                    .unwrap();
 927                let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
 928                buffer_rows_to_fold.insert(buffer_row);
 929                creases.push(
 930                    Crease::inline(
 931                        range,
 932                        FoldPlaceholder {
 933                            render: render_fold_icon_button(
 934                                cx.entity().downgrade(),
 935                                section.icon.path().into(),
 936                                section.label.clone(),
 937                            ),
 938                            merge_adjacent: false,
 939                            ..Default::default()
 940                        },
 941                        render_slash_command_output_toggle,
 942                        |_, _, _, _| Empty.into_any_element(),
 943                    )
 944                    .with_metadata(CreaseMetadata {
 945                        icon_path: section.icon.path().into(),
 946                        label: section.label,
 947                    }),
 948                );
 949            }
 950
 951            editor.insert_creases(creases, cx);
 952
 953            if expand_result {
 954                buffer_rows_to_fold.clear();
 955            }
 956            for buffer_row in buffer_rows_to_fold.into_iter().rev() {
 957                editor.fold_at(buffer_row, window, cx);
 958            }
 959        });
 960    }
 961
 962    fn handle_editor_event(
 963        &mut self,
 964        _: &Entity<Editor>,
 965        event: &EditorEvent,
 966        window: &mut Window,
 967        cx: &mut Context<Self>,
 968    ) {
 969        match event {
 970            EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
 971                let cursor_scroll_position = self.cursor_scroll_position(window, cx);
 972                if *autoscroll {
 973                    self.scroll_position = cursor_scroll_position;
 974                } else if self.scroll_position != cursor_scroll_position {
 975                    self.scroll_position = None;
 976                }
 977            }
 978            EditorEvent::SelectionsChanged { .. } => {
 979                self.scroll_position = self.cursor_scroll_position(window, cx);
 980            }
 981            _ => {}
 982        }
 983        cx.emit(event.clone());
 984    }
 985
 986    fn handle_editor_search_event(
 987        &mut self,
 988        _: &Entity<Editor>,
 989        event: &SearchEvent,
 990        _window: &mut Window,
 991        cx: &mut Context<Self>,
 992    ) {
 993        cx.emit(event.clone());
 994    }
 995
 996    fn cursor_scroll_position(
 997        &self,
 998        window: &mut Window,
 999        cx: &mut Context<Self>,
1000    ) -> Option<ScrollPosition> {
1001        self.editor.update(cx, |editor, cx| {
1002            let snapshot = editor.snapshot(window, cx);
1003            let cursor = editor.selections.newest_anchor().head();
1004            let cursor_row = cursor
1005                .to_display_point(&snapshot.display_snapshot)
1006                .row()
1007                .as_f64();
1008            let scroll_position = editor
1009                .scroll_manager
1010                .scroll_position(&snapshot.display_snapshot, cx);
1011
1012            let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
1013            if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
1014                Some(ScrollPosition {
1015                    cursor,
1016                    offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
1017                })
1018            } else {
1019                None
1020            }
1021        })
1022    }
1023
1024    fn esc_kbd(cx: &App) -> Div {
1025        let colors = cx.theme().colors().clone();
1026
1027        h_flex()
1028            .items_center()
1029            .gap_1()
1030            .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
1031            .text_size(TextSize::XSmall.rems(cx))
1032            .text_color(colors.text_muted)
1033            .child("Press")
1034            .child(
1035                h_flex()
1036                    .rounded_sm()
1037                    .px_1()
1038                    .mr_0p5()
1039                    .border_1()
1040                    .border_color(colors.border_variant.alpha(0.6))
1041                    .bg(colors.element_background.alpha(0.6))
1042                    .child("esc"),
1043            )
1044            .child("to cancel")
1045    }
1046
1047    fn update_message_headers(&mut self, cx: &mut Context<Self>) {
1048        self.editor.update(cx, |editor, cx| {
1049            let buffer = editor.buffer().read(cx).snapshot(cx);
1050
1051            let excerpt_id = *buffer.as_singleton().unwrap().0;
1052            let mut old_blocks = std::mem::take(&mut self.blocks);
1053            let mut blocks_to_remove: HashMap<_, _> = old_blocks
1054                .iter()
1055                .map(|(message_id, (_, block_id))| (*message_id, *block_id))
1056                .collect();
1057            let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
1058
1059            let render_block = |message: MessageMetadata| -> RenderBlock {
1060                Arc::new({
1061                    let text_thread = self.text_thread.clone();
1062
1063                    move |cx| {
1064                        let message_id = MessageId(message.timestamp);
1065                        let llm_loading = message.role == Role::Assistant
1066                            && message.status == MessageStatus::Pending;
1067
1068                        let (label, spinner, note) = match message.role {
1069                            Role::User => (
1070                                Label::new("You").color(Color::Default).into_any_element(),
1071                                None,
1072                                None,
1073                            ),
1074                            Role::Assistant => {
1075                                let base_label = Label::new("Agent").color(Color::Info);
1076                                let mut spinner = None;
1077                                let mut note = None;
1078                                let animated_label = if llm_loading {
1079                                    base_label
1080                                        .with_animation(
1081                                            "pulsating-label",
1082                                            Animation::new(Duration::from_secs(2))
1083                                                .repeat()
1084                                                .with_easing(pulsating_between(0.4, 0.8)),
1085                                            |label, delta| label.alpha(delta),
1086                                        )
1087                                        .into_any_element()
1088                                } else {
1089                                    base_label.into_any_element()
1090                                };
1091                                if llm_loading {
1092                                    spinner = Some(
1093                                        Icon::new(IconName::ArrowCircle)
1094                                            .size(IconSize::XSmall)
1095                                            .color(Color::Info)
1096                                            .with_rotate_animation(2)
1097                                            .into_any_element(),
1098                                    );
1099                                    note = Some(Self::esc_kbd(cx).into_any_element());
1100                                }
1101                                (animated_label, spinner, note)
1102                            }
1103                            Role::System => (
1104                                Label::new("System")
1105                                    .color(Color::Warning)
1106                                    .into_any_element(),
1107                                None,
1108                                None,
1109                            ),
1110                        };
1111
1112                        let sender = h_flex()
1113                            .items_center()
1114                            .gap_2p5()
1115                            .child(
1116                                ButtonLike::new("role")
1117                                    .style(ButtonStyle::Filled)
1118                                    .child(
1119                                        h_flex()
1120                                            .items_center()
1121                                            .gap_1p5()
1122                                            .child(label)
1123                                            .children(spinner),
1124                                    )
1125                                    .tooltip(|_window, cx| {
1126                                        Tooltip::with_meta(
1127                                            "Toggle message role",
1128                                            None,
1129                                            "Available roles: You (User), Agent, System",
1130                                            cx,
1131                                        )
1132                                    })
1133                                    .on_click({
1134                                        let text_thread = text_thread.clone();
1135                                        move |_, _window, cx| {
1136                                            text_thread.update(cx, |text_thread, cx| {
1137                                                text_thread.cycle_message_roles(
1138                                                    HashSet::from_iter(Some(message_id)),
1139                                                    cx,
1140                                                )
1141                                            })
1142                                        }
1143                                    }),
1144                            )
1145                            .children(note);
1146
1147                        h_flex()
1148                            .id(("message_header", message_id.as_u64()))
1149                            .pl(cx.margins.gutter.full_width())
1150                            .h_11()
1151                            .w_full()
1152                            .relative()
1153                            .gap_1p5()
1154                            .child(sender)
1155                            .children(match &message.cache {
1156                                Some(cache) if cache.is_final_anchor => match cache.status {
1157                                    CacheStatus::Cached => Some(
1158                                        div()
1159                                            .id("cached")
1160                                            .child(
1161                                                Icon::new(IconName::DatabaseZap)
1162                                                    .size(IconSize::XSmall)
1163                                                    .color(Color::Hint),
1164                                            )
1165                                            .tooltip(|_window, cx| {
1166                                                Tooltip::with_meta(
1167                                                    "Context Cached",
1168                                                    None,
1169                                                    "Large messages cached to optimize performance",
1170                                                    cx,
1171                                                )
1172                                            })
1173                                            .into_any_element(),
1174                                    ),
1175                                    CacheStatus::Pending => Some(
1176                                        div()
1177                                            .child(
1178                                                Icon::new(IconName::Ellipsis)
1179                                                    .size(IconSize::XSmall)
1180                                                    .color(Color::Hint),
1181                                            )
1182                                            .into_any_element(),
1183                                    ),
1184                                },
1185                                _ => None,
1186                            })
1187                            .children(match &message.status {
1188                                MessageStatus::Error(error) => Some(
1189                                    Button::new("show-error", "Error")
1190                                        .color(Color::Error)
1191                                        .selected_label_color(Color::Error)
1192                                        .selected_icon_color(Color::Error)
1193                                        .icon(IconName::XCircle)
1194                                        .icon_color(Color::Error)
1195                                        .icon_size(IconSize::XSmall)
1196                                        .icon_position(IconPosition::Start)
1197                                        .tooltip(Tooltip::text("View Details"))
1198                                        .on_click({
1199                                            let text_thread = text_thread.clone();
1200                                            let error = error.clone();
1201                                            move |_, _window, cx| {
1202                                                text_thread.update(cx, |_, cx| {
1203                                                    cx.emit(TextThreadEvent::ShowAssistError(
1204                                                        error.clone(),
1205                                                    ));
1206                                                });
1207                                            }
1208                                        })
1209                                        .into_any_element(),
1210                                ),
1211                                MessageStatus::Canceled => Some(
1212                                    h_flex()
1213                                        .gap_1()
1214                                        .items_center()
1215                                        .child(
1216                                            Icon::new(IconName::XCircle)
1217                                                .color(Color::Disabled)
1218                                                .size(IconSize::XSmall),
1219                                        )
1220                                        .child(
1221                                            Label::new("Canceled")
1222                                                .size(LabelSize::Small)
1223                                                .color(Color::Disabled),
1224                                        )
1225                                        .into_any_element(),
1226                                ),
1227                                _ => None,
1228                            })
1229                            .into_any_element()
1230                    }
1231                })
1232            };
1233            let create_block_properties = |message: &Message| BlockProperties {
1234                height: Some(2),
1235                style: BlockStyle::Sticky,
1236                placement: BlockPlacement::Above(
1237                    buffer
1238                        .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
1239                        .unwrap(),
1240                ),
1241                priority: usize::MAX,
1242                render: render_block(MessageMetadata::from(message)),
1243            };
1244            let mut new_blocks = vec![];
1245            let mut block_index_to_message = vec![];
1246            for message in self.text_thread.read(cx).messages(cx) {
1247                if blocks_to_remove.remove(&message.id).is_some() {
1248                    // This is an old message that we might modify.
1249                    let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
1250                        debug_assert!(
1251                            false,
1252                            "old_blocks should contain a message_id we've just removed."
1253                        );
1254                        continue;
1255                    };
1256                    // Should we modify it?
1257                    let message_meta = MessageMetadata::from(&message);
1258                    if meta != &message_meta {
1259                        blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
1260                        *meta = message_meta;
1261                    }
1262                } else {
1263                    // This is a new message.
1264                    new_blocks.push(create_block_properties(&message));
1265                    block_index_to_message.push((message.id, MessageMetadata::from(&message)));
1266                }
1267            }
1268            editor.replace_blocks(blocks_to_replace, None, cx);
1269            editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
1270
1271            let ids = editor.insert_blocks(new_blocks, None, cx);
1272            old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
1273                |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
1274            ));
1275            self.blocks = old_blocks;
1276        });
1277    }
1278
1279    /// Returns either the selected text, or the content of the Markdown code
1280    /// block surrounding the cursor.
1281    fn get_selection_or_code_block(
1282        context_editor_view: &Entity<TextThreadEditor>,
1283        cx: &mut Context<Workspace>,
1284    ) -> Option<(String, bool)> {
1285        const CODE_FENCE_DELIMITER: &str = "```";
1286
1287        let text_thread_editor = context_editor_view.read(cx).editor.clone();
1288        text_thread_editor.update(cx, |text_thread_editor, cx| {
1289            let display_map = text_thread_editor.display_snapshot(cx);
1290            if text_thread_editor
1291                .selections
1292                .newest::<Point>(&display_map)
1293                .is_empty()
1294            {
1295                let snapshot = text_thread_editor.buffer().read(cx).snapshot(cx);
1296                let (_, _, snapshot) = snapshot.as_singleton()?;
1297
1298                let head = text_thread_editor
1299                    .selections
1300                    .newest::<Point>(&display_map)
1301                    .head();
1302                let offset = snapshot.point_to_offset(head);
1303
1304                let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
1305                let mut text = snapshot
1306                    .text_for_range(surrounding_code_block_range)
1307                    .collect::<String>();
1308
1309                // If there is no newline trailing the closing three-backticks, then
1310                // tree-sitter-md extends the range of the content node to include
1311                // the backticks.
1312                if text.ends_with(CODE_FENCE_DELIMITER) {
1313                    text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
1314                }
1315
1316                (!text.is_empty()).then_some((text, true))
1317            } else {
1318                let selection = text_thread_editor.selections.newest_adjusted(&display_map);
1319                let buffer = text_thread_editor.buffer().read(cx).snapshot(cx);
1320                let selected_text = buffer.text_for_range(selection.range()).collect::<String>();
1321
1322                (!selected_text.is_empty()).then_some((selected_text, false))
1323            }
1324        })
1325    }
1326
1327    pub fn insert_selection(
1328        workspace: &mut Workspace,
1329        _: &InsertIntoEditor,
1330        window: &mut Window,
1331        cx: &mut Context<Workspace>,
1332    ) {
1333        let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1334            return;
1335        };
1336        let Some(context_editor_view) =
1337            agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
1338        else {
1339            return;
1340        };
1341        let Some(active_editor_view) = workspace
1342            .active_item(cx)
1343            .and_then(|item| item.act_as::<Editor>(cx))
1344        else {
1345            return;
1346        };
1347
1348        if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
1349            active_editor_view.update(cx, |editor, cx| {
1350                editor.insert(&text, window, cx);
1351                editor.focus_handle(cx).focus(window, cx);
1352            })
1353        }
1354    }
1355
1356    pub fn copy_code(
1357        workspace: &mut Workspace,
1358        _: &CopyCode,
1359        window: &mut Window,
1360        cx: &mut Context<Workspace>,
1361    ) {
1362        let result = maybe!({
1363            let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
1364            let context_editor_view =
1365                agent_panel_delegate.active_text_thread_editor(workspace, window, cx)?;
1366            Self::get_selection_or_code_block(&context_editor_view, cx)
1367        });
1368        let Some((text, is_code_block)) = result else {
1369            return;
1370        };
1371
1372        cx.write_to_clipboard(ClipboardItem::new_string(text));
1373
1374        struct CopyToClipboardToast;
1375        workspace.show_toast(
1376            Toast::new(
1377                NotificationId::unique::<CopyToClipboardToast>(),
1378                format!(
1379                    "{} copied to clipboard.",
1380                    if is_code_block {
1381                        "Code block"
1382                    } else {
1383                        "Selection"
1384                    }
1385                ),
1386            )
1387            .autohide(),
1388            cx,
1389        );
1390    }
1391
1392    pub fn handle_insert_dragged_files(
1393        workspace: &mut Workspace,
1394        action: &InsertDraggedFiles,
1395        window: &mut Window,
1396        cx: &mut Context<Workspace>,
1397    ) {
1398        let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1399            return;
1400        };
1401        let Some(context_editor_view) =
1402            agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
1403        else {
1404            return;
1405        };
1406
1407        let project = context_editor_view.read(cx).project.clone();
1408
1409        let paths = match action {
1410            InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
1411            InsertDraggedFiles::ExternalFiles(paths) => {
1412                let tasks = paths
1413                    .clone()
1414                    .into_iter()
1415                    .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
1416                    .collect::<Vec<_>>();
1417
1418                cx.background_spawn(async move {
1419                    let mut paths = vec![];
1420                    let mut worktrees = vec![];
1421
1422                    let opened_paths = futures::future::join_all(tasks).await;
1423
1424                    for entry in opened_paths {
1425                        if let Some((worktree, project_path)) = entry.log_err() {
1426                            worktrees.push(worktree);
1427                            paths.push(project_path);
1428                        }
1429                    }
1430
1431                    (paths, worktrees)
1432                })
1433            }
1434        };
1435
1436        context_editor_view.update(cx, |_, cx| {
1437            cx.spawn_in(window, async move |this, cx| {
1438                let (paths, dragged_file_worktrees) = paths.await;
1439                this.update_in(cx, |this, window, cx| {
1440                    this.insert_dragged_files(paths, dragged_file_worktrees, window, cx);
1441                })
1442                .ok();
1443            })
1444            .detach();
1445        })
1446    }
1447
1448    pub fn insert_dragged_files(
1449        &mut self,
1450        opened_paths: Vec<ProjectPath>,
1451        added_worktrees: Vec<Entity<Worktree>>,
1452        window: &mut Window,
1453        cx: &mut Context<Self>,
1454    ) {
1455        let mut file_slash_command_args = vec![];
1456        for project_path in opened_paths.into_iter() {
1457            let Some(worktree) = self
1458                .project
1459                .read(cx)
1460                .worktree_for_id(project_path.worktree_id, cx)
1461            else {
1462                continue;
1463            };
1464            let path_style = worktree.read(cx).path_style();
1465            let full_path = worktree
1466                .read(cx)
1467                .root_name()
1468                .join(&project_path.path)
1469                .display(path_style)
1470                .into_owned();
1471            file_slash_command_args.push(full_path);
1472        }
1473
1474        let cmd_name = FileSlashCommand.name();
1475
1476        let file_argument = file_slash_command_args.join(" ");
1477
1478        self.editor.update(cx, |editor, cx| {
1479            editor.insert("\n", window, cx);
1480            editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
1481        });
1482        self.confirm_command(&ConfirmCommand, window, cx);
1483        self.dragged_file_worktrees.extend(added_worktrees);
1484    }
1485
1486    pub fn quote_selection(
1487        workspace: &mut Workspace,
1488        _: &AddSelectionToThread,
1489        window: &mut Window,
1490        cx: &mut Context<Workspace>,
1491    ) {
1492        let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1493            return;
1494        };
1495
1496        // Get buffer info for the delegate call (even if empty, AcpThreadView ignores these
1497        // params and calls insert_selections which handles both terminal and buffer)
1498        if let Some((selections, buffer)) = maybe!({
1499            let editor = workspace
1500                .active_item(cx)
1501                .and_then(|item| item.act_as::<Editor>(cx))?;
1502
1503            let buffer = editor.read(cx).buffer().clone();
1504            let snapshot = buffer.read(cx).snapshot(cx);
1505            let selections = editor.update(cx, |editor, cx| {
1506                editor
1507                    .selections
1508                    .all_adjusted(&editor.display_snapshot(cx))
1509                    .into_iter()
1510                    .filter_map(|s| {
1511                        (!s.is_empty())
1512                            .then(|| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
1513                    })
1514                    .collect::<Vec<_>>()
1515            });
1516            Some((selections, buffer))
1517        }) {
1518            agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
1519        }
1520    }
1521
1522    pub fn quote_ranges(
1523        &mut self,
1524        ranges: Vec<Range<Point>>,
1525        snapshot: MultiBufferSnapshot,
1526        window: &mut Window,
1527        cx: &mut Context<Self>,
1528    ) {
1529        let creases = selections_creases(ranges, snapshot, cx);
1530
1531        self.editor.update(cx, |editor, cx| {
1532            editor.insert("\n", window, cx);
1533            for (text, crease_title) in creases {
1534                let point = editor
1535                    .selections
1536                    .newest::<Point>(&editor.display_snapshot(cx))
1537                    .head();
1538                let start_row = MultiBufferRow(point.row);
1539
1540                editor.insert(&text, window, cx);
1541
1542                let snapshot = editor.buffer().read(cx).snapshot(cx);
1543                let anchor_before = snapshot.anchor_after(point);
1544                let anchor_after = editor
1545                    .selections
1546                    .newest_anchor()
1547                    .head()
1548                    .bias_left(&snapshot);
1549
1550                editor.insert("\n", window, cx);
1551
1552                let fold_placeholder =
1553                    quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
1554                let crease = Crease::inline(
1555                    anchor_before..anchor_after,
1556                    fold_placeholder,
1557                    render_quote_selection_output_toggle,
1558                    |_, _, _, _| Empty.into_any(),
1559                );
1560                editor.insert_creases(vec![crease], cx);
1561                editor.fold_at(start_row, window, cx);
1562            }
1563        })
1564    }
1565
1566    pub fn quote_terminal_text(
1567        &mut self,
1568        text: String,
1569        window: &mut Window,
1570        cx: &mut Context<Self>,
1571    ) {
1572        let crease_title = "terminal".to_string();
1573        let formatted_text = format!("```console\n{}\n```\n", text);
1574
1575        self.editor.update(cx, |editor, cx| {
1576            // Insert newline first if not at the start of a line
1577            let point = editor
1578                .selections
1579                .newest::<Point>(&editor.display_snapshot(cx))
1580                .head();
1581            if point.column > 0 {
1582                editor.insert("\n", window, cx);
1583            }
1584
1585            let point = editor
1586                .selections
1587                .newest::<Point>(&editor.display_snapshot(cx))
1588                .head();
1589            let start_row = MultiBufferRow(point.row);
1590
1591            editor.insert(&formatted_text, window, cx);
1592
1593            let snapshot = editor.buffer().read(cx).snapshot(cx);
1594            let anchor_before = snapshot.anchor_after(point);
1595            let anchor_after = editor
1596                .selections
1597                .newest_anchor()
1598                .head()
1599                .bias_left(&snapshot);
1600
1601            let fold_placeholder =
1602                quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
1603            let crease = Crease::inline(
1604                anchor_before..anchor_after,
1605                fold_placeholder,
1606                render_quote_selection_output_toggle,
1607                |_, _, _, _| Empty.into_any(),
1608            );
1609            editor.insert_creases(vec![crease], cx);
1610            editor.fold_at(start_row, window, cx);
1611        })
1612    }
1613
1614    fn copy(&mut self, _: &editor::actions::Copy, _window: &mut Window, cx: &mut Context<Self>) {
1615        if self.editor.read(cx).selections.count() == 1 {
1616            let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
1617            cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1618                copied_text,
1619                metadata,
1620            ));
1621            cx.stop_propagation();
1622            return;
1623        }
1624
1625        cx.propagate();
1626    }
1627
1628    fn cut(&mut self, _: &editor::actions::Cut, window: &mut Window, cx: &mut Context<Self>) {
1629        if self.editor.read(cx).selections.count() == 1 {
1630            let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
1631
1632            self.editor.update(cx, |editor, cx| {
1633                editor.transact(window, cx, |this, window, cx| {
1634                    this.change_selections(Default::default(), window, cx, |s| {
1635                        s.select(selections);
1636                    });
1637                    this.insert("", window, cx);
1638                    cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1639                        copied_text,
1640                        metadata,
1641                    ));
1642                });
1643            });
1644
1645            cx.stop_propagation();
1646            return;
1647        }
1648
1649        cx.propagate();
1650    }
1651
1652    fn get_clipboard_contents(
1653        &mut self,
1654        cx: &mut Context<Self>,
1655    ) -> (
1656        String,
1657        CopyMetadata,
1658        Vec<text::Selection<MultiBufferOffset>>,
1659    ) {
1660        let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
1661            let mut selection = editor
1662                .selections
1663                .newest_adjusted(&editor.display_snapshot(cx));
1664            let snapshot = editor.buffer().read(cx).snapshot(cx);
1665
1666            selection.goal = SelectionGoal::None;
1667
1668            let selection_start = snapshot.point_to_offset(selection.start);
1669
1670            (
1671                selection.map(|point| snapshot.point_to_offset(point)),
1672                editor.display_map.update(cx, |display_map, cx| {
1673                    display_map
1674                        .snapshot(cx)
1675                        .crease_snapshot
1676                        .creases_in_range(
1677                            MultiBufferRow(selection.start.row)
1678                                ..MultiBufferRow(selection.end.row + 1),
1679                            &snapshot,
1680                        )
1681                        .filter_map(|crease| {
1682                            if let Crease::Inline {
1683                                range, metadata, ..
1684                            } = &crease
1685                            {
1686                                let metadata = metadata.as_ref()?;
1687                                let start = range
1688                                    .start
1689                                    .to_offset(&snapshot)
1690                                    .saturating_sub(selection_start);
1691                                let end = range
1692                                    .end
1693                                    .to_offset(&snapshot)
1694                                    .saturating_sub(selection_start);
1695
1696                                let range_relative_to_selection = start..end;
1697                                if !range_relative_to_selection.is_empty() {
1698                                    return Some(SelectedCreaseMetadata {
1699                                        range_relative_to_selection,
1700                                        crease: metadata.clone(),
1701                                    });
1702                                }
1703                            }
1704                            None
1705                        })
1706                        .collect::<Vec<_>>()
1707                }),
1708            )
1709        });
1710
1711        let text_thread = self.text_thread.read(cx);
1712
1713        let mut text = String::new();
1714
1715        // If selection is empty, we want to copy the entire line
1716        if selection.range().is_empty() {
1717            let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
1718            let point = snapshot.offset_to_point(selection.range().start);
1719            selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
1720            selection.end = snapshot
1721                .point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
1722            for chunk in snapshot.text_for_range(selection.range()) {
1723                text.push_str(chunk);
1724            }
1725        } else {
1726            for message in text_thread.messages(cx) {
1727                if message.offset_range.start >= selection.range().end.0 {
1728                    break;
1729                } else if message.offset_range.end >= selection.range().start.0 {
1730                    let range = cmp::max(message.offset_range.start, selection.range().start.0)
1731                        ..cmp::min(message.offset_range.end, selection.range().end.0);
1732                    if !range.is_empty() {
1733                        for chunk in text_thread.buffer().read(cx).text_for_range(range) {
1734                            text.push_str(chunk);
1735                        }
1736                        if message.offset_range.end < selection.range().end.0 {
1737                            text.push('\n');
1738                        }
1739                    }
1740                }
1741            }
1742        }
1743        (text, CopyMetadata { creases }, vec![selection])
1744    }
1745
1746    fn paste(
1747        &mut self,
1748        action: &editor::actions::Paste,
1749        window: &mut Window,
1750        cx: &mut Context<Self>,
1751    ) {
1752        let Some(workspace) = self.workspace.upgrade() else {
1753            return;
1754        };
1755        let editor_clipboard_selections = cx
1756            .read_from_clipboard()
1757            .and_then(|item| item.entries().first().cloned())
1758            .and_then(|entry| match entry {
1759                ClipboardEntry::String(text) => {
1760                    text.metadata_json::<Vec<editor::ClipboardSelection>>()
1761                }
1762                _ => None,
1763            });
1764
1765        // Insert creases for pasted clipboard selections that:
1766        // 1. Contain exactly one selection
1767        // 2. Have an associated file path
1768        // 3. Span multiple lines (not single-line selections)
1769        // 4. Belong to a file that exists in the current project
1770        let should_insert_creases = util::maybe!({
1771            let selections = editor_clipboard_selections.as_ref()?;
1772            if selections.len() > 1 {
1773                return Some(false);
1774            }
1775            let selection = selections.first()?;
1776            let file_path = selection.file_path.as_ref()?;
1777            let line_range = selection.line_range.as_ref()?;
1778
1779            if line_range.start() == line_range.end() {
1780                return Some(false);
1781            }
1782
1783            Some(
1784                workspace
1785                    .read(cx)
1786                    .project()
1787                    .read(cx)
1788                    .project_path_for_absolute_path(file_path, cx)
1789                    .is_some(),
1790            )
1791        })
1792        .unwrap_or(false);
1793
1794        if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
1795            if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
1796                if let Some(selections) = editor_clipboard_selections {
1797                    cx.stop_propagation();
1798
1799                    let text = clipboard_text.text();
1800                    self.editor.update(cx, |editor, cx| {
1801                        let mut current_offset = 0;
1802                        let weak_editor = cx.entity().downgrade();
1803
1804                        for selection in selections {
1805                            if let (Some(file_path), Some(line_range)) =
1806                                (selection.file_path, selection.line_range)
1807                            {
1808                                let selected_text =
1809                                    &text[current_offset..current_offset + selection.len];
1810                                let fence = assistant_slash_commands::codeblock_fence_for_path(
1811                                    file_path.to_str(),
1812                                    Some(line_range.clone()),
1813                                );
1814                                let formatted_text = format!("{fence}{selected_text}\n```");
1815
1816                                let insert_point = editor
1817                                    .selections
1818                                    .newest::<Point>(&editor.display_snapshot(cx))
1819                                    .head();
1820                                let start_row = MultiBufferRow(insert_point.row);
1821
1822                                editor.insert(&formatted_text, window, cx);
1823
1824                                let snapshot = editor.buffer().read(cx).snapshot(cx);
1825                                let anchor_before = snapshot.anchor_after(insert_point);
1826                                let anchor_after = editor
1827                                    .selections
1828                                    .newest_anchor()
1829                                    .head()
1830                                    .bias_left(&snapshot);
1831
1832                                editor.insert("\n", window, cx);
1833
1834                                let crease_text = acp_thread::selection_name(
1835                                    Some(file_path.as_ref()),
1836                                    &line_range,
1837                                );
1838
1839                                let fold_placeholder = quote_selection_fold_placeholder(
1840                                    crease_text,
1841                                    weak_editor.clone(),
1842                                );
1843                                let crease = Crease::inline(
1844                                    anchor_before..anchor_after,
1845                                    fold_placeholder,
1846                                    render_quote_selection_output_toggle,
1847                                    |_, _, _, _| Empty.into_any(),
1848                                );
1849                                editor.insert_creases(vec![crease], cx);
1850                                editor.fold_at(start_row, window, cx);
1851
1852                                current_offset += selection.len;
1853                                if !selection.is_entire_line && current_offset < text.len() {
1854                                    current_offset += 1;
1855                                }
1856                            }
1857                        }
1858                    });
1859                    return;
1860                }
1861            }
1862        }
1863
1864        cx.stop_propagation();
1865
1866        let mut images = if let Some(item) = cx.read_from_clipboard() {
1867            item.into_entries()
1868                .filter_map(|entry| {
1869                    if let ClipboardEntry::Image(image) = entry {
1870                        Some(image)
1871                    } else {
1872                        None
1873                    }
1874                })
1875                .collect()
1876        } else {
1877            Vec::new()
1878        };
1879
1880        if let Some(paths) = cx.read_from_clipboard() {
1881            for path in paths
1882                .into_entries()
1883                .filter_map(|entry| {
1884                    if let ClipboardEntry::ExternalPaths(paths) = entry {
1885                        Some(paths.paths().to_owned())
1886                    } else {
1887                        None
1888                    }
1889                })
1890                .flatten()
1891            {
1892                let Ok(content) = std::fs::read(path) else {
1893                    continue;
1894                };
1895                let Ok(format) = image::guess_format(&content) else {
1896                    continue;
1897                };
1898                images.push(gpui::Image::from_bytes(
1899                    match format {
1900                        image::ImageFormat::Png => gpui::ImageFormat::Png,
1901                        image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
1902                        image::ImageFormat::WebP => gpui::ImageFormat::Webp,
1903                        image::ImageFormat::Gif => gpui::ImageFormat::Gif,
1904                        image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
1905                        image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
1906                        image::ImageFormat::Ico => gpui::ImageFormat::Ico,
1907                        _ => continue,
1908                    },
1909                    content,
1910                ));
1911            }
1912        }
1913
1914        let metadata = if let Some(item) = cx.read_from_clipboard() {
1915            item.entries().first().and_then(|entry| {
1916                if let ClipboardEntry::String(text) = entry {
1917                    text.metadata_json::<CopyMetadata>()
1918                } else {
1919                    None
1920                }
1921            })
1922        } else {
1923            None
1924        };
1925
1926        if images.is_empty() {
1927            self.editor.update(cx, |editor, cx| {
1928                let paste_position = editor
1929                    .selections
1930                    .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
1931                    .head();
1932                editor.paste(action, window, cx);
1933
1934                if let Some(metadata) = metadata {
1935                    let buffer = editor.buffer().read(cx).snapshot(cx);
1936
1937                    let mut buffer_rows_to_fold = BTreeSet::new();
1938                    let weak_editor = cx.entity().downgrade();
1939                    editor.insert_creases(
1940                        metadata.creases.into_iter().map(|metadata| {
1941                            let start = buffer.anchor_after(
1942                                paste_position + metadata.range_relative_to_selection.start,
1943                            );
1944                            let end = buffer.anchor_before(
1945                                paste_position + metadata.range_relative_to_selection.end,
1946                            );
1947
1948                            let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1949                            buffer_rows_to_fold.insert(buffer_row);
1950                            Crease::inline(
1951                                start..end,
1952                                FoldPlaceholder {
1953                                    render: render_fold_icon_button(
1954                                        weak_editor.clone(),
1955                                        metadata.crease.icon_path.clone(),
1956                                        metadata.crease.label.clone(),
1957                                    ),
1958                                    ..Default::default()
1959                                },
1960                                render_slash_command_output_toggle,
1961                                |_, _, _, _| Empty.into_any(),
1962                            )
1963                            .with_metadata(metadata.crease)
1964                        }),
1965                        cx,
1966                    );
1967                    for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1968                        editor.fold_at(buffer_row, window, cx);
1969                    }
1970                }
1971            });
1972        } else {
1973            let mut image_positions = Vec::new();
1974            self.editor.update(cx, |editor, cx| {
1975                editor.transact(window, cx, |editor, _window, cx| {
1976                    let edits = editor
1977                        .selections
1978                        .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
1979                        .into_iter()
1980                        .map(|selection| (selection.start..selection.end, "\n"));
1981                    editor.edit(edits, cx);
1982
1983                    let snapshot = editor.buffer().read(cx).snapshot(cx);
1984                    for selection in editor
1985                        .selections
1986                        .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
1987                    {
1988                        image_positions.push(snapshot.anchor_before(selection.end));
1989                    }
1990                });
1991            });
1992
1993            self.text_thread.update(cx, |text_thread, cx| {
1994                for image in images {
1995                    let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
1996                    else {
1997                        continue;
1998                    };
1999                    let image_id = image.id();
2000                    let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
2001
2002                    for image_position in image_positions.iter() {
2003                        text_thread.insert_content(
2004                            Content::Image {
2005                                anchor: image_position.text_anchor,
2006                                image_id,
2007                                image: image_task.clone(),
2008                                render_image: render_image.clone(),
2009                            },
2010                            cx,
2011                        );
2012                    }
2013                }
2014            });
2015        }
2016    }
2017
2018    fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
2019        self.editor.update(cx, |editor, cx| {
2020            editor.paste(&editor::actions::Paste, window, cx);
2021        });
2022    }
2023
2024    fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
2025        self.editor.update(cx, |editor, cx| {
2026            let buffer = editor.buffer().read(cx).snapshot(cx);
2027            let excerpt_id = *buffer.as_singleton().unwrap().0;
2028            let old_blocks = std::mem::take(&mut self.image_blocks);
2029            let new_blocks = self
2030                .text_thread
2031                .read(cx)
2032                .contents(cx)
2033                .map(
2034                    |Content::Image {
2035                         anchor,
2036                         render_image,
2037                         ..
2038                     }| (anchor, render_image),
2039                )
2040                .filter_map(|(anchor, render_image)| {
2041                    const MAX_HEIGHT_IN_LINES: u32 = 8;
2042                    let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
2043                    let image = render_image;
2044                    anchor.is_valid(&buffer).then(|| BlockProperties {
2045                        placement: BlockPlacement::Above(anchor),
2046                        height: Some(MAX_HEIGHT_IN_LINES),
2047                        style: BlockStyle::Sticky,
2048                        render: Arc::new(move |cx| {
2049                            let image_size = size_for_image(
2050                                &image,
2051                                size(
2052                                    cx.max_width - cx.margins.gutter.full_width(),
2053                                    MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
2054                                ),
2055                            );
2056                            h_flex()
2057                                .pl(cx.margins.gutter.full_width())
2058                                .child(
2059                                    img(image.clone())
2060                                        .object_fit(gpui::ObjectFit::ScaleDown)
2061                                        .w(image_size.width)
2062                                        .h(image_size.height),
2063                                )
2064                                .into_any_element()
2065                        }),
2066                        priority: 0,
2067                    })
2068                })
2069                .collect::<Vec<_>>();
2070
2071            editor.remove_blocks(old_blocks, None, cx);
2072            let ids = editor.insert_blocks(new_blocks, None, cx);
2073            self.image_blocks = HashSet::from_iter(ids);
2074        });
2075    }
2076
2077    fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context<Self>) {
2078        self.text_thread.update(cx, |text_thread, cx| {
2079            let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
2080            for selection in selections.as_ref() {
2081                let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2082                let range = selection
2083                    .map(|endpoint| endpoint.to_offset(&buffer))
2084                    .range();
2085                text_thread.split_message(range.start.0..range.end.0, cx);
2086            }
2087        });
2088    }
2089
2090    fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
2091        self.text_thread.update(cx, |text_thread, cx| {
2092            text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2093        });
2094    }
2095
2096    pub fn title(&self, cx: &App) -> SharedString {
2097        self.text_thread.read(cx).summary().or_default()
2098    }
2099
2100    pub fn regenerate_summary(&mut self, cx: &mut Context<Self>) {
2101        self.text_thread
2102            .update(cx, |text_thread, cx| text_thread.summarize(true, cx));
2103    }
2104
2105    fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
2106        let (token_count_color, token_count, max_token_count, tooltip) =
2107            match token_state(&self.text_thread, cx)? {
2108                TokenState::NoTokensLeft {
2109                    max_token_count,
2110                    token_count,
2111                } => (
2112                    Color::Error,
2113                    token_count,
2114                    max_token_count,
2115                    Some("Token Limit Reached"),
2116                ),
2117                TokenState::HasMoreTokens {
2118                    max_token_count,
2119                    token_count,
2120                    over_warn_threshold,
2121                } => {
2122                    let (color, tooltip) = if over_warn_threshold {
2123                        (Color::Warning, Some("Token Limit is Close to Exhaustion"))
2124                    } else {
2125                        (Color::Muted, None)
2126                    };
2127                    (color, token_count, max_token_count, tooltip)
2128                }
2129            };
2130
2131        Some(
2132            h_flex()
2133                .id("token-count")
2134                .gap_0p5()
2135                .child(
2136                    Label::new(humanize_token_count(token_count))
2137                        .size(LabelSize::Small)
2138                        .color(token_count_color),
2139                )
2140                .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2141                .child(
2142                    Label::new(humanize_token_count(max_token_count))
2143                        .size(LabelSize::Small)
2144                        .color(Color::Muted),
2145                )
2146                .when_some(tooltip, |element, tooltip| {
2147                    element.tooltip(Tooltip::text(tooltip))
2148                }),
2149        )
2150    }
2151
2152    fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2153        let focus_handle = self.focus_handle(cx);
2154
2155        let (style, tooltip) = match token_state(&self.text_thread, cx) {
2156            Some(TokenState::NoTokensLeft { .. }) => (
2157                ButtonStyle::Tinted(TintColor::Error),
2158                Some(Tooltip::text("Token limit reached")(window, cx)),
2159            ),
2160            Some(TokenState::HasMoreTokens {
2161                over_warn_threshold,
2162                ..
2163            }) => {
2164                let (style, tooltip) = if over_warn_threshold {
2165                    (
2166                        ButtonStyle::Tinted(TintColor::Warning),
2167                        Some(Tooltip::text("Token limit is close to exhaustion")(
2168                            window, cx,
2169                        )),
2170                    )
2171                } else {
2172                    (ButtonStyle::Filled, None)
2173                };
2174                (style, tooltip)
2175            }
2176            None => (ButtonStyle::Filled, None),
2177        };
2178
2179        Button::new("send_button", "Send")
2180            .label_size(LabelSize::Small)
2181            .disabled(self.sending_disabled(cx))
2182            .style(style)
2183            .when_some(tooltip, |button, tooltip| {
2184                button.tooltip(move |_, _| tooltip.clone())
2185            })
2186            .layer(ElevationIndex::ModalSurface)
2187            .key_binding(
2188                KeyBinding::for_action_in(&Assist, &focus_handle, cx)
2189                    .map(|kb| kb.size(rems_from_px(12.))),
2190            )
2191            .on_click(move |_event, window, cx| {
2192                focus_handle.dispatch_action(&Assist, window, cx);
2193            })
2194    }
2195
2196    /// Whether or not we should allow messages to be sent.
2197    /// Will return false if the selected provided has a configuration error or
2198    /// if the user has not accepted the terms of service for this provider.
2199    fn sending_disabled(&self, cx: &mut Context<'_, TextThreadEditor>) -> bool {
2200        let model_registry = LanguageModelRegistry::read_global(cx);
2201        let Some(configuration_error) =
2202            model_registry.configuration_error(model_registry.default_model(), cx)
2203        else {
2204            return false;
2205        };
2206
2207        match configuration_error {
2208            ConfigurationError::NoProvider
2209            | ConfigurationError::ModelNotFound
2210            | ConfigurationError::ProviderNotAuthenticated(_) => true,
2211        }
2212    }
2213
2214    fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
2215        slash_command_picker::SlashCommandSelector::new(
2216            self.slash_commands.clone(),
2217            cx.entity().downgrade(),
2218            IconButton::new("trigger", IconName::Plus)
2219                .icon_size(IconSize::Small)
2220                .icon_color(Color::Muted)
2221                .selected_icon_color(Color::Accent)
2222                .selected_style(ButtonStyle::Filled),
2223            move |_window, cx| {
2224                Tooltip::with_meta("Add Context", None, "Type / to insert via keyboard", cx)
2225            },
2226        )
2227    }
2228
2229    fn render_language_model_selector(
2230        &self,
2231        window: &mut Window,
2232        cx: &mut Context<Self>,
2233    ) -> impl IntoElement {
2234        let active_model = LanguageModelRegistry::read_global(cx)
2235            .default_model()
2236            .map(|default| default.model);
2237        let model_name = match active_model {
2238            Some(model) => model.name().0,
2239            None => SharedString::from("Select Model"),
2240        };
2241
2242        let active_provider = LanguageModelRegistry::read_global(cx)
2243            .default_model()
2244            .map(|default| default.provider);
2245
2246        let provider_icon = active_provider
2247            .as_ref()
2248            .map(|p| p.icon())
2249            .unwrap_or(IconOrSvg::Icon(IconName::Ai));
2250
2251        let focus_handle = self.editor().focus_handle(cx);
2252
2253        let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
2254            (Color::Accent, IconName::ChevronUp)
2255        } else {
2256            (Color::Muted, IconName::ChevronDown)
2257        };
2258
2259        let provider_icon_element = match provider_icon {
2260            IconOrSvg::Svg(path) => Icon::from_external_svg(path),
2261            IconOrSvg::Icon(name) => Icon::new(name),
2262        }
2263        .color(color)
2264        .size(IconSize::XSmall);
2265
2266        let show_cycle_row = self
2267            .language_model_selector
2268            .read(cx)
2269            .delegate
2270            .favorites_count()
2271            > 1;
2272
2273        let tooltip = Tooltip::element({
2274            move |_, _cx| {
2275                ModelSelectorTooltip::new(focus_handle.clone())
2276                    .show_cycle_row(show_cycle_row)
2277                    .into_any_element()
2278            }
2279        });
2280
2281        PickerPopoverMenu::new(
2282            self.language_model_selector.clone(),
2283            ButtonLike::new("active-model")
2284                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
2285                .child(
2286                    h_flex()
2287                        .gap_0p5()
2288                        .child(provider_icon_element)
2289                        .child(
2290                            Label::new(model_name)
2291                                .color(color)
2292                                .size(LabelSize::Small)
2293                                .ml_0p5(),
2294                        )
2295                        .child(Icon::new(icon).color(color).size(IconSize::XSmall)),
2296                ),
2297            tooltip,
2298            gpui::Corner::BottomRight,
2299            cx,
2300        )
2301        .with_handle(self.language_model_selector_menu_handle.clone())
2302        .render(window, cx)
2303    }
2304
2305    fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2306        let last_error = self.last_error.as_ref()?;
2307
2308        Some(
2309            div()
2310                .absolute()
2311                .right_3()
2312                .bottom_12()
2313                .max_w_96()
2314                .py_2()
2315                .px_3()
2316                .elevation_2(cx)
2317                .occlude()
2318                .child(match last_error {
2319                    AssistError::PaymentRequired => self.render_payment_required_error(cx),
2320                    AssistError::Message(error_message) => {
2321                        self.render_assist_error(error_message, cx)
2322                    }
2323                })
2324                .into_any(),
2325        )
2326    }
2327
2328    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2329        const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
2330
2331        v_flex()
2332            .gap_0p5()
2333            .child(
2334                h_flex()
2335                    .gap_1p5()
2336                    .items_center()
2337                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2338                    .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2339            )
2340            .child(
2341                div()
2342                    .id("error-message")
2343                    .max_h_24()
2344                    .overflow_y_scroll()
2345                    .child(Label::new(ERROR_MESSAGE)),
2346            )
2347            .child(
2348                h_flex()
2349                    .justify_end()
2350                    .mt_1()
2351                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2352                        |this, _, _window, cx| {
2353                            this.last_error = None;
2354                            cx.open_url(&zed_urls::account_url(cx));
2355                            cx.notify();
2356                        },
2357                    )))
2358                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2359                        |this, _, _window, cx| {
2360                            this.last_error = None;
2361                            cx.notify();
2362                        },
2363                    ))),
2364            )
2365            .into_any()
2366    }
2367
2368    fn render_assist_error(
2369        &self,
2370        error_message: &SharedString,
2371        cx: &mut Context<Self>,
2372    ) -> AnyElement {
2373        v_flex()
2374            .gap_0p5()
2375            .child(
2376                h_flex()
2377                    .gap_1p5()
2378                    .items_center()
2379                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2380                    .child(
2381                        Label::new("Error interacting with language model")
2382                            .weight(FontWeight::MEDIUM),
2383                    ),
2384            )
2385            .child(
2386                div()
2387                    .id("error-message")
2388                    .max_h_32()
2389                    .overflow_y_scroll()
2390                    .child(Label::new(error_message.clone())),
2391            )
2392            .child(
2393                h_flex()
2394                    .justify_end()
2395                    .mt_1()
2396                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2397                        |this, _, _window, cx| {
2398                            this.last_error = None;
2399                            cx.notify();
2400                        },
2401                    ))),
2402            )
2403            .into_any()
2404    }
2405}
2406
2407/// Returns the contents of the *outermost* fenced code block that contains the given offset.
2408fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
2409    const CODE_BLOCK_NODE: &str = "fenced_code_block";
2410    const CODE_BLOCK_CONTENT: &str = "code_fence_content";
2411
2412    let layer = snapshot.syntax_layers().next()?;
2413
2414    let root_node = layer.node();
2415    let mut cursor = root_node.walk();
2416
2417    // Go to the first child for the given offset
2418    while cursor.goto_first_child_for_byte(offset).is_some() {
2419        // If we're at the end of the node, go to the next one.
2420        // Example: if you have a fenced-code-block, and you're on the start of the line
2421        // right after the closing ```, you want to skip the fenced-code-block and
2422        // go to the next sibling.
2423        if cursor.node().end_byte() == offset {
2424            cursor.goto_next_sibling();
2425        }
2426
2427        if cursor.node().start_byte() > offset {
2428            break;
2429        }
2430
2431        // We found the fenced code block.
2432        if cursor.node().kind() == CODE_BLOCK_NODE {
2433            // Now we need to find the child node that contains the code.
2434            cursor.goto_first_child();
2435            loop {
2436                if cursor.node().kind() == CODE_BLOCK_CONTENT {
2437                    return Some(cursor.node().byte_range());
2438                }
2439                if !cursor.goto_next_sibling() {
2440                    break;
2441                }
2442            }
2443        }
2444    }
2445
2446    None
2447}
2448
2449fn render_thought_process_fold_icon_button(
2450    editor: WeakEntity<Editor>,
2451    status: ThoughtProcessStatus,
2452) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
2453    Arc::new(move |fold_id, fold_range, _cx| {
2454        let editor = editor.clone();
2455
2456        let button = ButtonLike::new(fold_id).layer(ElevationIndex::ElevatedSurface);
2457        let button = match status {
2458            ThoughtProcessStatus::Pending => button
2459                .child(
2460                    Icon::new(IconName::ToolThink)
2461                        .size(IconSize::Small)
2462                        .color(Color::Muted),
2463                )
2464                .child(
2465                    Label::new("Thinking…").color(Color::Muted).with_animation(
2466                        "pulsating-label",
2467                        Animation::new(Duration::from_secs(2))
2468                            .repeat()
2469                            .with_easing(pulsating_between(0.4, 0.8)),
2470                        |label, delta| label.alpha(delta),
2471                    ),
2472                ),
2473            ThoughtProcessStatus::Completed => button
2474                .style(ButtonStyle::Filled)
2475                .child(Icon::new(IconName::ToolThink).size(IconSize::Small))
2476                .child(Label::new("Thought Process").single_line()),
2477        };
2478
2479        button
2480            .on_click(move |_, window, cx| {
2481                editor
2482                    .update(cx, |editor, cx| {
2483                        let buffer_start = fold_range
2484                            .start
2485                            .to_point(&editor.buffer().read(cx).read(cx));
2486                        let buffer_row = MultiBufferRow(buffer_start.row);
2487                        editor.unfold_at(buffer_row, window, cx);
2488                    })
2489                    .ok();
2490            })
2491            .into_any_element()
2492    })
2493}
2494
2495fn render_fold_icon_button(
2496    editor: WeakEntity<Editor>,
2497    icon_path: SharedString,
2498    label: SharedString,
2499) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
2500    Arc::new(move |fold_id, fold_range, _cx| {
2501        let editor = editor.clone();
2502        ButtonLike::new(fold_id)
2503            .style(ButtonStyle::Filled)
2504            .layer(ElevationIndex::ElevatedSurface)
2505            .child(Icon::from_path(icon_path.clone()))
2506            .child(Label::new(label.clone()).single_line())
2507            .on_click(move |_, window, cx| {
2508                editor
2509                    .update(cx, |editor, cx| {
2510                        let buffer_start = fold_range
2511                            .start
2512                            .to_point(&editor.buffer().read(cx).read(cx));
2513                        let buffer_row = MultiBufferRow(buffer_start.row);
2514                        editor.unfold_at(buffer_row, window, cx);
2515                    })
2516                    .ok();
2517            })
2518            .into_any_element()
2519    })
2520}
2521
2522type ToggleFold = Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>;
2523
2524fn render_slash_command_output_toggle(
2525    row: MultiBufferRow,
2526    is_folded: bool,
2527    fold: ToggleFold,
2528    _window: &mut Window,
2529    _cx: &mut App,
2530) -> AnyElement {
2531    Disclosure::new(
2532        ("slash-command-output-fold-indicator", row.0 as u64),
2533        !is_folded,
2534    )
2535    .toggle_state(is_folded)
2536    .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2537    .into_any_element()
2538}
2539
2540pub fn fold_toggle(
2541    name: &'static str,
2542) -> impl Fn(
2543    MultiBufferRow,
2544    bool,
2545    Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
2546    &mut Window,
2547    &mut App,
2548) -> AnyElement {
2549    move |row, is_folded, fold, _window, _cx| {
2550        Disclosure::new((name, row.0 as u64), !is_folded)
2551            .toggle_state(is_folded)
2552            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2553            .into_any_element()
2554    }
2555}
2556
2557fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -> FoldPlaceholder {
2558    FoldPlaceholder {
2559        render: Arc::new({
2560            move |fold_id, fold_range, _cx| {
2561                let editor = editor.clone();
2562                ButtonLike::new(fold_id)
2563                    .style(ButtonStyle::Filled)
2564                    .layer(ElevationIndex::ElevatedSurface)
2565                    .child(Icon::new(IconName::TextSnippet))
2566                    .child(Label::new(title.clone()).single_line())
2567                    .on_click(move |_, window, cx| {
2568                        editor
2569                            .update(cx, |editor, cx| {
2570                                let buffer_start = fold_range
2571                                    .start
2572                                    .to_point(&editor.buffer().read(cx).read(cx));
2573                                let buffer_row = MultiBufferRow(buffer_start.row);
2574                                editor.unfold_at(buffer_row, window, cx);
2575                            })
2576                            .ok();
2577                    })
2578                    .into_any_element()
2579            }
2580        }),
2581        merge_adjacent: false,
2582        ..Default::default()
2583    }
2584}
2585
2586fn render_quote_selection_output_toggle(
2587    row: MultiBufferRow,
2588    is_folded: bool,
2589    fold: ToggleFold,
2590    _window: &mut Window,
2591    _cx: &mut App,
2592) -> AnyElement {
2593    Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
2594        .toggle_state(is_folded)
2595        .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2596        .into_any_element()
2597}
2598
2599fn render_pending_slash_command_gutter_decoration(
2600    row: MultiBufferRow,
2601    status: &PendingSlashCommandStatus,
2602    confirm_command: Arc<dyn Fn(&mut Window, &mut App)>,
2603) -> AnyElement {
2604    let mut icon = IconButton::new(
2605        ("slash-command-gutter-decoration", row.0),
2606        ui::IconName::TriangleRight,
2607    )
2608    .on_click(move |_e, window, cx| confirm_command(window, cx))
2609    .icon_size(ui::IconSize::Small)
2610    .size(ui::ButtonSize::None);
2611
2612    match status {
2613        PendingSlashCommandStatus::Idle => {
2614            icon = icon.icon_color(Color::Muted);
2615        }
2616        PendingSlashCommandStatus::Running { .. } => {
2617            icon = icon.toggle_state(true);
2618        }
2619        PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
2620    }
2621
2622    icon.into_any_element()
2623}
2624
2625#[derive(Debug, Clone, Serialize, Deserialize)]
2626struct CopyMetadata {
2627    creases: Vec<SelectedCreaseMetadata>,
2628}
2629
2630#[derive(Debug, Clone, Serialize, Deserialize)]
2631struct SelectedCreaseMetadata {
2632    range_relative_to_selection: Range<usize>,
2633    crease: CreaseMetadata,
2634}
2635
2636impl EventEmitter<EditorEvent> for TextThreadEditor {}
2637impl EventEmitter<SearchEvent> for TextThreadEditor {}
2638
2639impl Render for TextThreadEditor {
2640    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2641        let language_model_selector = self.language_model_selector_menu_handle.clone();
2642
2643        v_flex()
2644            .key_context("ContextEditor")
2645            .capture_action(cx.listener(TextThreadEditor::cancel))
2646            .capture_action(cx.listener(TextThreadEditor::save))
2647            .capture_action(cx.listener(TextThreadEditor::copy))
2648            .capture_action(cx.listener(TextThreadEditor::cut))
2649            .capture_action(cx.listener(TextThreadEditor::paste))
2650            .on_action(cx.listener(TextThreadEditor::paste_raw))
2651            .capture_action(cx.listener(TextThreadEditor::cycle_message_role))
2652            .capture_action(cx.listener(TextThreadEditor::confirm_command))
2653            .on_action(cx.listener(TextThreadEditor::assist))
2654            .on_action(cx.listener(TextThreadEditor::split))
2655            .on_action(move |_: &ToggleModelSelector, window, cx| {
2656                language_model_selector.toggle(window, cx);
2657            })
2658            .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
2659                this.language_model_selector.update(cx, |selector, cx| {
2660                    selector.delegate.cycle_favorite_models(window, cx);
2661                });
2662            }))
2663            .size_full()
2664            .child(
2665                div()
2666                    .flex_grow()
2667                    .bg(cx.theme().colors().editor_background)
2668                    .child(self.editor.clone()),
2669            )
2670            .children(self.render_last_error(cx))
2671            .child(
2672                h_flex()
2673                    .relative()
2674                    .py_2()
2675                    .pl_1p5()
2676                    .pr_2()
2677                    .w_full()
2678                    .justify_between()
2679                    .border_t_1()
2680                    .border_color(cx.theme().colors().border_variant)
2681                    .bg(cx.theme().colors().editor_background)
2682                    .child(
2683                        h_flex()
2684                            .gap_0p5()
2685                            .child(self.render_inject_context_menu(cx)),
2686                    )
2687                    .child(
2688                        h_flex()
2689                            .gap_2p5()
2690                            .children(self.render_remaining_tokens(cx))
2691                            .child(
2692                                h_flex()
2693                                    .gap_1()
2694                                    .child(self.render_language_model_selector(window, cx))
2695                                    .child(self.render_send_button(window, cx)),
2696                            ),
2697                    ),
2698            )
2699    }
2700}
2701
2702impl Focusable for TextThreadEditor {
2703    fn focus_handle(&self, cx: &App) -> FocusHandle {
2704        self.editor.focus_handle(cx)
2705    }
2706}
2707
2708impl Item for TextThreadEditor {
2709    type Event = editor::EditorEvent;
2710
2711    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
2712        util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()
2713    }
2714
2715    fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2716        match event {
2717            EditorEvent::Edited { .. } => {
2718                f(item::ItemEvent::Edit);
2719            }
2720            EditorEvent::TitleChanged => {
2721                f(item::ItemEvent::UpdateTab);
2722            }
2723            _ => {}
2724        }
2725    }
2726
2727    fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
2728        Some(self.title(cx).to_string().into())
2729    }
2730
2731    fn as_searchable(
2732        &self,
2733        handle: &Entity<Self>,
2734        _: &App,
2735    ) -> Option<Box<dyn SearchableItemHandle>> {
2736        Some(Box::new(handle.clone()))
2737    }
2738
2739    fn set_nav_history(
2740        &mut self,
2741        nav_history: pane::ItemNavHistory,
2742        window: &mut Window,
2743        cx: &mut Context<Self>,
2744    ) {
2745        self.editor.update(cx, |editor, cx| {
2746            Item::set_nav_history(editor, nav_history, window, cx)
2747        })
2748    }
2749
2750    fn navigate(
2751        &mut self,
2752        data: Arc<dyn Any + Send>,
2753        window: &mut Window,
2754        cx: &mut Context<Self>,
2755    ) -> bool {
2756        self.editor
2757            .update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
2758    }
2759
2760    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2761        self.editor
2762            .update(cx, |editor, cx| Item::deactivated(editor, window, cx))
2763    }
2764
2765    fn act_as_type<'a>(
2766        &'a self,
2767        type_id: TypeId,
2768        self_handle: &'a Entity<Self>,
2769        _: &'a App,
2770    ) -> Option<gpui::AnyEntity> {
2771        if type_id == TypeId::of::<Self>() {
2772            Some(self_handle.clone().into())
2773        } else if type_id == TypeId::of::<Editor>() {
2774            Some(self.editor.clone().into())
2775        } else {
2776            None
2777        }
2778    }
2779
2780    fn include_in_nav_history() -> bool {
2781        false
2782    }
2783}
2784
2785impl SearchableItem for TextThreadEditor {
2786    type Match = <Editor as SearchableItem>::Match;
2787
2788    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2789        self.editor.update(cx, |editor, cx| {
2790            editor.clear_matches(window, cx);
2791        });
2792    }
2793
2794    fn update_matches(
2795        &mut self,
2796        matches: &[Self::Match],
2797        active_match_index: Option<usize>,
2798        window: &mut Window,
2799        cx: &mut Context<Self>,
2800    ) {
2801        self.editor.update(cx, |editor, cx| {
2802            editor.update_matches(matches, active_match_index, window, cx)
2803        });
2804    }
2805
2806    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
2807        self.editor
2808            .update(cx, |editor, cx| editor.query_suggestion(window, cx))
2809    }
2810
2811    fn activate_match(
2812        &mut self,
2813        index: usize,
2814        matches: &[Self::Match],
2815        window: &mut Window,
2816        cx: &mut Context<Self>,
2817    ) {
2818        self.editor.update(cx, |editor, cx| {
2819            editor.activate_match(index, matches, window, cx);
2820        });
2821    }
2822
2823    fn select_matches(
2824        &mut self,
2825        matches: &[Self::Match],
2826        window: &mut Window,
2827        cx: &mut Context<Self>,
2828    ) {
2829        self.editor
2830            .update(cx, |editor, cx| editor.select_matches(matches, window, cx));
2831    }
2832
2833    fn replace(
2834        &mut self,
2835        identifier: &Self::Match,
2836        query: &project::search::SearchQuery,
2837        window: &mut Window,
2838        cx: &mut Context<Self>,
2839    ) {
2840        self.editor.update(cx, |editor, cx| {
2841            editor.replace(identifier, query, window, cx)
2842        });
2843    }
2844
2845    fn find_matches(
2846        &mut self,
2847        query: Arc<project::search::SearchQuery>,
2848        window: &mut Window,
2849        cx: &mut Context<Self>,
2850    ) -> Task<Vec<Self::Match>> {
2851        self.editor
2852            .update(cx, |editor, cx| editor.find_matches(query, window, cx))
2853    }
2854
2855    fn active_match_index(
2856        &mut self,
2857        direction: Direction,
2858        matches: &[Self::Match],
2859        window: &mut Window,
2860        cx: &mut Context<Self>,
2861    ) -> Option<usize> {
2862        self.editor.update(cx, |editor, cx| {
2863            editor.active_match_index(direction, matches, window, cx)
2864        })
2865    }
2866}
2867
2868impl FollowableItem for TextThreadEditor {
2869    fn remote_id(&self) -> Option<workspace::ViewId> {
2870        self.remote_id
2871    }
2872
2873    fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
2874        let context_id = self.text_thread.read(cx).id().to_proto();
2875        let editor_proto = self
2876            .editor
2877            .update(cx, |editor, cx| editor.to_state_proto(window, cx));
2878        Some(proto::view::Variant::ContextEditor(
2879            proto::view::ContextEditor {
2880                context_id,
2881                editor: if let Some(proto::view::Variant::Editor(proto)) = editor_proto {
2882                    Some(proto)
2883                } else {
2884                    None
2885                },
2886            },
2887        ))
2888    }
2889
2890    fn from_state_proto(
2891        workspace: Entity<Workspace>,
2892        id: workspace::ViewId,
2893        state: &mut Option<proto::view::Variant>,
2894        window: &mut Window,
2895        cx: &mut App,
2896    ) -> Option<Task<Result<Entity<Self>>>> {
2897        let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
2898            return None;
2899        };
2900        let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
2901            unreachable!()
2902        };
2903
2904        let text_thread_id = TextThreadId::from_proto(state.context_id);
2905        let editor_state = state.editor?;
2906
2907        let project = workspace.read(cx).project().clone();
2908        let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
2909
2910        let text_thread_editor_task = workspace.update(cx, |workspace, cx| {
2911            agent_panel_delegate.open_remote_text_thread(workspace, text_thread_id, window, cx)
2912        });
2913
2914        Some(window.spawn(cx, async move |cx| {
2915            let text_thread_editor = text_thread_editor_task.await?;
2916            text_thread_editor
2917                .update_in(cx, |text_thread_editor, window, cx| {
2918                    text_thread_editor.remote_id = Some(id);
2919                    text_thread_editor.editor.update(cx, |editor, cx| {
2920                        editor.apply_update_proto(
2921                            &project,
2922                            proto::update_view::Variant::Editor(proto::update_view::Editor {
2923                                selections: editor_state.selections,
2924                                pending_selection: editor_state.pending_selection,
2925                                scroll_top_anchor: editor_state.scroll_top_anchor,
2926                                scroll_x: editor_state.scroll_y,
2927                                scroll_y: editor_state.scroll_y,
2928                                ..Default::default()
2929                            }),
2930                            window,
2931                            cx,
2932                        )
2933                    })
2934                })?
2935                .await?;
2936            Ok(text_thread_editor)
2937        }))
2938    }
2939
2940    fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
2941        Editor::to_follow_event(event)
2942    }
2943
2944    fn add_event_to_update_proto(
2945        &self,
2946        event: &Self::Event,
2947        update: &mut Option<proto::update_view::Variant>,
2948        window: &mut Window,
2949        cx: &mut App,
2950    ) -> bool {
2951        self.editor.update(cx, |editor, cx| {
2952            editor.add_event_to_update_proto(event, update, window, cx)
2953        })
2954    }
2955
2956    fn apply_update_proto(
2957        &mut self,
2958        project: &Entity<Project>,
2959        message: proto::update_view::Variant,
2960        window: &mut Window,
2961        cx: &mut Context<Self>,
2962    ) -> Task<Result<()>> {
2963        self.editor.update(cx, |editor, cx| {
2964            editor.apply_update_proto(project, message, window, cx)
2965        })
2966    }
2967
2968    fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
2969        true
2970    }
2971
2972    fn set_leader_id(
2973        &mut self,
2974        leader_id: Option<CollaboratorId>,
2975        window: &mut Window,
2976        cx: &mut Context<Self>,
2977    ) {
2978        self.editor
2979            .update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
2980    }
2981
2982    fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<item::Dedup> {
2983        if existing.text_thread.read(cx).id() == self.text_thread.read(cx).id() {
2984            Some(item::Dedup::KeepExisting)
2985        } else {
2986            None
2987        }
2988    }
2989}
2990
2991enum PendingSlashCommand {}
2992
2993fn invoked_slash_command_fold_placeholder(
2994    command_id: InvokedSlashCommandId,
2995    text_thread: WeakEntity<TextThread>,
2996) -> FoldPlaceholder {
2997    FoldPlaceholder {
2998        constrain_width: false,
2999        merge_adjacent: false,
3000        render: Arc::new(move |fold_id, _, cx| {
3001            let Some(text_thread) = text_thread.upgrade() else {
3002                return Empty.into_any();
3003            };
3004
3005            let Some(command) = text_thread.read(cx).invoked_slash_command(&command_id) else {
3006                return Empty.into_any();
3007            };
3008
3009            h_flex()
3010                .id(fold_id)
3011                .px_1()
3012                .ml_6()
3013                .gap_2()
3014                .bg(cx.theme().colors().surface_background)
3015                .rounded_sm()
3016                .child(Label::new(format!("/{}", command.name)))
3017                .map(|parent| match &command.status {
3018                    InvokedSlashCommandStatus::Running(_) => {
3019                        parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4))
3020                    }
3021                    InvokedSlashCommandStatus::Error(message) => parent.child(
3022                        Label::new(format!("error: {message}"))
3023                            .single_line()
3024                            .color(Color::Error),
3025                    ),
3026                    InvokedSlashCommandStatus::Finished => parent,
3027                })
3028                .into_any_element()
3029        }),
3030        type_tag: Some(TypeId::of::<PendingSlashCommand>()),
3031    }
3032}
3033
3034enum TokenState {
3035    NoTokensLeft {
3036        max_token_count: u64,
3037        token_count: u64,
3038    },
3039    HasMoreTokens {
3040        max_token_count: u64,
3041        token_count: u64,
3042        over_warn_threshold: bool,
3043    },
3044}
3045
3046fn token_state(text_thread: &Entity<TextThread>, cx: &App) -> Option<TokenState> {
3047    const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3048
3049    let model = LanguageModelRegistry::read_global(cx)
3050        .default_model()?
3051        .model;
3052    let token_count = text_thread.read(cx).token_count()?;
3053    let max_token_count = model.max_token_count();
3054    let token_state = if max_token_count.saturating_sub(token_count) == 0 {
3055        TokenState::NoTokensLeft {
3056            max_token_count,
3057            token_count,
3058        }
3059    } else {
3060        let over_warn_threshold =
3061            token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3062        TokenState::HasMoreTokens {
3063            max_token_count,
3064            token_count,
3065            over_warn_threshold,
3066        }
3067    };
3068    Some(token_state)
3069}
3070
3071fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
3072    let image_size = data
3073        .size(0)
3074        .map(|dimension| Pixels::from(u32::from(dimension)));
3075    let image_ratio = image_size.width / image_size.height;
3076    let bounds_ratio = max_size.width / max_size.height;
3077
3078    if image_size.width > max_size.width || image_size.height > max_size.height {
3079        if bounds_ratio > image_ratio {
3080            size(
3081                image_size.width * (max_size.height / image_size.height),
3082                max_size.height,
3083            )
3084        } else {
3085            size(
3086                max_size.width,
3087                image_size.height * (max_size.width / image_size.width),
3088            )
3089        }
3090    } else {
3091        size(image_size.width, image_size.height)
3092    }
3093}
3094
3095pub fn humanize_token_count(count: u64) -> String {
3096    match count {
3097        0..=999 => count.to_string(),
3098        1000..=9999 => {
3099            let thousands = count / 1000;
3100            let hundreds = (count % 1000 + 50) / 100;
3101            if hundreds == 0 {
3102                format!("{}k", thousands)
3103            } else if hundreds == 10 {
3104                format!("{}k", thousands + 1)
3105            } else {
3106                format!("{}.{}k", thousands, hundreds)
3107            }
3108        }
3109        1_000_000..=9_999_999 => {
3110            let millions = count / 1_000_000;
3111            let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
3112            if hundred_thousands == 0 {
3113                format!("{}M", millions)
3114            } else if hundred_thousands == 10 {
3115                format!("{}M", millions + 1)
3116            } else {
3117                format!("{}.{}M", millions, hundred_thousands)
3118            }
3119        }
3120        10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
3121        _ => format!("{}k", (count + 500) / 1000),
3122    }
3123}
3124
3125pub fn make_lsp_adapter_delegate(
3126    project: &Entity<Project>,
3127    cx: &mut App,
3128) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
3129    project.update(cx, |project, cx| {
3130        // TODO: Find the right worktree.
3131        let Some(worktree) = project.worktrees(cx).next() else {
3132            return Ok(None::<Arc<dyn LspAdapterDelegate>>);
3133        };
3134        let http_client = project.client().http_client();
3135        project.lsp_store().update(cx, |_, cx| {
3136            Ok(Some(LocalLspAdapterDelegate::new(
3137                project.languages().clone(),
3138                project.environment(),
3139                cx.weak_entity(),
3140                &worktree,
3141                http_client,
3142                project.fs().clone(),
3143                cx,
3144            ) as Arc<dyn LspAdapterDelegate>))
3145        })
3146    })
3147}
3148
3149#[cfg(test)]
3150mod tests {
3151    use super::*;
3152    use editor::{MultiBufferOffset, SelectionEffects};
3153    use fs::FakeFs;
3154    use gpui::{App, TestAppContext, VisualTestContext};
3155    use indoc::indoc;
3156    use language::{Buffer, LanguageRegistry};
3157    use pretty_assertions::assert_eq;
3158    use prompt_store::PromptBuilder;
3159    use text::OffsetRangeExt;
3160    use unindent::Unindent;
3161    use util::path;
3162
3163    #[gpui::test]
3164    async fn test_copy_paste_whole_message(cx: &mut TestAppContext) {
3165        let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(vec![
3166            (Role::User, "What is the Zed editor?"),
3167            (
3168                Role::Assistant,
3169                "Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.",
3170            ),
3171            (Role::User, ""),
3172        ],cx).await;
3173
3174        // Select & Copy whole user message
3175        assert_copy_paste_text_thread_editor(
3176            &text_thread_editor,
3177            message_range(&context, 0, &mut cx),
3178            indoc! {"
3179                What is the Zed editor?
3180                Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3181                What is the Zed editor?
3182            "},
3183            &mut cx,
3184        );
3185
3186        // Select & Copy whole assistant message
3187        assert_copy_paste_text_thread_editor(
3188            &text_thread_editor,
3189            message_range(&context, 1, &mut cx),
3190            indoc! {"
3191                What is the Zed editor?
3192                Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3193                What is the Zed editor?
3194                Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3195            "},
3196            &mut cx,
3197        );
3198    }
3199
3200    #[gpui::test]
3201    async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
3202        let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(
3203            vec![
3204                (Role::User, "user1"),
3205                (Role::Assistant, "assistant1"),
3206                (Role::Assistant, "assistant2"),
3207                (Role::User, ""),
3208            ],
3209            cx,
3210        )
3211        .await;
3212
3213        // Copy and paste first assistant message
3214        let message_2_range = message_range(&context, 1, &mut cx);
3215        assert_copy_paste_text_thread_editor(
3216            &text_thread_editor,
3217            message_2_range.start..message_2_range.start,
3218            indoc! {"
3219                user1
3220                assistant1
3221                assistant2
3222                assistant1
3223            "},
3224            &mut cx,
3225        );
3226
3227        // Copy and cut second assistant message
3228        let message_3_range = message_range(&context, 2, &mut cx);
3229        assert_copy_paste_text_thread_editor(
3230            &text_thread_editor,
3231            message_3_range.start..message_3_range.start,
3232            indoc! {"
3233                user1
3234                assistant1
3235                assistant2
3236                assistant1
3237                assistant2
3238            "},
3239            &mut cx,
3240        );
3241    }
3242
3243    #[gpui::test]
3244    fn test_find_code_blocks(cx: &mut App) {
3245        let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
3246
3247        let buffer = cx.new(|cx| {
3248            let text = r#"
3249                line 0
3250                line 1
3251                ```rust
3252                fn main() {}
3253                ```
3254                line 5
3255                line 6
3256                line 7
3257                ```go
3258                func main() {}
3259                ```
3260                line 11
3261                ```
3262                this is plain text code block
3263                ```
3264
3265                ```go
3266                func another() {}
3267                ```
3268                line 19
3269            "#
3270            .unindent();
3271            let mut buffer = Buffer::local(text, cx);
3272            buffer.set_language(Some(markdown.clone()), cx);
3273            buffer
3274        });
3275        let snapshot = buffer.read(cx).snapshot();
3276
3277        let code_blocks = vec![
3278            Point::new(3, 0)..Point::new(4, 0),
3279            Point::new(9, 0)..Point::new(10, 0),
3280            Point::new(13, 0)..Point::new(14, 0),
3281            Point::new(17, 0)..Point::new(18, 0),
3282        ]
3283        .into_iter()
3284        .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
3285        .collect::<Vec<_>>();
3286
3287        let expected_results = vec![
3288            (0, None),
3289            (1, None),
3290            (2, Some(code_blocks[0].clone())),
3291            (3, Some(code_blocks[0].clone())),
3292            (4, Some(code_blocks[0].clone())),
3293            (5, None),
3294            (6, None),
3295            (7, None),
3296            (8, Some(code_blocks[1].clone())),
3297            (9, Some(code_blocks[1].clone())),
3298            (10, Some(code_blocks[1].clone())),
3299            (11, None),
3300            (12, Some(code_blocks[2].clone())),
3301            (13, Some(code_blocks[2].clone())),
3302            (14, Some(code_blocks[2].clone())),
3303            (15, None),
3304            (16, Some(code_blocks[3].clone())),
3305            (17, Some(code_blocks[3].clone())),
3306            (18, Some(code_blocks[3].clone())),
3307            (19, None),
3308        ];
3309
3310        for (row, expected) in expected_results {
3311            let offset = snapshot.point_to_offset(Point::new(row, 0));
3312            let range = find_surrounding_code_block(&snapshot, offset);
3313            assert_eq!(range, expected, "unexpected result on row {:?}", row);
3314        }
3315    }
3316
3317    async fn setup_text_thread_editor_text(
3318        messages: Vec<(Role, &str)>,
3319        cx: &mut TestAppContext,
3320    ) -> (
3321        Entity<TextThread>,
3322        Entity<TextThreadEditor>,
3323        VisualTestContext,
3324    ) {
3325        cx.update(init_test);
3326
3327        let fs = FakeFs::new(cx.executor());
3328        let text_thread = create_text_thread_with_messages(messages, cx);
3329
3330        let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
3331        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3332        let workspace = window.root(cx).unwrap();
3333        let mut cx = VisualTestContext::from_window(*window, cx);
3334
3335        let text_thread_editor = window
3336            .update(&mut cx, |_, window, cx| {
3337                cx.new(|cx| {
3338                    TextThreadEditor::for_text_thread(
3339                        text_thread.clone(),
3340                        fs,
3341                        workspace.downgrade(),
3342                        project,
3343                        None,
3344                        window,
3345                        cx,
3346                    )
3347                })
3348            })
3349            .unwrap();
3350
3351        (text_thread, text_thread_editor, cx)
3352    }
3353
3354    fn message_range(
3355        text_thread: &Entity<TextThread>,
3356        message_ix: usize,
3357        cx: &mut TestAppContext,
3358    ) -> Range<MultiBufferOffset> {
3359        let range = text_thread.update(cx, |text_thread, cx| {
3360            text_thread
3361                .messages(cx)
3362                .nth(message_ix)
3363                .unwrap()
3364                .anchor_range
3365                .to_offset(&text_thread.buffer().read(cx).snapshot())
3366        });
3367        MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
3368    }
3369
3370    fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>(
3371        text_thread_editor: &Entity<TextThreadEditor>,
3372        range: Range<T>,
3373        expected_text: &str,
3374        cx: &mut VisualTestContext,
3375    ) {
3376        text_thread_editor.update_in(cx, |text_thread_editor, window, cx| {
3377            text_thread_editor.editor.update(cx, |editor, cx| {
3378                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3379                    s.select_ranges([range])
3380                });
3381            });
3382
3383            text_thread_editor.copy(&Default::default(), window, cx);
3384
3385            text_thread_editor.editor.update(cx, |editor, cx| {
3386                editor.move_to_end(&Default::default(), window, cx);
3387            });
3388
3389            text_thread_editor.paste(&Default::default(), window, cx);
3390
3391            text_thread_editor.editor.update(cx, |editor, cx| {
3392                assert_eq!(editor.text(cx), expected_text);
3393            });
3394        });
3395    }
3396
3397    fn create_text_thread_with_messages(
3398        mut messages: Vec<(Role, &str)>,
3399        cx: &mut TestAppContext,
3400    ) -> Entity<TextThread> {
3401        let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3402        let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
3403        cx.new(|cx| {
3404            let mut text_thread = TextThread::local(
3405                registry,
3406                prompt_builder.clone(),
3407                Arc::new(SlashCommandWorkingSet::default()),
3408                cx,
3409            );
3410            let mut message_1 = text_thread.messages(cx).next().unwrap();
3411            let (role, text) = messages.remove(0);
3412
3413            loop {
3414                if role == message_1.role {
3415                    text_thread.buffer().update(cx, |buffer, cx| {
3416                        buffer.edit([(message_1.offset_range, text)], None, cx);
3417                    });
3418                    break;
3419                }
3420                let mut ids = HashSet::default();
3421                ids.insert(message_1.id);
3422                text_thread.cycle_message_roles(ids, cx);
3423                message_1 = text_thread.messages(cx).next().unwrap();
3424            }
3425
3426            let mut last_message_id = message_1.id;
3427            for (role, text) in messages {
3428                text_thread.insert_message_after(last_message_id, role, MessageStatus::Done, cx);
3429                let message = text_thread.messages(cx).last().unwrap();
3430                last_message_id = message.id;
3431                text_thread.buffer().update(cx, |buffer, cx| {
3432                    buffer.edit([(message.offset_range, text)], None, cx);
3433                })
3434            }
3435
3436            text_thread
3437        })
3438    }
3439
3440    fn init_test(cx: &mut App) {
3441        let settings_store = SettingsStore::test(cx);
3442        prompt_store::init(cx);
3443        editor::init(cx);
3444        LanguageModelRegistry::test(cx);
3445        cx.set_global(settings_store);
3446
3447        theme::init(theme::LoadThemes::JustBase, cx);
3448    }
3449
3450    #[gpui::test]
3451    async fn test_quote_terminal_text(cx: &mut TestAppContext) {
3452        let (_context, text_thread_editor, mut cx) =
3453            setup_text_thread_editor_text(vec![(Role::User, "")], cx).await;
3454
3455        let terminal_output = "$ ls -la\ntotal 0\ndrwxr-xr-x  2 user user  40 Jan  1 00:00 .";
3456
3457        text_thread_editor.update_in(&mut cx, |text_thread_editor, window, cx| {
3458            text_thread_editor.quote_terminal_text(terminal_output.to_string(), window, cx);
3459
3460            text_thread_editor.editor.update(cx, |editor, cx| {
3461                let text = editor.text(cx);
3462                // The text should contain the terminal output wrapped in a code block
3463                assert!(
3464                    text.contains(&format!("```console\n{}\n```", terminal_output)),
3465                    "Terminal text should be wrapped in code block. Got: {}",
3466                    text
3467                );
3468            });
3469        });
3470    }
3471}