message_editor.rs

   1use std::collections::BTreeMap;
   2use std::sync::Arc;
   3
   4use crate::assistant_model_selector::ModelType;
   5use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
   6use buffer_diff::BufferDiff;
   7use collections::HashSet;
   8use editor::actions::MoveUp;
   9use editor::{
  10    ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle,
  11    MultiBuffer,
  12};
  13use file_icons::FileIcons;
  14use fs::Fs;
  15use gpui::{
  16    Animation, AnimationExt, App, Entity, Focusable, Subscription, TextStyle, WeakEntity,
  17    linear_color_stop, linear_gradient, point, pulsating_between,
  18};
  19use language::{Buffer, Language};
  20use language_model::{ConfiguredModel, LanguageModelRegistry};
  21use language_model_selector::ToggleModelSelector;
  22use multi_buffer;
  23use project::Project;
  24use settings::Settings;
  25use std::time::Duration;
  26use theme::ThemeSettings;
  27use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
  28use util::ResultExt as _;
  29use workspace::Workspace;
  30
  31use crate::assistant_model_selector::AssistantModelSelector;
  32use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
  33use crate::context_store::{ContextStore, refresh_context_store_text};
  34use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
  35use crate::profile_selector::ProfileSelector;
  36use crate::thread::{RequestKind, Thread, TokenUsageRatio};
  37use crate::thread_store::ThreadStore;
  38use crate::{
  39    AgentDiff, Chat, ChatMode, ExpandMessageEditor, NewThread, OpenAgentDiff, RemoveAllContext,
  40    ToggleContextPicker, ToggleProfileSelector,
  41};
  42
  43pub struct MessageEditor {
  44    thread: Entity<Thread>,
  45    incompatible_tools_state: Entity<IncompatibleToolsState>,
  46    editor: Entity<Editor>,
  47    #[allow(dead_code)]
  48    workspace: WeakEntity<Workspace>,
  49    project: Entity<Project>,
  50    context_store: Entity<ContextStore>,
  51    context_strip: Entity<ContextStrip>,
  52    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
  53    model_selector: Entity<AssistantModelSelector>,
  54    profile_selector: Entity<ProfileSelector>,
  55    edits_expanded: bool,
  56    editor_is_expanded: bool,
  57    waiting_for_summaries_to_send: bool,
  58    _subscriptions: Vec<Subscription>,
  59}
  60
  61const MAX_EDITOR_LINES: usize = 8;
  62
  63impl MessageEditor {
  64    pub fn new(
  65        fs: Arc<dyn Fs>,
  66        workspace: WeakEntity<Workspace>,
  67        context_store: Entity<ContextStore>,
  68        thread_store: WeakEntity<ThreadStore>,
  69        thread: Entity<Thread>,
  70        window: &mut Window,
  71        cx: &mut Context<Self>,
  72    ) -> Self {
  73        let context_picker_menu_handle = PopoverMenuHandle::default();
  74        let model_selector_menu_handle = PopoverMenuHandle::default();
  75
  76        let language = Language::new(
  77            language::LanguageConfig {
  78                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
  79                ..Default::default()
  80            },
  81            None,
  82        );
  83
  84        let editor = cx.new(|cx| {
  85            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
  86            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
  87            let mut editor = Editor::new(
  88                editor::EditorMode::AutoHeight {
  89                    max_lines: MAX_EDITOR_LINES,
  90                },
  91                buffer,
  92                None,
  93                window,
  94                cx,
  95            );
  96            editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
  97            editor.set_show_indent_guides(false, cx);
  98            editor.set_soft_wrap();
  99            editor.set_context_menu_options(ContextMenuOptions {
 100                min_entries_visible: 12,
 101                max_entries_visible: 12,
 102                placement: Some(ContextMenuPlacement::Above),
 103            });
 104            editor
 105        });
 106
 107        let editor_entity = editor.downgrade();
 108        editor.update(cx, |editor, _| {
 109            editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
 110                workspace.clone(),
 111                context_store.downgrade(),
 112                Some(thread_store.clone()),
 113                editor_entity,
 114            ))));
 115        });
 116
 117        let context_strip = cx.new(|cx| {
 118            ContextStrip::new(
 119                context_store.clone(),
 120                workspace.clone(),
 121                Some(thread_store.clone()),
 122                context_picker_menu_handle.clone(),
 123                SuggestContextKind::File,
 124                window,
 125                cx,
 126            )
 127        });
 128
 129        let incompatible_tools =
 130            cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));
 131
 132        let subscriptions =
 133            vec![cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event)];
 134
 135        Self {
 136            editor: editor.clone(),
 137            project: thread.read(cx).project().clone(),
 138            thread,
 139            incompatible_tools_state: incompatible_tools.clone(),
 140            workspace,
 141            context_store,
 142            context_strip,
 143            context_picker_menu_handle,
 144            model_selector: cx.new(|cx| {
 145                AssistantModelSelector::new(
 146                    fs.clone(),
 147                    model_selector_menu_handle,
 148                    editor.focus_handle(cx),
 149                    ModelType::Default,
 150                    window,
 151                    cx,
 152                )
 153            }),
 154            edits_expanded: false,
 155            editor_is_expanded: false,
 156            waiting_for_summaries_to_send: false,
 157            profile_selector: cx
 158                .new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
 159            _subscriptions: subscriptions,
 160        }
 161    }
 162
 163    fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
 164        cx.notify();
 165    }
 166
 167    pub fn expand_message_editor(
 168        &mut self,
 169        _: &ExpandMessageEditor,
 170        _window: &mut Window,
 171        cx: &mut Context<Self>,
 172    ) {
 173        self.set_editor_is_expanded(!self.editor_is_expanded, cx);
 174    }
 175
 176    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
 177        self.editor_is_expanded = is_expanded;
 178        self.editor.update(cx, |editor, _| {
 179            if self.editor_is_expanded {
 180                editor.set_mode(EditorMode::Full {
 181                    scale_ui_elements_with_buffer_font_size: false,
 182                    show_active_line_background: false,
 183                })
 184            } else {
 185                editor.set_mode(EditorMode::AutoHeight {
 186                    max_lines: MAX_EDITOR_LINES,
 187                })
 188            }
 189        });
 190        cx.notify();
 191    }
 192
 193    fn toggle_context_picker(
 194        &mut self,
 195        _: &ToggleContextPicker,
 196        window: &mut Window,
 197        cx: &mut Context<Self>,
 198    ) {
 199        self.context_picker_menu_handle.toggle(window, cx);
 200    }
 201    pub fn remove_all_context(
 202        &mut self,
 203        _: &RemoveAllContext,
 204        _window: &mut Window,
 205        cx: &mut Context<Self>,
 206    ) {
 207        self.context_store.update(cx, |store, _cx| store.clear());
 208        cx.notify();
 209    }
 210
 211    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
 212        if self.is_editor_empty(cx) {
 213            return;
 214        }
 215
 216        if self.thread.read(cx).is_generating() {
 217            self.stop_current_and_send_new_message(window, cx);
 218            return;
 219        }
 220
 221        self.set_editor_is_expanded(false, cx);
 222        self.send_to_model(RequestKind::Chat, window, cx);
 223
 224        cx.notify();
 225    }
 226
 227    fn is_editor_empty(&self, cx: &App) -> bool {
 228        self.editor.read(cx).text(cx).trim().is_empty()
 229    }
 230
 231    fn is_model_selected(&self, cx: &App) -> bool {
 232        LanguageModelRegistry::read_global(cx)
 233            .default_model()
 234            .is_some()
 235    }
 236
 237    fn send_to_model(
 238        &mut self,
 239        request_kind: RequestKind,
 240        window: &mut Window,
 241        cx: &mut Context<Self>,
 242    ) {
 243        let model_registry = LanguageModelRegistry::read_global(cx);
 244        let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
 245            return;
 246        };
 247
 248        if provider.must_accept_terms(cx) {
 249            cx.notify();
 250            return;
 251        }
 252
 253        let user_message = self.editor.update(cx, |editor, cx| {
 254            let text = editor.text(cx);
 255            editor.clear(window, cx);
 256            text
 257        });
 258
 259        let refresh_task =
 260            refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
 261
 262        let thread = self.thread.clone();
 263        let context_store = self.context_store.clone();
 264        let git_store = self.project.read(cx).git_store().clone();
 265        let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
 266
 267        cx.spawn(async move |this, cx| {
 268            let checkpoint = checkpoint.await.ok();
 269            refresh_task.await;
 270
 271            thread
 272                .update(cx, |thread, cx| {
 273                    let context = context_store.read(cx).context().clone();
 274                    thread.insert_user_message(user_message, context, checkpoint, cx);
 275                })
 276                .log_err();
 277
 278            if let Some(wait_for_summaries) = context_store
 279                .update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
 280                .log_err()
 281            {
 282                this.update(cx, |this, cx| {
 283                    this.waiting_for_summaries_to_send = true;
 284                    cx.notify();
 285                })
 286                .log_err();
 287
 288                wait_for_summaries.await;
 289
 290                this.update(cx, |this, cx| {
 291                    this.waiting_for_summaries_to_send = false;
 292                    cx.notify();
 293                })
 294                .log_err();
 295            }
 296
 297            // Send to model after summaries are done
 298            thread
 299                .update(cx, |thread, cx| {
 300                    thread.send_to_model(model, request_kind, cx);
 301                })
 302                .log_err();
 303        })
 304        .detach();
 305    }
 306
 307    fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 308        let cancelled = self
 309            .thread
 310            .update(cx, |thread, cx| thread.cancel_last_completion(cx));
 311
 312        if cancelled {
 313            self.set_editor_is_expanded(false, cx);
 314            self.send_to_model(RequestKind::Chat, window, cx);
 315        }
 316    }
 317
 318    fn handle_context_strip_event(
 319        &mut self,
 320        _context_strip: &Entity<ContextStrip>,
 321        event: &ContextStripEvent,
 322        window: &mut Window,
 323        cx: &mut Context<Self>,
 324    ) {
 325        match event {
 326            ContextStripEvent::PickerDismissed
 327            | ContextStripEvent::BlurredEmpty
 328            | ContextStripEvent::BlurredDown => {
 329                let editor_focus_handle = self.editor.focus_handle(cx);
 330                window.focus(&editor_focus_handle);
 331            }
 332            ContextStripEvent::BlurredUp => {}
 333        }
 334    }
 335
 336    fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
 337        if self.context_picker_menu_handle.is_deployed() {
 338            cx.propagate();
 339        } else {
 340            self.context_strip.focus_handle(cx).focus(window);
 341        }
 342    }
 343
 344    fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
 345        AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
 346    }
 347
 348    fn handle_file_click(
 349        &self,
 350        buffer: Entity<Buffer>,
 351        window: &mut Window,
 352        cx: &mut Context<Self>,
 353    ) {
 354        if let Ok(diff) = AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
 355        {
 356            let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
 357            diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
 358        }
 359    }
 360
 361    fn render_editor(
 362        &self,
 363        font_size: Rems,
 364        line_height: Pixels,
 365        window: &mut Window,
 366        cx: &mut Context<Self>,
 367    ) -> Div {
 368        let thread = self.thread.read(cx);
 369
 370        let editor_bg_color = cx.theme().colors().editor_background;
 371        let is_generating = thread.is_generating();
 372        let focus_handle = self.editor.focus_handle(cx);
 373
 374        let is_model_selected = self.is_model_selected(cx);
 375        let is_editor_empty = self.is_editor_empty(cx);
 376
 377        let model = LanguageModelRegistry::read_global(cx)
 378            .default_model()
 379            .map(|default| default.model.clone());
 380
 381        let incompatible_tools = model
 382            .as_ref()
 383            .map(|model| {
 384                self.incompatible_tools_state.update(cx, |state, cx| {
 385                    state
 386                        .incompatible_tools(model, cx)
 387                        .iter()
 388                        .cloned()
 389                        .collect::<Vec<_>>()
 390                })
 391            })
 392            .unwrap_or_default();
 393
 394        let is_editor_expanded = self.editor_is_expanded;
 395        let expand_icon = if is_editor_expanded {
 396            IconName::Minimize
 397        } else {
 398            IconName::Maximize
 399        };
 400
 401        v_flex()
 402            .key_context("MessageEditor")
 403            .on_action(cx.listener(Self::chat))
 404            .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
 405                this.profile_selector
 406                    .read(cx)
 407                    .menu_handle()
 408                    .toggle(window, cx);
 409            }))
 410            .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
 411                this.model_selector
 412                    .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
 413            }))
 414            .on_action(cx.listener(Self::toggle_context_picker))
 415            .on_action(cx.listener(Self::remove_all_context))
 416            .on_action(cx.listener(Self::move_up))
 417            .on_action(cx.listener(Self::toggle_chat_mode))
 418            .on_action(cx.listener(Self::expand_message_editor))
 419            .gap_2()
 420            .p_2()
 421            .bg(editor_bg_color)
 422            .border_t_1()
 423            .border_color(cx.theme().colors().border)
 424            .child(
 425                h_flex()
 426                    .items_start()
 427                    .justify_between()
 428                    .child(self.context_strip.clone())
 429                    .child(
 430                        IconButton::new("toggle-height", expand_icon)
 431                            .icon_size(IconSize::XSmall)
 432                            .icon_color(Color::Muted)
 433                            .tooltip({
 434                                let focus_handle = focus_handle.clone();
 435                                move |window, cx| {
 436                                    let expand_label = if is_editor_expanded {
 437                                        "Minimize Message Editor".to_string()
 438                                    } else {
 439                                        "Expand Message Editor".to_string()
 440                                    };
 441
 442                                    Tooltip::for_action_in(
 443                                        expand_label,
 444                                        &ExpandMessageEditor,
 445                                        &focus_handle,
 446                                        window,
 447                                        cx,
 448                                    )
 449                                }
 450                            })
 451                            .on_click(cx.listener(|_, _, window, cx| {
 452                                window.dispatch_action(Box::new(ExpandMessageEditor), cx);
 453                            })),
 454                    ),
 455            )
 456            .child(
 457                v_flex()
 458                    .size_full()
 459                    .gap_4()
 460                    .when(is_editor_expanded, |this| {
 461                        this.h(vh(0.8, window)).justify_between()
 462                    })
 463                    .child(
 464                        div()
 465                            .min_h_16()
 466                            .when(is_editor_expanded, |this| this.h_full())
 467                            .child({
 468                                let settings = ThemeSettings::get_global(cx);
 469
 470                                let text_style = TextStyle {
 471                                    color: cx.theme().colors().text,
 472                                    font_family: settings.buffer_font.family.clone(),
 473                                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
 474                                    font_features: settings.buffer_font.features.clone(),
 475                                    font_size: font_size.into(),
 476                                    line_height: line_height.into(),
 477                                    ..Default::default()
 478                                };
 479
 480                                EditorElement::new(
 481                                    &self.editor,
 482                                    EditorStyle {
 483                                        background: editor_bg_color,
 484                                        local_player: cx.theme().players().local(),
 485                                        text: text_style,
 486                                        syntax: cx.theme().syntax().clone(),
 487                                        ..Default::default()
 488                                    },
 489                                )
 490                                .into_any()
 491                            }),
 492                    )
 493                    .child(
 494                        h_flex()
 495                            .flex_none()
 496                            .justify_between()
 497                            .child(h_flex().gap_2().child(self.profile_selector.clone()))
 498                            .child(
 499                                h_flex()
 500                                    .gap_1()
 501                                    .when(!incompatible_tools.is_empty(), |this| {
 502                                        this.child(
 503                                            IconButton::new(
 504                                                "tools-incompatible-warning",
 505                                                IconName::Warning,
 506                                            )
 507                                            .icon_color(Color::Warning)
 508                                            .icon_size(IconSize::Small)
 509                                            .tooltip({
 510                                                move |_, cx| {
 511                                                    cx.new(|_| IncompatibleToolsTooltip {
 512                                                        incompatible_tools: incompatible_tools
 513                                                            .clone(),
 514                                                    })
 515                                                    .into()
 516                                                }
 517                                            }),
 518                                        )
 519                                    })
 520                                    .child(self.model_selector.clone())
 521                                    .map({
 522                                        let focus_handle = focus_handle.clone();
 523                                        move |parent| {
 524                                            if is_generating {
 525                                                parent
 526                                                    .when(is_editor_empty, |parent| {
 527                                                        parent.child(
 528                                                            IconButton::new(
 529                                                                "stop-generation",
 530                                                                IconName::StopFilled,
 531                                                            )
 532                                                            .icon_color(Color::Error)
 533                                                            .style(ButtonStyle::Tinted(
 534                                                                ui::TintColor::Error,
 535                                                            ))
 536                                                            .tooltip(move |window, cx| {
 537                                                                Tooltip::for_action(
 538                                                                    "Stop Generation",
 539                                                                    &editor::actions::Cancel,
 540                                                                    window,
 541                                                                    cx,
 542                                                                )
 543                                                            })
 544                                                            .on_click({
 545                                                                let focus_handle =
 546                                                                    focus_handle.clone();
 547                                                                move |_event, window, cx| {
 548                                                                    focus_handle.dispatch_action(
 549                                                                        &editor::actions::Cancel,
 550                                                                        window,
 551                                                                        cx,
 552                                                                    );
 553                                                                }
 554                                                            })
 555                                                            .with_animation(
 556                                                                "pulsating-label",
 557                                                                Animation::new(
 558                                                                    Duration::from_secs(2),
 559                                                                )
 560                                                                .repeat()
 561                                                                .with_easing(pulsating_between(
 562                                                                    0.4, 1.0,
 563                                                                )),
 564                                                                |icon_button, delta| {
 565                                                                    icon_button.alpha(delta)
 566                                                                },
 567                                                            ),
 568                                                        )
 569                                                    })
 570                                                    .when(!is_editor_empty, |parent| {
 571                                                        parent.child(
 572                                                    IconButton::new("send-message", IconName::Send)
 573                                                        .icon_color(Color::Accent)
 574                                                        .style(ButtonStyle::Filled)
 575                                                        .disabled(
 576                                                            !is_model_selected
 577                                                                || self
 578                                                                    .waiting_for_summaries_to_send,
 579                                                        )
 580                                                        .on_click({
 581                                                            let focus_handle = focus_handle.clone();
 582                                                            move |_event, window, cx| {
 583                                                                focus_handle.dispatch_action(
 584                                                                    &Chat, window, cx,
 585                                                                );
 586                                                            }
 587                                                        })
 588                                                        .tooltip(move |window, cx| {
 589                                                            Tooltip::for_action(
 590                                                                "Stop and Send New Message",
 591                                                                &Chat,
 592                                                                window,
 593                                                                cx,
 594                                                            )
 595                                                        }),
 596                                                )
 597                                                    })
 598                                            } else {
 599                                                parent.child(
 600                                                    IconButton::new("send-message", IconName::Send)
 601                                                        .icon_color(Color::Accent)
 602                                                        .style(ButtonStyle::Filled)
 603                                                        .disabled(
 604                                                            is_editor_empty
 605                                                                || !is_model_selected
 606                                                                || self
 607                                                                    .waiting_for_summaries_to_send,
 608                                                        )
 609                                                        .on_click({
 610                                                            let focus_handle = focus_handle.clone();
 611                                                            move |_event, window, cx| {
 612                                                                focus_handle.dispatch_action(
 613                                                                    &Chat, window, cx,
 614                                                                );
 615                                                            }
 616                                                        })
 617                                                        .when(
 618                                                            !is_editor_empty && is_model_selected,
 619                                                            |button| {
 620                                                                button.tooltip(move |window, cx| {
 621                                                                    Tooltip::for_action(
 622                                                                        "Send", &Chat, window, cx,
 623                                                                    )
 624                                                                })
 625                                                            },
 626                                                        )
 627                                                        .when(is_editor_empty, |button| {
 628                                                            button.tooltip(Tooltip::text(
 629                                                                "Type a message to submit",
 630                                                            ))
 631                                                        })
 632                                                        .when(!is_model_selected, |button| {
 633                                                            button.tooltip(Tooltip::text(
 634                                                                "Select a model to continue",
 635                                                            ))
 636                                                        }),
 637                                                )
 638                                            }
 639                                        }
 640                                    }),
 641                            ),
 642                    ),
 643            )
 644    }
 645
 646    fn render_changed_buffers(
 647        &self,
 648        changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
 649        window: &mut Window,
 650        cx: &mut Context<Self>,
 651    ) -> Div {
 652        let focus_handle = self.editor.focus_handle(cx);
 653
 654        let editor_bg_color = cx.theme().colors().editor_background;
 655        let border_color = cx.theme().colors().border;
 656        let active_color = cx.theme().colors().element_selected;
 657        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
 658        let is_edit_changes_expanded = self.edits_expanded;
 659
 660        v_flex()
 661            .mx_2()
 662            .bg(bg_edit_files_disclosure)
 663            .border_1()
 664            .border_b_0()
 665            .border_color(border_color)
 666            .rounded_t_md()
 667            .shadow(smallvec::smallvec![gpui::BoxShadow {
 668                color: gpui::black().opacity(0.15),
 669                offset: point(px(1.), px(-1.)),
 670                blur_radius: px(3.),
 671                spread_radius: px(0.),
 672            }])
 673            .child(
 674                h_flex()
 675                    .id("edits-container")
 676                    .cursor_pointer()
 677                    .p_1p5()
 678                    .justify_between()
 679                    .when(is_edit_changes_expanded, |this| {
 680                        this.border_b_1().border_color(border_color)
 681                    })
 682                    .on_click(
 683                        cx.listener(|this, _, window, cx| this.handle_review_click(window, cx)),
 684                    )
 685                    .child(
 686                        h_flex()
 687                            .gap_1()
 688                            .child(
 689                                Disclosure::new("edits-disclosure", is_edit_changes_expanded)
 690                                    .on_click(cx.listener(|this, _ev, _window, cx| {
 691                                        this.edits_expanded = !this.edits_expanded;
 692                                        cx.notify();
 693                                    })),
 694                            )
 695                            .child(
 696                                Label::new("Edits")
 697                                    .size(LabelSize::Small)
 698                                    .color(Color::Muted),
 699                            )
 700                            .child(Label::new("").size(LabelSize::XSmall).color(Color::Muted))
 701                            .child(
 702                                Label::new(format!(
 703                                    "{} {}",
 704                                    changed_buffers.len(),
 705                                    if changed_buffers.len() == 1 {
 706                                        "file"
 707                                    } else {
 708                                        "files"
 709                                    }
 710                                ))
 711                                .size(LabelSize::Small)
 712                                .color(Color::Muted),
 713                            ),
 714                    )
 715                    .child(
 716                        Button::new("review", "Review Changes")
 717                            .label_size(LabelSize::Small)
 718                            .key_binding(
 719                                KeyBinding::for_action_in(
 720                                    &OpenAgentDiff,
 721                                    &focus_handle,
 722                                    window,
 723                                    cx,
 724                                )
 725                                .map(|kb| kb.size(rems_from_px(12.))),
 726                            )
 727                            .on_click(cx.listener(|this, _, window, cx| {
 728                                this.handle_review_click(window, cx)
 729                            })),
 730                    ),
 731            )
 732            .when(is_edit_changes_expanded, |parent| {
 733                parent.child(
 734                    v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
 735                        |(index, (buffer, _diff))| {
 736                            let file = buffer.read(cx).file()?;
 737                            let path = file.path();
 738
 739                            let parent_label = path.parent().and_then(|parent| {
 740                                let parent_str = parent.to_string_lossy();
 741
 742                                if parent_str.is_empty() {
 743                                    None
 744                                } else {
 745                                    Some(
 746                                        Label::new(format!(
 747                                            "/{}{}",
 748                                            parent_str,
 749                                            std::path::MAIN_SEPARATOR_STR
 750                                        ))
 751                                        .color(Color::Muted)
 752                                        .size(LabelSize::XSmall)
 753                                        .buffer_font(cx),
 754                                    )
 755                                }
 756                            });
 757
 758                            let name_label = path.file_name().map(|name| {
 759                                Label::new(name.to_string_lossy().to_string())
 760                                    .size(LabelSize::XSmall)
 761                                    .buffer_font(cx)
 762                            });
 763
 764                            let file_icon = FileIcons::get_icon(&path, cx)
 765                                .map(Icon::from_path)
 766                                .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
 767                                .unwrap_or_else(|| {
 768                                    Icon::new(IconName::File)
 769                                        .color(Color::Muted)
 770                                        .size(IconSize::Small)
 771                                });
 772
 773                            let hover_color = cx
 774                                .theme()
 775                                .colors()
 776                                .element_background
 777                                .blend(cx.theme().colors().editor_foreground.opacity(0.025));
 778
 779                            let overlay_gradient = linear_gradient(
 780                                90.,
 781                                linear_color_stop(editor_bg_color, 1.),
 782                                linear_color_stop(editor_bg_color.opacity(0.2), 0.),
 783                            );
 784
 785                            let overlay_gradient_hover = linear_gradient(
 786                                90.,
 787                                linear_color_stop(hover_color, 1.),
 788                                linear_color_stop(hover_color.opacity(0.2), 0.),
 789                            );
 790
 791                            let element = h_flex()
 792                                .group("edited-code")
 793                                .id(("file-container", index))
 794                                .cursor_pointer()
 795                                .relative()
 796                                .py_1()
 797                                .pl_2()
 798                                .pr_1()
 799                                .gap_2()
 800                                .justify_between()
 801                                .bg(cx.theme().colors().editor_background)
 802                                .hover(|style| style.bg(hover_color))
 803                                .when(index + 1 < changed_buffers.len(), |parent| {
 804                                    parent.border_color(border_color).border_b_1()
 805                                })
 806                                .child(
 807                                    h_flex()
 808                                        .id("file-name")
 809                                        .pr_8()
 810                                        .gap_1p5()
 811                                        .max_w_full()
 812                                        .overflow_x_scroll()
 813                                        .child(file_icon)
 814                                        .child(
 815                                            h_flex()
 816                                                .gap_0p5()
 817                                                .children(name_label)
 818                                                .children(parent_label),
 819                                        ) // TODO: show lines changed
 820                                        .child(Label::new("+").color(Color::Created))
 821                                        .child(Label::new("-").color(Color::Deleted)),
 822                                )
 823                                .child(
 824                                    div().visible_on_hover("edited-code").child(
 825                                        Button::new("review", "Review")
 826                                            .label_size(LabelSize::Small)
 827                                            .on_click({
 828                                                let buffer = buffer.clone();
 829                                                cx.listener(move |this, _, window, cx| {
 830                                                    this.handle_file_click(
 831                                                        buffer.clone(),
 832                                                        window,
 833                                                        cx,
 834                                                    );
 835                                                })
 836                                            }),
 837                                    ),
 838                                )
 839                                .child(
 840                                    div()
 841                                        .id("gradient-overlay")
 842                                        .absolute()
 843                                        .h_5_6()
 844                                        .w_12()
 845                                        .bottom_0()
 846                                        .right(px(52.))
 847                                        .bg(overlay_gradient)
 848                                        .group_hover("edited-code", |style| {
 849                                            style.bg(overlay_gradient_hover)
 850                                        }),
 851                                )
 852                                .on_click({
 853                                    let buffer = buffer.clone();
 854                                    cx.listener(move |this, _, window, cx| {
 855                                        this.handle_file_click(buffer.clone(), window, cx);
 856                                    })
 857                                });
 858
 859                            Some(element)
 860                        },
 861                    )),
 862                )
 863            })
 864    }
 865
 866    fn render_token_limit_callout(
 867        &self,
 868        line_height: Pixels,
 869        token_usage_ratio: TokenUsageRatio,
 870        cx: &mut Context<Self>,
 871    ) -> Div {
 872        let heading = if token_usage_ratio == TokenUsageRatio::Exceeded {
 873            "Thread reached the token limit"
 874        } else {
 875            "Thread reaching the token limit soon"
 876        };
 877
 878        h_flex()
 879            .p_2()
 880            .gap_2()
 881            .flex_wrap()
 882            .justify_between()
 883            .bg(
 884                if token_usage_ratio == TokenUsageRatio::Exceeded {
 885                    cx.theme().status().error_background.opacity(0.1)
 886                } else {
 887                    cx.theme().status().warning_background.opacity(0.1)
 888                })
 889            .border_t_1()
 890            .border_color(cx.theme().colors().border)
 891            .child(
 892                h_flex()
 893                    .gap_2()
 894                    .items_start()
 895                    .child(
 896                        h_flex()
 897                            .h(line_height)
 898                            .justify_center()
 899                            .child(
 900                                if token_usage_ratio == TokenUsageRatio::Exceeded {
 901                                    Icon::new(IconName::X)
 902                                        .color(Color::Error)
 903                                        .size(IconSize::XSmall)
 904                                } else {
 905                                    Icon::new(IconName::Warning)
 906                                        .color(Color::Warning)
 907                                        .size(IconSize::XSmall)
 908                                }
 909                            ),
 910                    )
 911                    .child(
 912                        v_flex()
 913                            .mr_auto()
 914                            .child(Label::new(heading).size(LabelSize::Small))
 915                            .child(
 916                                Label::new(
 917                                    "Start a new thread from a summary to continue the conversation.",
 918                                )
 919                                .size(LabelSize::Small)
 920                                .color(Color::Muted),
 921                            ),
 922                    ),
 923            )
 924            .child(
 925                Button::new("new-thread", "Start New Thread")
 926                    .on_click(cx.listener(|this, _, window, cx| {
 927                        let from_thread_id = Some(this.thread.read(cx).id().clone());
 928
 929                        window.dispatch_action(Box::new(NewThread {
 930                            from_thread_id
 931                        }), cx);
 932                    }))
 933                    .icon(IconName::Plus)
 934                    .icon_position(IconPosition::Start)
 935                    .icon_size(IconSize::Small)
 936                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
 937                    .label_size(LabelSize::Small),
 938            )
 939    }
 940}
 941
 942impl Focusable for MessageEditor {
 943    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
 944        self.editor.focus_handle(cx)
 945    }
 946}
 947
 948impl Render for MessageEditor {
 949    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 950        let thread = self.thread.read(cx);
 951        let total_token_usage = thread.total_token_usage(cx);
 952
 953        let action_log = self.thread.read(cx).action_log();
 954        let changed_buffers = action_log.read(cx).changed_buffers(cx);
 955
 956        let font_size = TextSize::Small.rems(cx);
 957        let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
 958
 959        v_flex()
 960            .size_full()
 961            .when(self.waiting_for_summaries_to_send, |parent| {
 962                parent.child(
 963                    h_flex().py_3().w_full().justify_center().child(
 964                        h_flex()
 965                            .flex_none()
 966                            .px_2()
 967                            .py_2()
 968                            .bg(cx.theme().colors().editor_background)
 969                            .border_1()
 970                            .border_color(cx.theme().colors().border_variant)
 971                            .rounded_lg()
 972                            .shadow_md()
 973                            .gap_1()
 974                            .child(
 975                                Icon::new(IconName::ArrowCircle)
 976                                    .size(IconSize::XSmall)
 977                                    .color(Color::Muted)
 978                                    .with_animation(
 979                                        "arrow-circle",
 980                                        Animation::new(Duration::from_secs(2)).repeat(),
 981                                        |icon, delta| {
 982                                            icon.transform(gpui::Transformation::rotate(
 983                                                gpui::percentage(delta),
 984                                            ))
 985                                        },
 986                                    ),
 987                            )
 988                            .child(
 989                                Label::new("Summarizing context…")
 990                                    .size(LabelSize::XSmall)
 991                                    .color(Color::Muted),
 992                            ),
 993                    ),
 994                )
 995            })
 996            .when(changed_buffers.len() > 0, |parent| {
 997                parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
 998            })
 999            .child(self.render_editor(font_size, line_height, window, cx))
1000            .when(
1001                total_token_usage.ratio != TokenUsageRatio::Normal,
1002                |parent| {
1003                    parent.child(self.render_token_limit_callout(
1004                        line_height,
1005                        total_token_usage.ratio,
1006                        cx,
1007                    ))
1008                },
1009            )
1010    }
1011}