Improve the generate commit message design (#26233)

Marshall Bowers , Mikayla Maki , and Danilo Leal created

[WIP]

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

assets/icons/ai_edit.svg                    |  10 
crates/git_ui/src/commit_message_prompt.txt |   2 
crates/git_ui/src/commit_modal.rs           |  11 
crates/git_ui/src/git_panel.rs              | 267 +++++++++++++---------
crates/ui/src/components/icon.rs            |   1 
crates/workspace/src/workspace.rs           |   4 
6 files changed, 181 insertions(+), 114 deletions(-)

Detailed changes

assets/icons/ai_edit.svg 🔗

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.5871 5.40624C12.8514 5.14195 13 4.78346 13 4.40965C13 4.03583 12.8516 3.67731 12.5873 3.41295C12.323 3.14859 11.9645 3.00005 11.5907 3C11.2169 2.99995 10.8584 3.14841 10.594 3.4127L3.92098 10.0874C3.80488 10.2031 3.71903 10.3456 3.67097 10.5024L3.01047 12.6784C2.99754 12.7217 2.99657 12.7676 3.00764 12.8113C3.01872 12.8551 3.04143 12.895 3.07337 12.9269C3.1053 12.9588 3.14528 12.9815 3.18905 12.9925C3.23282 13.0035 3.27875 13.0024 3.32197 12.9894L5.49849 12.3294C5.65508 12.2818 5.79758 12.1964 5.91349 12.0809L12.5871 5.40624Z" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 4L12 6" stroke="black" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.38818 3.53598V2.53598" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.56982 12.6995L9.56982 13.6995" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.38818 6.53598H3.38818" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5698 9.69949L12.5698 9.69949" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.38818 4.53598L3.38818 3.53598" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.5698 11.6995L12.5698 12.6995" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

crates/git_ui/src/commit_message_prompt.txt 🔗

@@ -15,5 +15,3 @@ Follow good Git style:
 - Use the imperative mood in the subject line
 - Wrap the body at 72 characters
 - Keep the body short and concise (omit it entirely if not useful)
-
-Here are the changes in this commit:

crates/git_ui/src/commit_modal.rs 🔗

@@ -304,8 +304,11 @@ impl CommitModal {
             git_panel.update(cx, |git_panel, cx| git_panel.editor_focus_handle(cx));
 
         let commit_button = panel_filled_button(commit_label)
-            .tooltip(move |window, cx| {
-                Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
+            .tooltip({
+                let panel_editor_focus_handle = panel_editor_focus_handle.clone();
+                move |window, cx| {
+                    Tooltip::for_action_in(tooltip, &Commit, &panel_editor_focus_handle, window, cx)
+                }
             })
             .disabled(!can_commit)
             .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
@@ -328,8 +331,8 @@ impl CommitModal {
                 h_flex()
                     .gap_1()
                     .child(branch_picker)
-                    .children(co_authors)
-                    .child(generate_commit_message),
+                    .children(generate_commit_message)
+                    .children(co_authors),
             )
             .child(div().flex_1())
             .child(

crates/git_ui/src/git_panel.rs 🔗

@@ -23,7 +23,7 @@ use git::repository::{
     ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
 };
 use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
-use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
+use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
 use gpui::{
     actions, anchored, deferred, hsla, percentage, point, uniform_list, Action, Animation,
     AnimationExt as _, AnyView, BoxShadow, ClickEvent, Corner, DismissEvent, Entity, EventEmitter,
@@ -35,7 +35,7 @@ use gpui::{
 use itertools::Itertools;
 use language::{Buffer, File};
 use language_model::{
-    LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
+    LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
 };
 use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
 use multi_buffer::ExcerptInfo;
@@ -78,6 +78,7 @@ actions!(
         FocusEditor,
         FocusChanges,
         ToggleFillCoAuthors,
+        GenerateCommitMessage
     ]
 );
 
@@ -124,6 +125,9 @@ pub fn init(cx: &mut App) {
             workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
                 workspace.toggle_panel_focus::<GitPanel>(window, cx);
             });
+            workspace.register_action(|workspace, _: &ExpandCommitEditor, window, cx| {
+                CommitModal::toggle(workspace, window, cx)
+            });
         },
     )
     .detach();
@@ -1438,18 +1442,12 @@ impl GitPanel {
     }
 
     /// Generates a commit message using an LLM.
-    fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
-        let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
-            return;
-        };
-        let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
-            return;
+    pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
+        let model = match current_language_model(cx) {
+            Some(value) => value,
+            None => return,
         };
 
-        if !provider.is_authenticated(cx) {
-            return;
-        }
-
         let Some(repo) = self.active_repository.as_ref() else {
             return;
         };
@@ -1478,17 +1476,30 @@ impl GitPanel {
                 });
 
                 let mut diff_text = diff.await??;
+
                 const ONE_MB: usize = 1_000_000;
                 if diff_text.len() > ONE_MB {
                     diff_text = diff_text.chars().take(ONE_MB).collect()
                 }
 
+                let subject = this.update(&mut cx, |this, cx| {
+                    this.commit_editor.read(cx).text(cx).lines().next().map(ToOwned::to_owned).unwrap_or_default()
+                })?;
+
+                let text_empty = subject.trim().is_empty();
+
+                let content = if text_empty {
+                    format!("{PROMPT}\nHere are the changes in this commit:\n{diff_text}")
+                } else {
+                    format!("{PROMPT}\nHere is the user's subject line:\n{subject}\nHere are the changes in this commit:\n{diff_text}\n")
+                };
+
                 const PROMPT: &str = include_str!("commit_message_prompt.txt");
 
                 let request = LanguageModelRequest {
                     messages: vec![LanguageModelRequestMessage {
                         role: Role::User,
-                        content: vec![format!("{PROMPT}\n{diff_text}").into()],
+                        content: vec![content.into()],
                         cache: false,
                     }],
                     tools: Vec::new(),
@@ -1499,6 +1510,15 @@ impl GitPanel {
                 let stream = model.stream_completion_text(request, &cx);
                 let mut messages = stream.await?;
 
+                if !text_empty {
+                    this.update(&mut cx, |this, cx| {
+                        this.commit_message_buffer(cx).update(cx, |buffer, cx| {
+                            let insert_position = buffer.anchor_before(buffer.len());
+                            buffer.edit([(insert_position..insert_position, "\n")], None, cx)
+                        });
+                    })?;
+                }
+
                 while let Some(message) = messages.stream.next().await {
                     let text = message?;
 
@@ -2178,64 +2198,82 @@ impl GitPanel {
         self.has_staged_changes()
     }
 
-    pub(crate) fn render_generate_commit_message_button(&self, cx: &Context<Self>) -> AnyElement {
-        if self.generate_commit_message_task.is_some() {
-            return Icon::new(IconName::ArrowCircle)
-                .size(IconSize::XSmall)
-                .color(Color::Info)
-                .with_animation(
-                    "arrow-circle",
-                    Animation::new(Duration::from_secs(2)).repeat(),
-                    |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
-                )
-                .into_any_element();
-        }
+    pub(crate) fn render_generate_commit_message_button(
+        &self,
+        cx: &Context<Self>,
+    ) -> Option<AnyElement> {
+        current_language_model(cx).is_some().then(|| {
+            if self.generate_commit_message_task.is_some() {
+                return h_flex()
+                    .gap_1()
+                    .child(
+                        Icon::new(IconName::ArrowCircle)
+                            .size(IconSize::XSmall)
+                            .color(Color::Info)
+                            .with_animation(
+                                "arrow-circle",
+                                Animation::new(Duration::from_secs(2)).repeat(),
+                                |icon, delta| {
+                                    icon.transform(Transformation::rotate(percentage(delta)))
+                                },
+                            ),
+                    )
+                    .child(
+                        Label::new("Generating Commit...")
+                            .size(LabelSize::Small)
+                            .color(Color::Muted),
+                    )
+                    .into_any_element();
+            }
 
-        IconButton::new("generate-commit-message", IconName::ZedAssistant)
-            .shape(ui::IconButtonShape::Square)
-            .tooltip(Tooltip::for_action_title_in(
-                "Generate commit message",
-                &git::GenerateCommitMessage,
-                &self.commit_editor.focus_handle(cx),
-            ))
-            .on_click(cx.listener(move |this, _event, _window, cx| {
-                this.generate_commit_message(cx);
-            }))
-            .into_any_element()
+            IconButton::new("generate-commit-message", IconName::AiEdit)
+                .shape(ui::IconButtonShape::Square)
+                .icon_color(Color::Muted)
+                .tooltip(Tooltip::for_action_title_in(
+                    "Generate Commit Message",
+                    &git::GenerateCommitMessage,
+                    &self.commit_editor.focus_handle(cx),
+                ))
+                .on_click(cx.listener(move |this, _event, _window, cx| {
+                    this.generate_commit_message(cx);
+                }))
+                .into_any_element()
+        })
     }
 
     pub(crate) fn render_co_authors(&self, cx: &Context<Self>) -> Option<AnyElement> {
         let potential_co_authors = self.potential_co_authors(cx);
-        if potential_co_authors.is_empty() {
-            None
-        } else {
-            Some(
-                IconButton::new("co-authors", IconName::Person)
-                    .icon_color(Color::Disabled)
-                    .selected_icon_color(Color::Selected)
-                    .toggle_state(self.add_coauthors)
-                    .tooltip(move |_, cx| {
-                        let title = format!(
-                            "Add co-authored-by:{}{}",
-                            if potential_co_authors.len() == 1 {
-                                ""
-                            } else {
-                                "\n"
-                            },
-                            potential_co_authors
-                                .iter()
-                                .map(|(name, email)| format!(" {} <{}>", name, email))
-                                .join("\n")
-                        );
-                        Tooltip::simple(title, cx)
-                    })
-                    .on_click(cx.listener(|this, _, _, cx| {
-                        this.add_coauthors = !this.add_coauthors;
-                        cx.notify();
-                    }))
-                    .into_any_element(),
-            )
-        }
+        // if potential_co_authors.is_empty() {
+        //     None
+        // } else {
+        Some(
+            IconButton::new("co-authors", IconName::Person)
+                .shape(ui::IconButtonShape::Square)
+                .icon_color(Color::Disabled)
+                .selected_icon_color(Color::Selected)
+                .toggle_state(self.add_coauthors)
+                .tooltip(move |_, cx| {
+                    let title = format!(
+                        "Add co-authored-by:{}{}",
+                        if potential_co_authors.len() == 1 {
+                            ""
+                        } else {
+                            "\n"
+                        },
+                        potential_co_authors
+                            .iter()
+                            .map(|(name, email)| format!(" {} <{}>", name, email))
+                            .join("\n")
+                    );
+                    Tooltip::simple(title, cx)
+                })
+                .on_click(cx.listener(|this, _, _, cx| {
+                    this.add_coauthors = !this.add_coauthors;
+                    cx.notify();
+                }))
+                .into_any_element(),
+        )
+        // }
     }
 
     pub fn configure_commit_button(&self, cx: &mut Context<Self>) -> (bool, &'static str) {
@@ -2292,19 +2330,18 @@ impl GitPanel {
         let panel_editor_style = panel_editor_style(true, window, cx);
 
         let enable_coauthors = self.render_co_authors(cx);
-
         let title = self.commit_button_title();
+
         let editor_focus_handle = self.commit_editor.focus_handle(cx);
+        let commit_tooltip_focus_handle = editor_focus_handle.clone();
+        let expand_tooltip_focus_handle = editor_focus_handle.clone();
 
         let branch = active_repository.read(cx).current_branch().cloned();
 
         let footer_size = px(32.);
         let gap = px(8.0);
-
         let max_height = window.line_height() * 5. + gap + footer_size;
 
-        let expand_button_size = px(16.);
-
         let git_panel = cx.entity().clone();
         let display_name = SharedString::from(Arc::from(
             active_repository
@@ -2325,9 +2362,9 @@ impl GitPanel {
                     .id("commit-editor-container")
                     .relative()
                     .h(max_height)
-                    // .w_full()
-                    // .border_t_1()
-                    // .border_color(cx.theme().colors().border)
+                    .w_full()
+                    .border_t_1()
+                    .border_color(cx.theme().colors().border_variant)
                     .bg(cx.theme().colors().editor_background)
                     .cursor_text()
                     .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
@@ -2338,54 +2375,66 @@ impl GitPanel {
                             .id("commit-footer")
                             .absolute()
                             .bottom_0()
-                            .right_2()
-                            .gap_0p5()
+                            .left_0()
+                            .w_full()
+                            .px_2()
                             .h(footer_size)
                             .flex_none()
-                            .children(enable_coauthors)
-                            .child(self.render_generate_commit_message_button(cx))
+                            .justify_between()
                             .child(
-                                panel_filled_button(title)
-                                    .tooltip(move |window, cx| {
-                                        if can_commit {
-                                            Tooltip::for_action_in(
-                                                tooltip,
-                                                &Commit,
-                                                &editor_focus_handle,
-                                                window,
-                                                cx,
-                                            )
-                                        } else {
-                                            Tooltip::simple(tooltip, cx)
-                                        }
-                                    })
-                                    .disabled(!can_commit || self.modal_open)
-                                    .on_click({
-                                        cx.listener(move |this, _: &ClickEvent, window, cx| {
-                                            telemetry::event!(
-                                                "Git Committed",
-                                                source = "Git Panel"
-                                            );
-                                            this.commit_changes(window, cx)
+                                self.render_generate_commit_message_button(cx)
+                                    .unwrap_or_else(|| div().into_any_element()),
+                            )
+                            .child(
+                                h_flex().gap_0p5().children(enable_coauthors).child(
+                                    panel_filled_button(title)
+                                        .tooltip(move |window, cx| {
+                                            if can_commit {
+                                                Tooltip::for_action_in(
+                                                    tooltip,
+                                                    &Commit,
+                                                    &commit_tooltip_focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                            } else {
+                                                Tooltip::simple(tooltip, cx)
+                                            }
                                         })
-                                    }),
+                                        .disabled(!can_commit || self.modal_open)
+                                        .on_click({
+                                            cx.listener(move |this, _: &ClickEvent, window, cx| {
+                                                this.commit_changes(window, cx)
+                                            })
+                                        }),
+                                ),
                             ),
                     )
-                    // .when(!self.modal_open, |el| {
-                    .child(EditorElement::new(&self.commit_editor, panel_editor_style))
                     .child(
                         div()
+                            .pr_2p5()
+                            .child(EditorElement::new(&self.commit_editor, panel_editor_style)),
+                    )
+                    .child(
+                        h_flex()
                             .absolute()
-                            .top_1()
+                            .top_2()
                             .right_2()
                             .opacity(0.5)
                             .hover(|this| this.opacity(1.0))
-                            .w(expand_button_size)
                             .child(
                                 panel_icon_button("expand-commit-editor", IconName::Maximize)
                                     .icon_size(IconSize::Small)
-                                    .style(ButtonStyle::Transparent)
-                                    .width(expand_button_size.into())
+                                    .size(ui::ButtonSize::Default)
+                                    .tooltip(move |window, cx| {
+                                        Tooltip::for_action_in(
+                                            "Open Commit Modal",
+                                            &git::ExpandCommitEditor,
+                                            &expand_tooltip_focus_handle,
+                                            window,
+                                            cx,
+                                        )
+                                    })
                                     .on_click(cx.listener({
                                         move |_, _, window, cx| {
                                             window.dispatch_action(
@@ -2963,6 +3012,12 @@ impl GitPanel {
     }
 }
 
+fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
+    let provider = LanguageModelRegistry::read_global(cx).active_provider()?;
+    let model = LanguageModelRegistry::read_global(cx).active_model()?;
+    provider.is_authenticated(cx).then(|| model)
+}
+
 impl Render for GitPanel {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let project = self.project.read(cx);

crates/workspace/src/workspace.rs 🔗

@@ -35,7 +35,7 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
+    action_as, actions, canvas, deferred, impl_action_as, impl_actions, point, relative, size,
     transparent_black, Action, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds,
     Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
     Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions,
@@ -5546,7 +5546,7 @@ impl Render for Workspace {
                                 .children(self.render_notifications(window, cx)),
                         )
                         .child(self.status_bar.clone())
-                        .child(self.modal_layer.clone())
+                        .child(deferred(self.modal_layer.clone()))
                         .child(self.toast_layer.clone()),
                 ),
             window,