835 lines · 40.1 KB · Rust
RawDownload
use std::sync::Arc; use crate::assistant_model_selector::ModelType;use collections::HashSet;use editor::actions::MoveUp;use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};use file_icons::FileIcons;use fs::Fs;use gpui::{    Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,    WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,};use language::Buffer;use language_model::{ConfiguredModel, LanguageModelRegistry};use language_model_selector::ToggleModelSelector;use multi_buffer;use project::Project;use settings::Settings;use std::time::Duration;use theme::ThemeSettings;use ui::{Disclosure, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};use util::ResultExt as _;use workspace::Workspace; use crate::assistant_model_selector::AssistantModelSelector;use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};use crate::context_store::{ContextStore, refresh_context_store_text};use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};use crate::profile_selector::ProfileSelector;use crate::thread::{RequestKind, Thread, TokenUsageRatio};use crate::thread_store::ThreadStore;use crate::{    AgentDiff, Chat, ChatMode, NewThread, OpenAgentDiff, RemoveAllContext, ThreadEvent,    ToggleContextPicker, ToggleProfileSelector,}; pub struct MessageEditor {    thread: Entity<Thread>,    editor: Entity<Editor>,    #[allow(dead_code)]    workspace: WeakEntity<Workspace>,    project: Entity<Project>,    context_store: Entity<ContextStore>,    context_strip: Entity<ContextStrip>,    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,    inline_context_picker: Entity<ContextPicker>,    inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,    model_selector: Entity<AssistantModelSelector>,    profile_selector: Entity<ProfileSelector>,    edits_expanded: bool,    waiting_for_summaries_to_send: bool,    _subscriptions: Vec<Subscription>,} impl MessageEditor {    pub fn new(        fs: Arc<dyn Fs>,        workspace: WeakEntity<Workspace>,        context_store: Entity<ContextStore>,        thread_store: WeakEntity<ThreadStore>,        thread: Entity<Thread>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let context_picker_menu_handle = PopoverMenuHandle::default();        let inline_context_picker_menu_handle = PopoverMenuHandle::default();        let model_selector_menu_handle = PopoverMenuHandle::default();         let editor = cx.new(|cx| {            let mut editor = Editor::auto_height(10, window, cx);            editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);            editor.set_show_indent_guides(false, cx);            editor.set_context_menu_options(ContextMenuOptions {                min_entries_visible: 12,                max_entries_visible: 12,                placement: Some(ContextMenuPlacement::Above),            });             editor        });         let editor_entity = editor.downgrade();        editor.update(cx, |editor, _| {            editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(                workspace.clone(),                context_store.downgrade(),                Some(thread_store.clone()),                editor_entity,            ))));        });         let inline_context_picker = cx.new(|cx| {            ContextPicker::new(                workspace.clone(),                Some(thread_store.clone()),                context_store.downgrade(),                ConfirmBehavior::Close,                window,                cx,            )        });         let context_strip = cx.new(|cx| {            ContextStrip::new(                context_store.clone(),                workspace.clone(),                Some(thread_store.clone()),                context_picker_menu_handle.clone(),                SuggestContextKind::File,                window,                cx,            )        });         let subscriptions = vec![            cx.subscribe_in(                &inline_context_picker,                window,                Self::handle_inline_context_picker_event,            ),            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),        ];         Self {            editor: editor.clone(),            project: thread.read(cx).project().clone(),            thread,            workspace,            context_store,            context_strip,            context_picker_menu_handle,            inline_context_picker,            inline_context_picker_menu_handle,            model_selector: cx.new(|cx| {                AssistantModelSelector::new(                    fs.clone(),                    model_selector_menu_handle,                    editor.focus_handle(cx),                    ModelType::Default,                    window,                    cx,                )            }),            edits_expanded: false,            waiting_for_summaries_to_send: false,            profile_selector: cx                .new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),            _subscriptions: subscriptions,        }    }     fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {        cx.notify();    }     fn toggle_context_picker(        &mut self,        _: &ToggleContextPicker,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.context_picker_menu_handle.toggle(window, cx);    }    pub fn remove_all_context(        &mut self,        _: &RemoveAllContext,        _window: &mut Window,        cx: &mut Context<Self>,    ) {        self.context_store.update(cx, |store, _cx| store.clear());        cx.notify();    }     fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {        if self.is_editor_empty(cx) {            return;        }         if self.thread.read(cx).is_generating() {            return;        }         self.send_to_model(RequestKind::Chat, window, cx);    }     fn is_editor_empty(&self, cx: &App) -> bool {        self.editor.read(cx).text(cx).is_empty()    }     fn is_model_selected(&self, cx: &App) -> bool {        LanguageModelRegistry::read_global(cx)            .default_model()            .is_some()    }     fn send_to_model(        &mut self,        request_kind: RequestKind,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let model_registry = LanguageModelRegistry::read_global(cx);        let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {            return;        };         if provider.must_accept_terms(cx) {            cx.notify();            return;        }         let user_message = self.editor.update(cx, |editor, cx| {            let text = editor.text(cx);            editor.clear(window, cx);            text        });         let refresh_task =            refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);         let system_prompt_context_task = self.thread.read(cx).load_system_prompt_context(cx);         let thread = self.thread.clone();        let context_store = self.context_store.clone();        let git_store = self.project.read(cx).git_store().clone();        let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));         cx.spawn(async move |this, cx| {            let checkpoint = checkpoint.await.ok();            refresh_task.await;            let (system_prompt_context, load_error) = system_prompt_context_task.await;             thread                .update(cx, |thread, cx| {                    thread.set_system_prompt_context(system_prompt_context);                    if let Some(load_error) = load_error {                        cx.emit(ThreadEvent::ShowError(load_error));                    }                })                .log_err();             thread                .update(cx, |thread, cx| {                    let context = context_store.read(cx).context().clone();                    thread.insert_user_message(user_message, context, checkpoint, cx);                })                .log_err();             if let Some(wait_for_summaries) = context_store                .update(cx, |context_store, cx| context_store.wait_for_summaries(cx))                .log_err()            {                this.update(cx, |this, cx| {                    this.waiting_for_summaries_to_send = true;                    cx.notify();                })                .log_err();                 wait_for_summaries.await;                 this.update(cx, |this, cx| {                    this.waiting_for_summaries_to_send = false;                    cx.notify();                })                .log_err();            }             // Send to model after summaries are done            thread                .update(cx, |thread, cx| {                    thread.send_to_model(model, request_kind, cx);                })                .log_err();        })        .detach();    }     fn handle_inline_context_picker_event(        &mut self,        _inline_context_picker: &Entity<ContextPicker>,        _event: &DismissEvent,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let editor_focus_handle = self.editor.focus_handle(cx);        window.focus(&editor_focus_handle);    }     fn handle_context_strip_event(        &mut self,        _context_strip: &Entity<ContextStrip>,        event: &ContextStripEvent,        window: &mut Window,        cx: &mut Context<Self>,    ) {        match event {            ContextStripEvent::PickerDismissed            | ContextStripEvent::BlurredEmpty            | ContextStripEvent::BlurredDown => {                let editor_focus_handle = self.editor.focus_handle(cx);                window.focus(&editor_focus_handle);            }            ContextStripEvent::BlurredUp => {}        }    }     fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {        if self.context_picker_menu_handle.is_deployed()            || self.inline_context_picker_menu_handle.is_deployed()        {            cx.propagate();        } else {            self.context_strip.focus_handle(cx).focus(window);        }    }     fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {        AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();    }     fn handle_file_click(        &self,        buffer: Entity<Buffer>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if let Ok(diff) = AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx)        {            let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);            diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));        }    }} impl Focusable for MessageEditor {    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {        self.editor.focus_handle(cx)    }} impl Render for MessageEditor {    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let font_size = TextSize::Default.rems(cx);        let line_height = font_size.to_pixels(window.rem_size()) * 1.5;         let focus_handle = self.editor.focus_handle(cx);        let inline_context_picker = self.inline_context_picker.clone();         let thread = self.thread.read(cx);        let is_generating = thread.is_generating();        let total_token_usage = thread.total_token_usage(cx);        let is_model_selected = self.is_model_selected(cx);        let is_editor_empty = self.is_editor_empty(cx);        let is_edit_changes_expanded = self.edits_expanded;         let action_log = self.thread.read(cx).action_log();        let changed_buffers = action_log.read(cx).changed_buffers(cx);        let changed_buffers_count = changed_buffers.len();         let editor_bg_color = cx.theme().colors().editor_background;        let border_color = cx.theme().colors().border;        let active_color = cx.theme().colors().element_selected;        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));         v_flex()            .size_full()            .when(self.waiting_for_summaries_to_send, |parent| {                parent.child(                    h_flex().py_3().w_full().justify_center().child(                        h_flex()                            .flex_none()                            .px_2()                            .py_2()                            .bg(editor_bg_color)                            .border_1()                            .border_color(cx.theme().colors().border_variant)                            .rounded_lg()                            .shadow_md()                            .gap_1()                            .child(                                Icon::new(IconName::ArrowCircle)                                    .size(IconSize::XSmall)                                    .color(Color::Muted)                                    .with_animation(                                        "arrow-circle",                                        Animation::new(Duration::from_secs(2)).repeat(),                                        |icon, delta| {                                            icon.transform(gpui::Transformation::rotate(                                                gpui::percentage(delta),                                            ))                                        },                                    ),                            )                            .child(                                Label::new("Summarizing context…")                                    .size(LabelSize::XSmall)                                    .color(Color::Muted),                            ),                    ),                )            })            .when(changed_buffers_count > 0, |parent| {                parent.child(                    v_flex()                        .mx_2()                        .bg(bg_edit_files_disclosure)                        .border_1()                        .border_b_0()                        .border_color(border_color)                        .rounded_t_md()                        .shadow(smallvec::smallvec![gpui::BoxShadow {                            color: gpui::black().opacity(0.15),                            offset: point(px(1.), px(-1.)),                            blur_radius: px(3.),                            spread_radius: px(0.),                        }])                        .child(                            h_flex()                                .id("edits-container")                                .cursor_pointer()                                .p_1p5()                                .justify_between()                                .when(is_edit_changes_expanded, |this| {                                    this.border_b_1().border_color(border_color)                                })                                .on_click(cx.listener(|this, _, window, cx| {                                    this.handle_review_click(window, cx)                                }))                                .child(                                    h_flex()                                        .gap_1()                                        .child(                                            Disclosure::new(                                                "edits-disclosure",                                                is_edit_changes_expanded,                                            )                                            .on_click(                                                cx.listener(|this, _ev, _window, cx| {                                                    this.edits_expanded = !this.edits_expanded;                                                    cx.notify();                                                }),                                            ),                                        )                                        .child(                                            Label::new("Edits")                                                .size(LabelSize::Small)                                                .color(Color::Muted),                                        )                                        .child(                                            Label::new("•")                                                .size(LabelSize::XSmall)                                                .color(Color::Muted),                                        )                                        .child(                                            Label::new(format!(                                                "{} {}",                                                changed_buffers_count,                                                if changed_buffers_count == 1 {                                                    "file"                                                } else {                                                    "files"                                                }                                            ))                                            .size(LabelSize::Small)                                            .color(Color::Muted),                                        ),                                )                                .child(                                    Button::new("review", "Review Changes")                                        .label_size(LabelSize::Small)                                        .key_binding(                                            KeyBinding::for_action_in(                                                &OpenAgentDiff,                                                &focus_handle,                                                window,                                                cx,                                            )                                            .map(|kb| kb.size(rems_from_px(12.))),                                        )                                        .on_click(cx.listener(|this, _, window, cx| {                                            this.handle_review_click(window, cx)                                        })),                                ),                        )                        .when(is_edit_changes_expanded, |parent| {                            parent.child(                                v_flex().children(                                    changed_buffers.into_iter().enumerate().flat_map(                                        |(index, (buffer, _diff))| {                                            let file = buffer.read(cx).file()?;                                            let path = file.path();                                             let parent_label = path.parent().and_then(|parent| {                                                let parent_str = parent.to_string_lossy();                                                 if parent_str.is_empty() {                                                    None                                                } else {                                                    Some(                                                        Label::new(format!(

Lines 1–500 of 835.