diff --git a/Cargo.lock b/Cargo.lock index 300fd87dd0..19ad12ae09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5438,6 +5438,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 fc5f777da2..8e4eabf59f 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -30,6 +30,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 0000000000..56ad556868 --- /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 2f62c22e65..2616f292e1 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -12,6 +12,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, @@ -21,6 +22,9 @@ use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll}; 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::{ @@ -195,6 +199,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, @@ -318,6 +323,7 @@ impl GitPanel { 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, @@ -1346,6 +1352,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 @@ -2013,6 +2084,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); @@ -2060,8 +2133,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)