message_editor.rs

   1use std::collections::BTreeMap;
   2use std::rc::Rc;
   3use std::sync::Arc;
   4
   5use crate::agent_diff::AgentDiffThread;
   6use crate::agent_model_selector::AgentModelSelector;
   7use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
   8use crate::ui::{
   9    BurnModeTooltip,
  10    preview::{AgentPreview, UsageCallout},
  11};
  12use agent::history_store::HistoryStore;
  13use agent::{
  14    context::{AgentContextKey, ContextLoadResult, load_context},
  15    context_store::ContextStoreEvent,
  16};
  17use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
  18use ai_onboarding::ApiKeysWithProviders;
  19use buffer_diff::BufferDiff;
  20use cloud_llm_client::CompletionIntent;
  21use collections::{HashMap, HashSet};
  22use editor::actions::{MoveUp, Paste};
  23use editor::display_map::CreaseId;
  24use editor::{
  25    Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorDisplayMode,
  26    EditorElement, EditorEvent, EditorStyle, MultiBuffer,
  27};
  28use file_icons::FileIcons;
  29use fs::Fs;
  30use futures::future::Shared;
  31use futures::{FutureExt as _, future};
  32use gpui::{
  33    Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext,
  34    Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point,
  35    pulsating_between,
  36};
  37use language::{Buffer, Language, Point};
  38use language_model::{
  39    ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent,
  40    ZED_CLOUD_PROVIDER_ID,
  41};
  42use multi_buffer;
  43use project::Project;
  44use prompt_store::PromptStore;
  45use settings::Settings;
  46use std::time::Duration;
  47use theme::ThemeSettings;
  48use ui::{
  49    Callout, Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*,
  50};
  51use util::ResultExt as _;
  52use workspace::{CollaboratorId, Workspace};
  53use zed_actions::agent::Chat;
  54use zed_actions::agent::ToggleModelSelector;
  55
  56use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
  57use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
  58use crate::profile_selector::{ProfileProvider, ProfileSelector};
  59use crate::{
  60    ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
  61    ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
  62    ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
  63};
  64use agent::{
  65    MessageCrease, Thread, TokenUsageRatio,
  66    context_store::ContextStore,
  67    thread_store::{TextThreadStore, ThreadStore},
  68};
  69
  70pub const MIN_EDITOR_LINES: usize = 4;
  71pub const MAX_EDITOR_LINES: usize = 8;
  72
  73#[derive(RegisterComponent)]
  74pub struct MessageEditor {
  75    thread: Entity<Thread>,
  76    incompatible_tools_state: Entity<IncompatibleToolsState>,
  77    editor: Entity<Editor>,
  78    workspace: WeakEntity<Workspace>,
  79    project: Entity<Project>,
  80    context_store: Entity<ContextStore>,
  81    prompt_store: Option<Entity<PromptStore>>,
  82    history_store: Option<WeakEntity<HistoryStore>>,
  83    context_strip: Entity<ContextStrip>,
  84    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
  85    model_selector: Entity<AgentModelSelector>,
  86    last_loaded_context: Option<ContextLoadResult>,
  87    load_context_task: Option<Shared<Task<()>>>,
  88    profile_selector: Entity<ProfileSelector>,
  89    edits_expanded: bool,
  90    editor_is_expanded: bool,
  91    last_estimated_token_count: Option<u64>,
  92    update_token_count_task: Option<Task<()>>,
  93    _subscriptions: Vec<Subscription>,
  94}
  95
  96pub(crate) fn create_editor(
  97    workspace: WeakEntity<Workspace>,
  98    context_store: WeakEntity<ContextStore>,
  99    thread_store: WeakEntity<ThreadStore>,
 100    text_thread_store: WeakEntity<TextThreadStore>,
 101    min_lines: usize,
 102    max_lines: Option<usize>,
 103    window: &mut Window,
 104    cx: &mut App,
 105) -> Entity<Editor> {
 106    let language = Language::new(
 107        language::LanguageConfig {
 108            completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
 109            ..Default::default()
 110        },
 111        None,
 112    );
 113
 114    let editor = cx.new(|cx| {
 115        let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
 116        let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 117        let settings = agent_settings::AgentSettings::get_global(cx);
 118
 119        let editor_mode = match settings.editor_mode {
 120            agent_settings::AgentEditorMode::EditorModeOverride(mode) => mode,
 121            agent_settings::AgentEditorMode::Inherit => {
 122                vim_mode_setting::EditorModeSetting::get_global(cx).0
 123            }
 124        };
 125
 126        let mut editor = Editor::new(
 127            editor::EditorDisplayMode::AutoHeight {
 128                min_lines,
 129                max_lines,
 130            },
 131            buffer,
 132            None,
 133            window,
 134            cx,
 135        );
 136        editor.set_placeholder_text("Message the agent – @ to include context", cx);
 137        editor.set_show_indent_guides(false, cx);
 138        editor.set_soft_wrap();
 139        editor.set_default_editor_mode(editor_mode);
 140        editor.set_context_menu_options(ContextMenuOptions {
 141            min_entries_visible: 12,
 142            max_entries_visible: 12,
 143            placement: Some(ContextMenuPlacement::Above),
 144        });
 145        editor.register_addon(ContextCreasesAddon::new());
 146        editor.register_addon(MessageEditorAddon::new());
 147        editor
 148    });
 149
 150    let editor_entity = editor.downgrade();
 151    editor.update(cx, |editor, _| {
 152        editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
 153            workspace,
 154            context_store,
 155            Some(thread_store),
 156            Some(text_thread_store),
 157            editor_entity,
 158            None,
 159        ))));
 160    });
 161    editor
 162}
 163
 164impl ProfileProvider for Entity<Thread> {
 165    fn profiles_supported(&self, cx: &App) -> bool {
 166        self.read(cx)
 167            .configured_model()
 168            .is_some_and(|model| model.model.supports_tools())
 169    }
 170
 171    fn profile_id(&self, cx: &App) -> AgentProfileId {
 172        self.read(cx).profile().id().clone()
 173    }
 174
 175    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
 176        self.update(cx, |this, cx| {
 177            this.set_profile(profile_id, cx);
 178        });
 179    }
 180}
 181
 182impl MessageEditor {
 183    pub fn new(
 184        fs: Arc<dyn Fs>,
 185        workspace: WeakEntity<Workspace>,
 186        context_store: Entity<ContextStore>,
 187        prompt_store: Option<Entity<PromptStore>>,
 188        thread_store: WeakEntity<ThreadStore>,
 189        text_thread_store: WeakEntity<TextThreadStore>,
 190        history_store: Option<WeakEntity<HistoryStore>>,
 191        thread: Entity<Thread>,
 192        window: &mut Window,
 193        cx: &mut Context<Self>,
 194    ) -> Self {
 195        let context_picker_menu_handle = PopoverMenuHandle::default();
 196        let model_selector_menu_handle = PopoverMenuHandle::default();
 197
 198        let editor = create_editor(
 199            workspace.clone(),
 200            context_store.downgrade(),
 201            thread_store.clone(),
 202            text_thread_store.clone(),
 203            MIN_EDITOR_LINES,
 204            Some(MAX_EDITOR_LINES),
 205            window,
 206            cx,
 207        );
 208
 209        let context_strip = cx.new(|cx| {
 210            ContextStrip::new(
 211                context_store.clone(),
 212                workspace.clone(),
 213                Some(thread_store.clone()),
 214                Some(text_thread_store.clone()),
 215                context_picker_menu_handle.clone(),
 216                SuggestContextKind::File,
 217                ModelUsageContext::Thread(thread.clone()),
 218                window,
 219                cx,
 220            )
 221        });
 222
 223        let incompatible_tools = cx.new(|cx| IncompatibleToolsState::new(thread.clone(), cx));
 224
 225        let subscriptions = vec![
 226            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
 227            cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| {
 228                if event == &EditorEvent::BufferEdited {
 229                    this.handle_message_changed(cx)
 230                }
 231            }),
 232            cx.observe(&context_store, |this, _, cx| {
 233                // When context changes, reload it for token counting.
 234                let _ = this.reload_context(cx);
 235            }),
 236            cx.observe(&thread.read(cx).action_log().clone(), |_, _, cx| {
 237                cx.notify()
 238            }),
 239        ];
 240
 241        let model_selector = cx.new(|cx| {
 242            AgentModelSelector::new(
 243                fs.clone(),
 244                model_selector_menu_handle,
 245                editor.focus_handle(cx),
 246                ModelUsageContext::Thread(thread.clone()),
 247                window,
 248                cx,
 249            )
 250        });
 251
 252        let profile_selector = cx.new(|cx| {
 253            ProfileSelector::new(fs, Arc::new(thread.clone()), editor.focus_handle(cx), cx)
 254        });
 255
 256        Self {
 257            editor: editor.clone(),
 258            project: thread.read(cx).project().clone(),
 259            thread,
 260            incompatible_tools_state: incompatible_tools,
 261            workspace,
 262            context_store,
 263            prompt_store,
 264            history_store,
 265            context_strip,
 266            context_picker_menu_handle,
 267            load_context_task: None,
 268            last_loaded_context: None,
 269            model_selector,
 270            edits_expanded: false,
 271            editor_is_expanded: false,
 272            profile_selector,
 273            last_estimated_token_count: None,
 274            update_token_count_task: None,
 275            _subscriptions: subscriptions,
 276        }
 277    }
 278
 279    pub fn context_store(&self) -> &Entity<ContextStore> {
 280        &self.context_store
 281    }
 282
 283    pub fn get_text(&self, cx: &App) -> String {
 284        self.editor.read(cx).text(cx)
 285    }
 286
 287    pub fn set_text(
 288        &mut self,
 289        text: impl Into<Arc<str>>,
 290        window: &mut Window,
 291        cx: &mut Context<Self>,
 292    ) {
 293        self.editor.update(cx, |editor, cx| {
 294            editor.set_text(text, window, cx);
 295        });
 296    }
 297
 298    pub fn expand_message_editor(
 299        &mut self,
 300        _: &ExpandMessageEditor,
 301        _window: &mut Window,
 302        cx: &mut Context<Self>,
 303    ) {
 304        self.set_editor_is_expanded(!self.editor_is_expanded, cx);
 305    }
 306
 307    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
 308        self.editor_is_expanded = is_expanded;
 309        self.editor.update(cx, |editor, _| {
 310            if self.editor_is_expanded {
 311                editor.set_display_mode(EditorDisplayMode::Full {
 312                    scale_ui_elements_with_buffer_font_size: false,
 313                    show_active_line_background: false,
 314                    sized_by_content: false,
 315                })
 316            } else {
 317                editor.set_display_mode(EditorDisplayMode::AutoHeight {
 318                    min_lines: MIN_EDITOR_LINES,
 319                    max_lines: Some(MAX_EDITOR_LINES),
 320                })
 321            }
 322        });
 323        cx.notify();
 324    }
 325
 326    fn toggle_context_picker(
 327        &mut self,
 328        _: &ToggleContextPicker,
 329        window: &mut Window,
 330        cx: &mut Context<Self>,
 331    ) {
 332        self.context_picker_menu_handle.toggle(window, cx);
 333    }
 334
 335    pub fn remove_all_context(
 336        &mut self,
 337        _: &RemoveAllContext,
 338        _window: &mut Window,
 339        cx: &mut Context<Self>,
 340    ) {
 341        self.context_store.update(cx, |store, cx| store.clear(cx));
 342        cx.notify();
 343    }
 344
 345    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
 346        if self.is_editor_empty(cx) {
 347            return;
 348        }
 349
 350        self.thread.update(cx, |thread, cx| {
 351            thread.cancel_editing(cx);
 352        });
 353
 354        if self.thread.read(cx).is_generating() {
 355            self.stop_current_and_send_new_message(window, cx);
 356            return;
 357        }
 358
 359        self.set_editor_is_expanded(false, cx);
 360        self.send_to_model(window, cx);
 361
 362        cx.emit(MessageEditorEvent::ScrollThreadToBottom);
 363        cx.notify();
 364    }
 365
 366    fn chat_with_follow(
 367        &mut self,
 368        _: &ChatWithFollow,
 369        window: &mut Window,
 370        cx: &mut Context<Self>,
 371    ) {
 372        self.workspace
 373            .update(cx, |this, cx| {
 374                this.follow(CollaboratorId::Agent, window, cx)
 375            })
 376            .log_err();
 377
 378        self.chat(&Chat, window, cx);
 379    }
 380
 381    fn is_editor_empty(&self, cx: &App) -> bool {
 382        self.editor.read(cx).text(cx).trim().is_empty()
 383    }
 384
 385    pub fn is_editor_fully_empty(&self, cx: &App) -> bool {
 386        self.editor.read(cx).is_empty(cx)
 387    }
 388
 389    fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 390        let Some(ConfiguredModel { model, provider }) = self
 391            .thread
 392            .update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
 393        else {
 394            return;
 395        };
 396
 397        if provider.must_accept_terms(cx) {
 398            cx.notify();
 399            return;
 400        }
 401
 402        let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| {
 403            let creases = extract_message_creases(editor, cx);
 404            let text = editor.text(cx);
 405            editor.clear(window, cx);
 406            (text, creases)
 407        });
 408
 409        self.last_estimated_token_count.take();
 410        cx.emit(MessageEditorEvent::EstimatedTokenCount);
 411
 412        let thread = self.thread.clone();
 413        let git_store = self.project.read(cx).git_store().clone();
 414        let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
 415        let context_task = self.reload_context(cx);
 416        let window_handle = window.window_handle();
 417
 418        cx.spawn(async move |_this, cx| {
 419            let (checkpoint, loaded_context) = future::join(checkpoint, context_task).await;
 420            let loaded_context = loaded_context.unwrap_or_default();
 421
 422            thread
 423                .update(cx, |thread, cx| {
 424                    thread.insert_user_message(
 425                        user_message,
 426                        loaded_context,
 427                        checkpoint.ok(),
 428                        user_message_creases,
 429                        cx,
 430                    );
 431                })
 432                .log_err();
 433
 434            thread
 435                .update(cx, |thread, cx| {
 436                    thread.advance_prompt_id();
 437                    thread.send_to_model(
 438                        model,
 439                        CompletionIntent::UserPrompt,
 440                        Some(window_handle),
 441                        cx,
 442                    );
 443                })
 444                .log_err();
 445        })
 446        .detach();
 447    }
 448
 449    fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 450        self.thread.update(cx, |thread, cx| {
 451            thread.cancel_editing(cx);
 452        });
 453
 454        let canceled = self.thread.update(cx, |thread, cx| {
 455            thread.cancel_last_completion(Some(window.window_handle()), cx)
 456        });
 457
 458        if canceled {
 459            self.set_editor_is_expanded(false, cx);
 460            self.send_to_model(window, cx);
 461        }
 462    }
 463
 464    fn handle_context_strip_event(
 465        &mut self,
 466        _context_strip: &Entity<ContextStrip>,
 467        event: &ContextStripEvent,
 468        window: &mut Window,
 469        cx: &mut Context<Self>,
 470    ) {
 471        match event {
 472            ContextStripEvent::PickerDismissed
 473            | ContextStripEvent::BlurredEmpty
 474            | ContextStripEvent::BlurredDown => {
 475                let editor_focus_handle = self.editor.focus_handle(cx);
 476                window.focus(&editor_focus_handle);
 477            }
 478            ContextStripEvent::BlurredUp => {}
 479        }
 480    }
 481
 482    fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
 483        if self.context_picker_menu_handle.is_deployed() {
 484            cx.propagate();
 485        } else if self.context_strip.read(cx).has_context_items(cx) {
 486            self.context_strip.focus_handle(cx).focus(window);
 487        }
 488    }
 489
 490    fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
 491        crate::active_thread::attach_pasted_images_as_context(&self.context_store, cx);
 492    }
 493
 494    fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 495        self.edits_expanded = true;
 496        AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
 497        cx.notify();
 498    }
 499
 500    fn handle_edit_bar_expand(&mut self, cx: &mut Context<Self>) {
 501        self.edits_expanded = !self.edits_expanded;
 502        cx.notify();
 503    }
 504
 505    fn handle_file_click(
 506        &self,
 507        buffer: Entity<Buffer>,
 508        window: &mut Window,
 509        cx: &mut Context<Self>,
 510    ) {
 511        if let Ok(diff) = AgentDiffPane::deploy(
 512            AgentDiffThread::Native(self.thread.clone()),
 513            self.workspace.clone(),
 514            window,
 515            cx,
 516        ) {
 517            let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
 518            diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
 519        }
 520    }
 521
 522    pub fn toggle_burn_mode(
 523        &mut self,
 524        _: &ToggleBurnMode,
 525        _window: &mut Window,
 526        cx: &mut Context<Self>,
 527    ) {
 528        self.thread.update(cx, |thread, _cx| {
 529            let active_completion_mode = thread.completion_mode();
 530
 531            thread.set_completion_mode(match active_completion_mode {
 532                CompletionMode::Burn => CompletionMode::Normal,
 533                CompletionMode::Normal => CompletionMode::Burn,
 534            });
 535        });
 536    }
 537
 538    fn handle_accept_all(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
 539        if self.thread.read(cx).has_pending_edit_tool_uses() {
 540            return;
 541        }
 542
 543        self.thread.update(cx, |thread, cx| {
 544            thread.keep_all_edits(cx);
 545        });
 546        cx.notify();
 547    }
 548
 549    fn handle_reject_all(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
 550        if self.thread.read(cx).has_pending_edit_tool_uses() {
 551            return;
 552        }
 553
 554        // Since there's no reject_all_edits method in the thread API,
 555        // we need to iterate through all buffers and reject their edits
 556        let action_log = self.thread.read(cx).action_log().clone();
 557        let changed_buffers = action_log.read(cx).changed_buffers(cx);
 558
 559        for (buffer, _) in changed_buffers {
 560            self.thread.update(cx, |thread, cx| {
 561                let buffer_snapshot = buffer.read(cx);
 562                let start = buffer_snapshot.anchor_before(Point::new(0, 0));
 563                let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
 564                thread
 565                    .reject_edits_in_ranges(buffer, vec![start..end], cx)
 566                    .detach();
 567            });
 568        }
 569        cx.notify();
 570    }
 571
 572    fn handle_reject_file_changes(
 573        &mut self,
 574        buffer: Entity<Buffer>,
 575        _window: &mut Window,
 576        cx: &mut Context<Self>,
 577    ) {
 578        if self.thread.read(cx).has_pending_edit_tool_uses() {
 579            return;
 580        }
 581
 582        self.thread.update(cx, |thread, cx| {
 583            let buffer_snapshot = buffer.read(cx);
 584            let start = buffer_snapshot.anchor_before(Point::new(0, 0));
 585            let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
 586            thread
 587                .reject_edits_in_ranges(buffer, vec![start..end], cx)
 588                .detach();
 589        });
 590        cx.notify();
 591    }
 592
 593    fn handle_accept_file_changes(
 594        &mut self,
 595        buffer: Entity<Buffer>,
 596        _window: &mut Window,
 597        cx: &mut Context<Self>,
 598    ) {
 599        if self.thread.read(cx).has_pending_edit_tool_uses() {
 600            return;
 601        }
 602
 603        self.thread.update(cx, |thread, cx| {
 604            let buffer_snapshot = buffer.read(cx);
 605            let start = buffer_snapshot.anchor_before(Point::new(0, 0));
 606            let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
 607            thread.keep_edits_in_range(buffer, start..end, cx);
 608        });
 609        cx.notify();
 610    }
 611
 612    fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
 613        let thread = self.thread.read(cx);
 614        let model = thread.configured_model();
 615        if !model?.model.supports_burn_mode() {
 616            return None;
 617        }
 618
 619        let active_completion_mode = thread.completion_mode();
 620        let burn_mode_enabled = active_completion_mode == CompletionMode::Burn;
 621        let icon = if burn_mode_enabled {
 622            IconName::ZedBurnModeOn
 623        } else {
 624            IconName::ZedBurnMode
 625        };
 626
 627        Some(
 628            IconButton::new("burn-mode", icon)
 629                .icon_size(IconSize::Small)
 630                .icon_color(Color::Muted)
 631                .toggle_state(burn_mode_enabled)
 632                .selected_icon_color(Color::Error)
 633                .on_click(cx.listener(|this, _event, window, cx| {
 634                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
 635                }))
 636                .tooltip(move |_window, cx| {
 637                    cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
 638                        .into()
 639                })
 640                .into_any_element(),
 641        )
 642    }
 643
 644    fn render_follow_toggle(
 645        &self,
 646        is_model_selected: bool,
 647        cx: &mut Context<Self>,
 648    ) -> impl IntoElement {
 649        let following = self
 650            .workspace
 651            .read_with(cx, |workspace, _| {
 652                workspace.is_being_followed(CollaboratorId::Agent)
 653            })
 654            .unwrap_or(false);
 655
 656        IconButton::new("follow-agent", IconName::Crosshair)
 657            .disabled(!is_model_selected)
 658            .icon_size(IconSize::Small)
 659            .icon_color(Color::Muted)
 660            .toggle_state(following)
 661            .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
 662            .tooltip(move |window, cx| {
 663                if following {
 664                    Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
 665                } else {
 666                    Tooltip::with_meta(
 667                        "Follow Agent",
 668                        Some(&Follow),
 669                        "Track the agent's location as it reads and edits files.",
 670                        window,
 671                        cx,
 672                    )
 673                }
 674            })
 675            .on_click(cx.listener(move |this, _, window, cx| {
 676                this.workspace
 677                    .update(cx, |workspace, cx| {
 678                        if following {
 679                            workspace.unfollow(CollaboratorId::Agent, window, cx);
 680                        } else {
 681                            workspace.follow(CollaboratorId::Agent, window, cx);
 682                        }
 683                    })
 684                    .ok();
 685            }))
 686    }
 687
 688    fn render_editor(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
 689        let thread = self.thread.read(cx);
 690        let model = thread.configured_model();
 691
 692        let editor_bg_color = cx.theme().colors().editor_background;
 693        let is_generating = thread.is_generating();
 694        let focus_handle = self.editor.focus_handle(cx);
 695
 696        let is_model_selected = model.is_some();
 697        let is_editor_empty = self.is_editor_empty(cx);
 698
 699        let incompatible_tools = model
 700            .as_ref()
 701            .map(|model| {
 702                self.incompatible_tools_state.update(cx, |state, cx| {
 703                    state.incompatible_tools(&model.model, cx).to_vec()
 704                })
 705            })
 706            .unwrap_or_default();
 707
 708        let is_editor_expanded = self.editor_is_expanded;
 709        let expand_icon = if is_editor_expanded {
 710            IconName::Minimize
 711        } else {
 712            IconName::Maximize
 713        };
 714
 715        v_flex()
 716            .key_context("MessageEditor")
 717            .on_action(cx.listener(Self::chat))
 718            .on_action(cx.listener(Self::chat_with_follow))
 719            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
 720                this.profile_selector
 721                    .read(cx)
 722                    .menu_handle()
 723                    .toggle(window, cx);
 724            }))
 725            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
 726                this.model_selector
 727                    .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
 728            }))
 729            .on_action(cx.listener(Self::toggle_context_picker))
 730            .on_action(cx.listener(Self::remove_all_context))
 731            .on_action(cx.listener(Self::move_up))
 732            .on_action(cx.listener(Self::expand_message_editor))
 733            .on_action(cx.listener(Self::toggle_burn_mode))
 734            .on_action(
 735                cx.listener(|this, _: &KeepAll, window, cx| this.handle_accept_all(window, cx)),
 736            )
 737            .on_action(
 738                cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)),
 739            )
 740            .capture_action(cx.listener(Self::paste))
 741            .p_2()
 742            .gap_2()
 743            .border_t_1()
 744            .border_color(cx.theme().colors().border)
 745            .bg(editor_bg_color)
 746            .child(
 747                h_flex()
 748                    .justify_between()
 749                    .child(self.context_strip.clone())
 750                    .when(focus_handle.is_focused(window), |this| {
 751                        this.child(
 752                            IconButton::new("toggle-height", expand_icon)
 753                                .icon_size(IconSize::Small)
 754                                .icon_color(Color::Muted)
 755                                .tooltip({
 756                                    let focus_handle = focus_handle.clone();
 757                                    move |window, cx| {
 758                                        let expand_label = if is_editor_expanded {
 759                                            "Minimize Message Editor".to_string()
 760                                        } else {
 761                                            "Expand Message Editor".to_string()
 762                                        };
 763
 764                                        Tooltip::for_action_in(
 765                                            expand_label,
 766                                            &ExpandMessageEditor,
 767                                            &focus_handle,
 768                                            window,
 769                                            cx,
 770                                        )
 771                                    }
 772                                })
 773                                .on_click(cx.listener(|_, _, window, cx| {
 774                                    window.dispatch_action(Box::new(ExpandMessageEditor), cx);
 775                                })),
 776                        )
 777                    }),
 778            )
 779            .child(
 780                v_flex()
 781                    .size_full()
 782                    .gap_1()
 783                    .when(is_editor_expanded, |this| {
 784                        this.h(vh(0.8, window)).justify_between()
 785                    })
 786                    .child({
 787                        let settings = ThemeSettings::get_global(cx);
 788                        let font_size = TextSize::Small
 789                            .rems(cx)
 790                            .to_pixels(settings.agent_font_size(cx));
 791                        let line_height = settings.buffer_line_height.value() * font_size;
 792
 793                        let text_style = TextStyle {
 794                            color: cx.theme().colors().text,
 795                            font_family: settings.buffer_font.family.clone(),
 796                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
 797                            font_features: settings.buffer_font.features.clone(),
 798                            font_size: font_size.into(),
 799                            line_height: line_height.into(),
 800                            ..Default::default()
 801                        };
 802
 803                        EditorElement::new(
 804                            &self.editor,
 805                            EditorStyle {
 806                                background: editor_bg_color,
 807                                local_player: cx.theme().players().local(),
 808                                text: text_style,
 809                                syntax: cx.theme().syntax().clone(),
 810                                ..Default::default()
 811                            },
 812                        )
 813                        .into_any()
 814                    })
 815                    .child(
 816                        h_flex()
 817                            .flex_none()
 818                            .flex_wrap()
 819                            .justify_between()
 820                            .child(
 821                                h_flex()
 822                                    .child(self.render_follow_toggle(is_model_selected, cx))
 823                                    .children(self.render_burn_mode_toggle(cx)),
 824                            )
 825                            .child(
 826                                h_flex()
 827                                    .gap_1()
 828                                    .flex_wrap()
 829                                    .when(!incompatible_tools.is_empty(), |this| {
 830                                        this.child(
 831                                            IconButton::new(
 832                                                "tools-incompatible-warning",
 833                                                IconName::Warning,
 834                                            )
 835                                            .icon_color(Color::Warning)
 836                                            .icon_size(IconSize::Small)
 837                                            .tooltip({
 838                                                move |_, cx| {
 839                                                    cx.new(|_| IncompatibleToolsTooltip {
 840                                                        incompatible_tools: incompatible_tools
 841                                                            .clone(),
 842                                                    })
 843                                                    .into()
 844                                                }
 845                                            }),
 846                                        )
 847                                    })
 848                                    .child(self.profile_selector.clone())
 849                                    .child(self.model_selector.clone())
 850                                    .map({
 851                                        move |parent| {
 852                                            if is_generating {
 853                                                parent
 854                                                    .when(is_editor_empty, |parent| {
 855                                                        parent.child(
 856                                                            IconButton::new(
 857                                                                "stop-generation",
 858                                                                IconName::Stop,
 859                                                            )
 860                                                            .icon_color(Color::Error)
 861                                                            .style(ButtonStyle::Tinted(
 862                                                                ui::TintColor::Error,
 863                                                            ))
 864                                                            .tooltip(move |window, cx| {
 865                                                                Tooltip::for_action(
 866                                                                    "Stop Generation",
 867                                                                    &editor::actions::Cancel,
 868                                                                    window,
 869                                                                    cx,
 870                                                                )
 871                                                            })
 872                                                            .on_click({
 873                                                                let focus_handle =
 874                                                                    focus_handle.clone();
 875                                                                move |_event, window, cx| {
 876                                                                    focus_handle.dispatch_action(
 877                                                                        &editor::actions::Cancel,
 878                                                                        window,
 879                                                                        cx,
 880                                                                    );
 881                                                                }
 882                                                            })
 883                                                            .with_animation(
 884                                                                "pulsating-label",
 885                                                                Animation::new(
 886                                                                    Duration::from_secs(2),
 887                                                                )
 888                                                                .repeat()
 889                                                                .with_easing(pulsating_between(
 890                                                                    0.4, 1.0,
 891                                                                )),
 892                                                                |icon_button, delta| {
 893                                                                    icon_button.alpha(delta)
 894                                                                },
 895                                                            ),
 896                                                        )
 897                                                    })
 898                                                    .when(!is_editor_empty, |parent| {
 899                                                        parent.child(
 900                                                            IconButton::new(
 901                                                                "send-message",
 902                                                                IconName::Send,
 903                                                            )
 904                                                            .icon_color(Color::Accent)
 905                                                            .style(ButtonStyle::Filled)
 906                                                            .disabled(!is_model_selected)
 907                                                            .on_click({
 908                                                                let focus_handle =
 909                                                                    focus_handle.clone();
 910                                                                move |_event, window, cx| {
 911                                                                    focus_handle.dispatch_action(
 912                                                                        &Chat, window, cx,
 913                                                                    );
 914                                                                }
 915                                                            })
 916                                                            .tooltip(move |window, cx| {
 917                                                                Tooltip::for_action(
 918                                                                    "Stop and Send New Message",
 919                                                                    &Chat,
 920                                                                    window,
 921                                                                    cx,
 922                                                                )
 923                                                            }),
 924                                                        )
 925                                                    })
 926                                            } else {
 927                                                parent.child(
 928                                                    IconButton::new("send-message", IconName::Send)
 929                                                        .icon_color(Color::Accent)
 930                                                        .style(ButtonStyle::Filled)
 931                                                        .disabled(
 932                                                            is_editor_empty || !is_model_selected,
 933                                                        )
 934                                                        .on_click({
 935                                                            let focus_handle = focus_handle.clone();
 936                                                            move |_event, window, cx| {
 937                                                                telemetry::event!(
 938                                                                    "Agent Message Sent",
 939                                                                    agent = "zed",
 940                                                                );
 941                                                                focus_handle.dispatch_action(
 942                                                                    &Chat, window, cx,
 943                                                                );
 944                                                            }
 945                                                        })
 946                                                        .when(
 947                                                            !is_editor_empty && is_model_selected,
 948                                                            |button| {
 949                                                                button.tooltip(move |window, cx| {
 950                                                                    Tooltip::for_action(
 951                                                                        "Send", &Chat, window, cx,
 952                                                                    )
 953                                                                })
 954                                                            },
 955                                                        )
 956                                                        .when(is_editor_empty, |button| {
 957                                                            button.tooltip(Tooltip::text(
 958                                                                "Type a message to submit",
 959                                                            ))
 960                                                        })
 961                                                        .when(!is_model_selected, |button| {
 962                                                            button.tooltip(Tooltip::text(
 963                                                                "Select a model to continue",
 964                                                            ))
 965                                                        }),
 966                                                )
 967                                            }
 968                                        }
 969                                    }),
 970                            ),
 971                    ),
 972            )
 973    }
 974
 975    fn render_edits_bar(
 976        &self,
 977        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
 978        window: &mut Window,
 979        cx: &mut Context<Self>,
 980    ) -> Div {
 981        let focus_handle = self.editor.focus_handle(cx);
 982
 983        let editor_bg_color = cx.theme().colors().editor_background;
 984        let border_color = cx.theme().colors().border;
 985        let active_color = cx.theme().colors().element_selected;
 986        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
 987
 988        let is_edit_changes_expanded = self.edits_expanded;
 989        let thread = self.thread.read(cx);
 990        let pending_edits = thread.has_pending_edit_tool_uses();
 991
 992        const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
 993
 994        v_flex()
 995            .mt_1()
 996            .mx_2()
 997            .bg(bg_edit_files_disclosure)
 998            .border_1()
 999            .border_b_0()
1000            .border_color(border_color)
1001            .rounded_t_md()
1002            .shadow(vec![gpui::BoxShadow {
1003                color: gpui::black().opacity(0.15),
1004                offset: point(px(1.), px(-1.)),
1005                blur_radius: px(3.),
1006                spread_radius: px(0.),
1007            }])
1008            .child(
1009                h_flex()
1010                    .p_1()
1011                    .justify_between()
1012                    .when(is_edit_changes_expanded, |this| {
1013                        this.border_b_1().border_color(border_color)
1014                    })
1015                    .child(
1016                        h_flex()
1017                            .id("edits-container")
1018                            .cursor_pointer()
1019                            .w_full()
1020                            .gap_1()
1021                            .child(
1022                                Disclosure::new("edits-disclosure", is_edit_changes_expanded)
1023                                    .on_click(cx.listener(|this, _, _, cx| {
1024                                        this.handle_edit_bar_expand(cx)
1025                                    })),
1026                            )
1027                            .map(|this| {
1028                                if pending_edits {
1029                                    this.child(
1030                                        Label::new(format!(
1031                                            "Editing {} {}",
1032                                            changed_buffers.len(),
1033                                            if changed_buffers.len() == 1 {
1034                                                "file"
1035                                            } else {
1036                                                "files"
1037                                            }
1038                                        ))
1039                                        .color(Color::Muted)
1040                                        .size(LabelSize::Small)
1041                                        .with_animation(
1042                                            "edit-label",
1043                                            Animation::new(Duration::from_secs(2))
1044                                                .repeat()
1045                                                .with_easing(pulsating_between(0.3, 0.7)),
1046                                            |label, delta| label.alpha(delta),
1047                                        ),
1048                                    )
1049                                } else {
1050                                    this.child(
1051                                        Label::new("Edits")
1052                                            .size(LabelSize::Small)
1053                                            .color(Color::Muted),
1054                                    )
1055                                    .child(
1056                                        Label::new("").size(LabelSize::XSmall).color(Color::Muted),
1057                                    )
1058                                    .child(
1059                                        Label::new(format!(
1060                                            "{} {}",
1061                                            changed_buffers.len(),
1062                                            if changed_buffers.len() == 1 {
1063                                                "file"
1064                                            } else {
1065                                                "files"
1066                                            }
1067                                        ))
1068                                        .size(LabelSize::Small)
1069                                        .color(Color::Muted),
1070                                    )
1071                                }
1072                            })
1073                            .on_click(
1074                                cx.listener(|this, _, _, cx| this.handle_edit_bar_expand(cx)),
1075                            ),
1076                    )
1077                    .child(
1078                        h_flex()
1079                            .gap_1()
1080                            .child(
1081                                IconButton::new("review-changes", IconName::ListTodo)
1082                                    .icon_size(IconSize::Small)
1083                                    .tooltip({
1084                                        let focus_handle = focus_handle.clone();
1085                                        move |window, cx| {
1086                                            Tooltip::for_action_in(
1087                                                "Review Changes",
1088                                                &OpenAgentDiff,
1089                                                &focus_handle,
1090                                                window,
1091                                                cx,
1092                                            )
1093                                        }
1094                                    })
1095                                    .on_click(cx.listener(|this, _, window, cx| {
1096                                        this.handle_review_click(window, cx)
1097                                    })),
1098                            )
1099                            .child(Divider::vertical().color(DividerColor::Border))
1100                            .child(
1101                                Button::new("reject-all-changes", "Reject All")
1102                                    .label_size(LabelSize::Small)
1103                                    .disabled(pending_edits)
1104                                    .when(pending_edits, |this| {
1105                                        this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1106                                    })
1107                                    .key_binding(
1108                                        KeyBinding::for_action_in(
1109                                            &RejectAll,
1110                                            &focus_handle.clone(),
1111                                            window,
1112                                            cx,
1113                                        )
1114                                        .map(|kb| kb.size(rems_from_px(10.))),
1115                                    )
1116                                    .on_click(cx.listener(|this, _, window, cx| {
1117                                        this.handle_reject_all(window, cx)
1118                                    })),
1119                            )
1120                            .child(
1121                                Button::new("accept-all-changes", "Accept All")
1122                                    .label_size(LabelSize::Small)
1123                                    .disabled(pending_edits)
1124                                    .when(pending_edits, |this| {
1125                                        this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1126                                    })
1127                                    .key_binding(
1128                                        KeyBinding::for_action_in(
1129                                            &KeepAll,
1130                                            &focus_handle,
1131                                            window,
1132                                            cx,
1133                                        )
1134                                        .map(|kb| kb.size(rems_from_px(10.))),
1135                                    )
1136                                    .on_click(cx.listener(|this, _, window, cx| {
1137                                        this.handle_accept_all(window, cx)
1138                                    })),
1139                            ),
1140                    ),
1141            )
1142            .when(is_edit_changes_expanded, |parent| {
1143                parent.child(
1144                    v_flex().children(changed_buffers.iter().enumerate().flat_map(
1145                        |(index, (buffer, _diff))| {
1146                            let file = buffer.read(cx).file()?;
1147                            let path = file.path();
1148
1149                            let file_path = path.parent().and_then(|parent| {
1150                                let parent_str = parent.to_string_lossy();
1151
1152                                if parent_str.is_empty() {
1153                                    None
1154                                } else {
1155                                    Some(
1156                                        Label::new(format!(
1157                                            "/{}{}",
1158                                            parent_str,
1159                                            std::path::MAIN_SEPARATOR_STR
1160                                        ))
1161                                        .color(Color::Muted)
1162                                        .size(LabelSize::XSmall)
1163                                        .buffer_font(cx),
1164                                    )
1165                                }
1166                            });
1167
1168                            let file_name = path.file_name().map(|name| {
1169                                Label::new(name.to_string_lossy().to_string())
1170                                    .size(LabelSize::XSmall)
1171                                    .buffer_font(cx)
1172                            });
1173
1174                            let file_icon = FileIcons::get_icon(path, cx)
1175                                .map(Icon::from_path)
1176                                .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
1177                                .unwrap_or_else(|| {
1178                                    Icon::new(IconName::File)
1179                                        .color(Color::Muted)
1180                                        .size(IconSize::Small)
1181                                });
1182
1183                            let overlay_gradient = linear_gradient(
1184                                90.,
1185                                linear_color_stop(editor_bg_color, 1.),
1186                                linear_color_stop(editor_bg_color.opacity(0.2), 0.),
1187                            );
1188
1189                            let element = h_flex()
1190                                .group("edited-code")
1191                                .id(("file-container", index))
1192                                .relative()
1193                                .py_1()
1194                                .pl_2()
1195                                .pr_1()
1196                                .gap_2()
1197                                .justify_between()
1198                                .bg(editor_bg_color)
1199                                .when(index < changed_buffers.len() - 1, |parent| {
1200                                    parent.border_color(border_color).border_b_1()
1201                                })
1202                                .child(
1203                                    h_flex()
1204                                        .id(("file-name", index))
1205                                        .pr_8()
1206                                        .gap_1p5()
1207                                        .max_w_full()
1208                                        .overflow_x_scroll()
1209                                        .child(file_icon)
1210                                        .child(
1211                                            h_flex()
1212                                                .gap_0p5()
1213                                                .children(file_name)
1214                                                .children(file_path),
1215                                        )
1216                                        .on_click({
1217                                            let buffer = buffer.clone();
1218                                            cx.listener(move |this, _, window, cx| {
1219                                                this.handle_file_click(buffer.clone(), window, cx);
1220                                            })
1221                                        }), // TODO: Implement line diff
1222                                            // .child(Label::new("+").color(Color::Created))
1223                                            // .child(Label::new("-").color(Color::Deleted)),
1224                                            //
1225                                )
1226                                .child(
1227                                    h_flex()
1228                                        .gap_1()
1229                                        .visible_on_hover("edited-code")
1230                                        .child(
1231                                            Button::new("review", "Review")
1232                                                .label_size(LabelSize::Small)
1233                                                .on_click({
1234                                                    let buffer = buffer.clone();
1235                                                    cx.listener(move |this, _, window, cx| {
1236                                                        this.handle_file_click(
1237                                                            buffer.clone(),
1238                                                            window,
1239                                                            cx,
1240                                                        );
1241                                                    })
1242                                                }),
1243                                        )
1244                                        .child(
1245                                            Divider::vertical().color(DividerColor::BorderVariant),
1246                                        )
1247                                        .child(
1248                                            Button::new("reject-file", "Reject")
1249                                                .label_size(LabelSize::Small)
1250                                                .disabled(pending_edits)
1251                                                .on_click({
1252                                                    let buffer = buffer.clone();
1253                                                    cx.listener(move |this, _, window, cx| {
1254                                                        this.handle_reject_file_changes(
1255                                                            buffer.clone(),
1256                                                            window,
1257                                                            cx,
1258                                                        );
1259                                                    })
1260                                                }),
1261                                        )
1262                                        .child(
1263                                            Button::new("accept-file", "Accept")
1264                                                .label_size(LabelSize::Small)
1265                                                .disabled(pending_edits)
1266                                                .on_click({
1267                                                    let buffer = buffer.clone();
1268                                                    cx.listener(move |this, _, window, cx| {
1269                                                        this.handle_accept_file_changes(
1270                                                            buffer.clone(),
1271                                                            window,
1272                                                            cx,
1273                                                        );
1274                                                    })
1275                                                }),
1276                                        ),
1277                                )
1278                                .child(
1279                                    div()
1280                                        .id("gradient-overlay")
1281                                        .absolute()
1282                                        .h_full()
1283                                        .w_12()
1284                                        .top_0()
1285                                        .bottom_0()
1286                                        .right(px(152.))
1287                                        .bg(overlay_gradient),
1288                                );
1289
1290                            Some(element)
1291                        },
1292                    )),
1293                )
1294            })
1295    }
1296
1297    fn is_using_zed_provider(&self, cx: &App) -> bool {
1298        self.thread
1299            .read(cx)
1300            .configured_model()
1301            .is_some_and(|model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
1302    }
1303
1304    fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
1305        if !self.is_using_zed_provider(cx) {
1306            return None;
1307        }
1308
1309        let user_store = self.project.read(cx).user_store().read(cx);
1310        if user_store.is_usage_based_billing_enabled() {
1311            return None;
1312        }
1313
1314        let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
1315
1316        let usage = user_store.model_request_usage()?;
1317
1318        Some(
1319            div()
1320                .child(UsageCallout::new(plan, usage))
1321                .line_height(line_height),
1322        )
1323    }
1324
1325    fn render_token_limit_callout(
1326        &self,
1327        line_height: Pixels,
1328        token_usage_ratio: TokenUsageRatio,
1329        cx: &mut Context<Self>,
1330    ) -> Option<Div> {
1331        let (icon, severity) = if token_usage_ratio == TokenUsageRatio::Exceeded {
1332            (IconName::Close, Severity::Error)
1333        } else {
1334            (IconName::Warning, Severity::Warning)
1335        };
1336
1337        let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
1338            "Thread reached the token limit"
1339        } else {
1340            "Thread reaching the token limit soon"
1341        };
1342
1343        let description = if self.is_using_zed_provider(cx) {
1344            "To continue, start a new thread from a summary or turn burn mode on."
1345        } else {
1346            "To continue, start a new thread from a summary."
1347        };
1348
1349        let callout = Callout::new()
1350            .line_height(line_height)
1351            .severity(severity)
1352            .icon(icon)
1353            .title(title)
1354            .description(description)
1355            .actions_slot(
1356                h_flex()
1357                    .gap_0p5()
1358                    .when(self.is_using_zed_provider(cx), |this| {
1359                        this.child(
1360                            IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
1361                                .icon_size(IconSize::XSmall)
1362                                .on_click(cx.listener(|this, _event, window, cx| {
1363                                    this.toggle_burn_mode(&ToggleBurnMode, window, cx);
1364                                })),
1365                        )
1366                    })
1367                    .child(
1368                        Button::new("start-new-thread", "Start New Thread")
1369                            .label_size(LabelSize::Small)
1370                            .on_click(cx.listener(|this, _, window, cx| {
1371                                let from_thread_id = Some(this.thread.read(cx).id().clone());
1372                                window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
1373                            })),
1374                    ),
1375            );
1376
1377        Some(
1378            div()
1379                .border_t_1()
1380                .border_color(cx.theme().colors().border)
1381                .child(callout),
1382        )
1383    }
1384
1385    pub fn last_estimated_token_count(&self) -> Option<u64> {
1386        self.last_estimated_token_count
1387    }
1388
1389    pub fn is_waiting_to_update_token_count(&self) -> bool {
1390        self.update_token_count_task.is_some()
1391    }
1392
1393    fn reload_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
1394        let load_task = cx.spawn(async move |this, cx| {
1395            let Ok(load_task) = this.update(cx, |this, cx| {
1396                let new_context = this
1397                    .context_store
1398                    .read(cx)
1399                    .new_context_for_thread(this.thread.read(cx), None);
1400                load_context(new_context, &this.project, &this.prompt_store, cx)
1401            }) else {
1402                return;
1403            };
1404            let result = load_task.await;
1405            this.update(cx, |this, cx| {
1406                this.last_loaded_context = Some(result);
1407                this.load_context_task = None;
1408                this.message_or_context_changed(false, cx);
1409            })
1410            .ok();
1411        });
1412        // Replace existing load task, if any, causing it to be canceled.
1413        let load_task = load_task.shared();
1414        self.load_context_task = Some(load_task.clone());
1415        cx.spawn(async move |this, cx| {
1416            load_task.await;
1417            this.read_with(cx, |this, _cx| this.last_loaded_context.clone())
1418                .ok()
1419                .flatten()
1420        })
1421    }
1422
1423    fn handle_message_changed(&mut self, cx: &mut Context<Self>) {
1424        self.message_or_context_changed(true, cx);
1425    }
1426
1427    fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
1428        cx.emit(MessageEditorEvent::Changed);
1429        self.update_token_count_task.take();
1430
1431        let Some(model) = self.thread.read(cx).configured_model() else {
1432            self.last_estimated_token_count.take();
1433            return;
1434        };
1435
1436        let editor = self.editor.clone();
1437
1438        self.update_token_count_task = Some(cx.spawn(async move |this, cx| {
1439            if debounce {
1440                cx.background_executor()
1441                    .timer(Duration::from_millis(200))
1442                    .await;
1443            }
1444
1445            let token_count = if let Some(task) = this
1446                .update(cx, |this, cx| {
1447                    let loaded_context = this
1448                        .last_loaded_context
1449                        .as_ref()
1450                        .map(|context_load_result| &context_load_result.loaded_context);
1451                    let message_text = editor.read(cx).text(cx);
1452
1453                    if message_text.is_empty()
1454                        && loaded_context.is_none_or(|loaded_context| loaded_context.is_empty())
1455                    {
1456                        return None;
1457                    }
1458
1459                    let mut request_message = LanguageModelRequestMessage {
1460                        role: language_model::Role::User,
1461                        content: Vec::new(),
1462                        cache: false,
1463                    };
1464
1465                    if let Some(loaded_context) = loaded_context {
1466                        loaded_context.add_to_request_message(&mut request_message);
1467                    }
1468
1469                    if !message_text.is_empty() {
1470                        request_message
1471                            .content
1472                            .push(MessageContent::Text(message_text));
1473                    }
1474
1475                    let request = language_model::LanguageModelRequest {
1476                        thread_id: None,
1477                        prompt_id: None,
1478                        intent: None,
1479                        mode: None,
1480                        messages: vec![request_message],
1481                        tools: vec![],
1482                        tool_choice: None,
1483                        stop: vec![],
1484                        temperature: AgentSettings::temperature_for_model(&model.model, cx),
1485                        thinking_allowed: true,
1486                    };
1487
1488                    Some(model.model.count_tokens(request, cx))
1489                })
1490                .ok()
1491                .flatten()
1492            {
1493                task.await.log_err()
1494            } else {
1495                Some(0)
1496            };
1497
1498            this.update(cx, |this, cx| {
1499                if let Some(token_count) = token_count {
1500                    this.last_estimated_token_count = Some(token_count);
1501                    cx.emit(MessageEditorEvent::EstimatedTokenCount);
1502                }
1503                this.update_token_count_task.take();
1504            })
1505            .ok();
1506        }));
1507    }
1508}
1509
1510#[derive(Default)]
1511pub struct ContextCreasesAddon {
1512    creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
1513    _subscription: Option<Subscription>,
1514}
1515
1516pub struct MessageEditorAddon {}
1517
1518impl MessageEditorAddon {
1519    pub fn new() -> Self {
1520        Self {}
1521    }
1522}
1523
1524impl Addon for MessageEditorAddon {
1525    fn to_any(&self) -> &dyn std::any::Any {
1526        self
1527    }
1528
1529    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1530        Some(self)
1531    }
1532
1533    fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1534        let settings = agent_settings::AgentSettings::get_global(cx);
1535        if settings.use_modifier_to_send {
1536            key_context.add("use_modifier_to_send");
1537        }
1538    }
1539}
1540
1541impl Addon for ContextCreasesAddon {
1542    fn to_any(&self) -> &dyn std::any::Any {
1543        self
1544    }
1545
1546    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1547        Some(self)
1548    }
1549}
1550
1551impl ContextCreasesAddon {
1552    pub fn new() -> Self {
1553        Self {
1554            creases: HashMap::default(),
1555            _subscription: None,
1556        }
1557    }
1558
1559    pub fn add_creases(
1560        &mut self,
1561        context_store: &Entity<ContextStore>,
1562        key: AgentContextKey,
1563        creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
1564        cx: &mut Context<Editor>,
1565    ) {
1566        self.creases.entry(key).or_default().extend(creases);
1567        self._subscription = Some(
1568            cx.subscribe(context_store, |editor, _, event, cx| match event {
1569                ContextStoreEvent::ContextRemoved(key) => {
1570                    let Some(this) = editor.addon_mut::<Self>() else {
1571                        return;
1572                    };
1573                    let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
1574                        .creases
1575                        .remove(key)
1576                        .unwrap_or_default()
1577                        .into_iter()
1578                        .unzip();
1579                    let ranges = editor
1580                        .remove_creases(crease_ids, cx)
1581                        .into_iter()
1582                        .map(|(_, range)| range)
1583                        .collect::<Vec<_>>();
1584                    editor.unfold_ranges(&ranges, false, false, cx);
1585                    editor.edit(ranges.into_iter().zip(replacement_texts), cx);
1586                    cx.notify();
1587                }
1588            }),
1589        )
1590    }
1591
1592    pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
1593        self.creases
1594    }
1595}
1596
1597pub fn extract_message_creases(
1598    editor: &mut Editor,
1599    cx: &mut Context<'_, Editor>,
1600) -> Vec<MessageCrease> {
1601    let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1602    let mut contexts_by_crease_id = editor
1603        .addon_mut::<ContextCreasesAddon>()
1604        .map(std::mem::take)
1605        .unwrap_or_default()
1606        .into_inner()
1607        .into_iter()
1608        .flat_map(|(key, creases)| {
1609            let context = key.0;
1610            creases
1611                .into_iter()
1612                .map(move |(id, _)| (id, context.clone()))
1613        })
1614        .collect::<HashMap<_, _>>();
1615    // Filter the addon's list of creases based on what the editor reports,
1616    // since the addon might have removed creases in it.
1617
1618    editor.display_map.update(cx, |display_map, cx| {
1619        display_map
1620            .snapshot(cx)
1621            .crease_snapshot
1622            .creases()
1623            .filter_map(|(id, crease)| {
1624                Some((
1625                    id,
1626                    (
1627                        crease.range().to_offset(&buffer_snapshot),
1628                        crease.metadata()?.clone(),
1629                    ),
1630                ))
1631            })
1632            .map(|(id, (range, metadata))| {
1633                let context = contexts_by_crease_id.remove(&id);
1634                MessageCrease {
1635                    range,
1636                    context,
1637                    label: metadata.label,
1638                    icon_path: metadata.icon_path,
1639                }
1640            })
1641            .collect()
1642    })
1643}
1644
1645impl EventEmitter<MessageEditorEvent> for MessageEditor {}
1646
1647pub enum MessageEditorEvent {
1648    EstimatedTokenCount,
1649    Changed,
1650    ScrollThreadToBottom,
1651}
1652
1653impl Focusable for MessageEditor {
1654    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1655        self.editor.focus_handle(cx)
1656    }
1657}
1658
1659impl Render for MessageEditor {
1660    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1661        let thread = self.thread.read(cx);
1662        let token_usage_ratio = thread
1663            .total_token_usage()
1664            .map_or(TokenUsageRatio::Normal, |total_token_usage| {
1665                total_token_usage.ratio()
1666            });
1667
1668        let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn;
1669
1670        let action_log = self.thread.read(cx).action_log();
1671        let changed_buffers = action_log.read(cx).changed_buffers(cx);
1672
1673        let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
1674
1675        let has_configured_providers = LanguageModelRegistry::read_global(cx)
1676            .providers()
1677            .iter()
1678            .filter(|provider| {
1679                provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
1680            })
1681            .count()
1682            > 0;
1683
1684        let is_signed_out = self
1685            .workspace
1686            .read_with(cx, |workspace, _| {
1687                workspace.client().status().borrow().is_signed_out()
1688            })
1689            .unwrap_or(true);
1690
1691        let has_history = self
1692            .history_store
1693            .as_ref()
1694            .and_then(|hs| hs.update(cx, |hs, cx| !hs.entries(cx).is_empty()).ok())
1695            .unwrap_or(false)
1696            || self
1697                .thread
1698                .read_with(cx, |thread, _| thread.messages().len() > 0);
1699
1700        v_flex()
1701            .size_full()
1702            .bg(cx.theme().colors().panel_background)
1703            .when(
1704                !has_history && is_signed_out && has_configured_providers,
1705                |this| this.child(cx.new(ApiKeysWithProviders::new)),
1706            )
1707            .when(!changed_buffers.is_empty(), |parent| {
1708                parent.child(self.render_edits_bar(&changed_buffers, window, cx))
1709            })
1710            .child(self.render_editor(window, cx))
1711            .children({
1712                let usage_callout = self.render_usage_callout(line_height, cx);
1713
1714                if usage_callout.is_some() {
1715                    usage_callout
1716                } else if token_usage_ratio != TokenUsageRatio::Normal && !burn_mode_enabled {
1717                    self.render_token_limit_callout(line_height, token_usage_ratio, cx)
1718                } else {
1719                    None
1720                }
1721            })
1722    }
1723}
1724
1725pub fn insert_message_creases(
1726    editor: &mut Editor,
1727    message_creases: &[MessageCrease],
1728    context_store: &Entity<ContextStore>,
1729    window: &mut Window,
1730    cx: &mut Context<'_, Editor>,
1731) {
1732    let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1733    let creases = message_creases
1734        .iter()
1735        .map(|crease| {
1736            let start = buffer_snapshot.anchor_after(crease.range.start);
1737            let end = buffer_snapshot.anchor_before(crease.range.end);
1738            crease_for_mention(
1739                crease.label.clone(),
1740                crease.icon_path.clone(),
1741                start..end,
1742                cx.weak_entity(),
1743            )
1744        })
1745        .collect::<Vec<_>>();
1746    let ids = editor.insert_creases(creases.clone(), cx);
1747    editor.fold_creases(creases, false, window, cx);
1748    if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
1749        for (crease, id) in message_creases.iter().zip(ids) {
1750            if let Some(context) = crease.context.as_ref() {
1751                let key = AgentContextKey(context.clone());
1752                addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
1753            }
1754        }
1755    }
1756}
1757impl Component for MessageEditor {
1758    fn scope() -> ComponentScope {
1759        ComponentScope::Agent
1760    }
1761
1762    fn description() -> Option<&'static str> {
1763        Some(
1764            "The composer experience of the Agent Panel. This interface handles context, composing messages, switching profiles, models and more.",
1765        )
1766    }
1767}
1768
1769impl AgentPreview for MessageEditor {
1770    fn agent_preview(
1771        workspace: WeakEntity<Workspace>,
1772        active_thread: Entity<ActiveThread>,
1773        window: &mut Window,
1774        cx: &mut App,
1775    ) -> Option<AnyElement> {
1776        if let Some(workspace) = workspace.upgrade() {
1777            let fs = workspace.read(cx).app_state().fs.clone();
1778            let project = workspace.read(cx).project().clone();
1779            let weak_project = project.downgrade();
1780            let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
1781            let active_thread = active_thread.read(cx);
1782            let thread = active_thread.thread().clone();
1783            let thread_store = active_thread.thread_store().clone();
1784            let text_thread_store = active_thread.text_thread_store().clone();
1785
1786            let default_message_editor = cx.new(|cx| {
1787                MessageEditor::new(
1788                    fs,
1789                    workspace.downgrade(),
1790                    context_store,
1791                    None,
1792                    thread_store.downgrade(),
1793                    text_thread_store.downgrade(),
1794                    None,
1795                    thread,
1796                    window,
1797                    cx,
1798                )
1799            });
1800
1801            Some(
1802                v_flex()
1803                    .gap_4()
1804                    .children(vec![single_example(
1805                        "Default Message Editor",
1806                        div()
1807                            .w(px(540.))
1808                            .pt_12()
1809                            .bg(cx.theme().colors().panel_background)
1810                            .border_1()
1811                            .border_color(cx.theme().colors().border)
1812                            .child(default_message_editor)
1813                            .into_any_element(),
1814                    )])
1815                    .into_any_element(),
1816            )
1817        } else {
1818            None
1819        }
1820    }
1821}
1822
1823register_agent_preview!(MessageEditor);