text_thread_editor.rs

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