diff --git a/Cargo.lock b/Cargo.lock index 7c266773bb4aa9e1b7f56175049ae1bd453d6747..c9494b4093d17fc322adae756e30f3f3436da501 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5446,6 +5446,7 @@ dependencies = [ "gpui", "itertools 0.14.0", "language", + "language_model", "linkify", "linkme", "log", diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index cd287c3f1900c523deb43f7d054ac21ef19f5010..43e2f030ec63e5ed5a8406f340f020a7f7975855 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -31,6 +31,7 @@ git.workspace = true gpui.workspace = true itertools.workspace = true language.workspace = true +language_model.workspace = true linkify.workspace = true linkme.workspace = true log.workspace = true diff --git a/crates/git_ui/src/commit_message_prompt.txt b/crates/git_ui/src/commit_message_prompt.txt new file mode 100644 index 0000000000000000000000000000000000000000..56ad556868a596b362e6a87a51a371e02f06d9e2 --- /dev/null +++ b/crates/git_ui/src/commit_message_prompt.txt @@ -0,0 +1,15 @@ +You are an expert at writing Git commits. Your job is to write a clear commit message that summarizes the changes. + +Only return the commit message in your response. Do not include any additional meta-commentary about the task. + +Follow good Git style: + +- Separate the subject from the body with a blank line +- Try to limit the subject line to 50 characters +- Capitalize the subject line +- Do not end the subject line with any punctuation +- Use the imperative mood in the subject line +- Wrap the body at 72 characters +- Use the body to explain *what* and *why* vs. *how* + +Here are the changes in this commit: diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 96e41d058d7c460e86cc3d4320231b2916d7c216..ca6cd457c511be42b3e5e4da6009379fc9395b2c 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -17,6 +17,7 @@ use editor::{ scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar, }; +use futures::StreamExt as _; use git::repository::{ Branch, CommitDetails, CommitSummary, PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, @@ -33,6 +34,9 @@ use gpui::{ }; use itertools::Itertools; use language::{Buffer, File}; +use language_model::{ + LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, +}; use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use multi_buffer::ExcerptInfo; use panel::{ @@ -216,6 +220,7 @@ pub struct GitPanel { conflicted_staged_count: usize, current_modifiers: Modifiers, add_coauthors: bool, + generate_commit_message_task: Option>>, entries: Vec, focus_handle: FocusHandle, fs: Arc, @@ -334,44 +339,46 @@ impl GitPanel { ) .detach(); - let scrollbar_state = - ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()); - - let mut git_panel = Self { - pending_remote_operations: Default::default(), - remote_operation_id: 0, - active_repository, - commit_editor, - conflicted_count: 0, - conflicted_staged_count: 0, - current_modifiers: window.modifiers(), - add_coauthors: true, - entries: Vec::new(), - focus_handle: cx.focus_handle(), - fs, - hide_scrollbar_task: None, - new_count: 0, - new_staged_count: 0, - pending: Vec::new(), - pending_commit: None, - pending_serialization: Task::ready(None), - project, - scroll_handle, - scrollbar_state, - selected_entry: None, - marked_entries: Vec::new(), - show_scrollbar: false, - tracked_count: 0, - tracked_staged_count: 0, - update_visible_entries_task: Task::ready(()), - width: Some(px(360.)), - context_menu: None, - workspace, - modal_open: false, - }; - git_panel.schedule_update(false, window, cx); - git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); - git_panel + let scrollbar_state = + ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()); + + let mut git_panel = Self { + pending_remote_operations: Default::default(), + remote_operation_id: 0, + active_repository, + commit_editor, + conflicted_count: 0, + conflicted_staged_count: 0, + current_modifiers: window.modifiers(), + add_coauthors: true, + generate_commit_message_task: None, + entries: Vec::new(), + focus_handle: cx.focus_handle(), + fs, + hide_scrollbar_task: None, + new_count: 0, + new_staged_count: 0, + pending: Vec::new(), + pending_commit: None, + pending_serialization: Task::ready(None), + project, + scroll_handle, + scrollbar_state, + selected_entry: None, + marked_entries: Vec::new(), + show_scrollbar: false, + tracked_count: 0, + tracked_staged_count: 0, + update_visible_entries_task: Task::ready(()), + width: Some(px(360.)), + context_menu: None, + workspace, + modal_open: false, + }; + git_panel.schedule_update(false, window, cx); + git_panel.show_scrollbar = git_panel.should_show_scrollbar(cx); + git_panel + }) } pub fn entry_by_path(&self, path: &RepoPath) -> Option { @@ -1422,6 +1429,71 @@ impl GitPanel { Some(format!("{} {}", action_text, file_name)) } + /// Generates a commit message using an LLM. + fn generate_commit_message(&mut self, cx: &mut Context) { + let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else { + return; + }; + let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else { + return; + }; + + if !provider.is_authenticated(cx) { + return; + } + + const PROMPT: &str = include_str!("commit_message_prompt.txt"); + + // TODO: We need to generate a diff from the actual Git state. + // + // It need not look exactly like the structure below, this is just an example generated by Claude. + let diff_text = "diff --git a/src/main.rs b/src/main.rs +index 1234567..abcdef0 100644 +--- a/src/main.rs ++++ b/src/main.rs +@@ -10,7 +10,7 @@ fn main() { + println!(\"Hello, world!\"); +- let unused_var = 42; ++ let important_value = 42; + + // Do something with the value +- // TODO: Implement this later ++ println!(\"The answer is {}\", important_value); + }"; + + let request = LanguageModelRequest { + messages: vec![LanguageModelRequestMessage { + role: Role::User, + content: vec![format!("{PROMPT}\n{diff_text}").into()], + cache: false, + }], + tools: Vec::new(), + stop: Vec::new(), + temperature: None, + }; + + self.generate_commit_message_task = Some(cx.spawn(|this, mut cx| { + async move { + let stream = model.stream_completion_text(request, &cx); + let mut messages = stream.await?; + + while let Some(message) = messages.stream.next().await { + let text = message?; + + 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, text)], None, cx); + }); + })?; + } + + anyhow::Ok(()) + } + .log_err() + })); + } + fn update_editor_placeholder(&mut self, cx: &mut Context) { let suggested_commit_message = self.suggest_commit_message(); let placeholder_text = suggested_commit_message @@ -2172,6 +2244,8 @@ impl GitPanel { let panel_editor_style = panel_editor_style(true, window, cx); let enable_coauthors = self.render_co_authors(cx); + // Note: This is hard-coded to `false` as it is not fully implemented. + let show_generate_commit_message_button = false; let title = self.commit_button_title(); let editor_focus_handle = self.commit_editor.focus_handle(cx); @@ -2219,8 +2293,18 @@ impl GitPanel { .absolute() .bottom_0() .right_2() + .gap_0p5() .h(footer_size) .flex_none() + .when(show_generate_commit_message_button, |parent| { + parent.child( + panel_filled_button("Generate Commit Message").on_click( + cx.listener(move |this, _event, _window, cx| { + this.generate_commit_message(cx); + }), + ), + ) + }) .children(enable_coauthors) .child( panel_filled_button(title)