diff --git a/assets/icons/ai_edit.svg b/assets/icons/ai_edit.svg
new file mode 100644
index 0000000000..2f93ab9fd9
--- /dev/null
+++ b/assets/icons/ai_edit.svg
@@ -0,0 +1,10 @@
+
diff --git a/crates/git_ui/src/commit_message_prompt.txt b/crates/git_ui/src/commit_message_prompt.txt
index 78b31e38dd..e2dca2fc08 100644
--- a/crates/git_ui/src/commit_message_prompt.txt
+++ b/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:
diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs
index 137ca8dcec..ff7b2a295d 100644
--- a/crates/git_ui/src/commit_modal.rs
+++ b/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(
diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs
index e4c9d80ab6..241f0505c1 100644
--- a/crates/git_ui/src/git_panel.rs
+++ b/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::(window, cx);
});
+ workspace.register_action(|workspace, _: &ExpandCommitEditor, window, cx| {
+ CommitModal::toggle(workspace, window, cx)
+ });
},
)
.detach();
@@ -1438,17 +1442,11 @@ impl GitPanel {
}
/// 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;
+ pub fn generate_commit_message(&mut self, cx: &mut Context) {
+ let model = match current_language_model(cx) {
+ Some(value) => value,
+ None => return,
};
- let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
- 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) -> 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,
+ ) -> Option {
+ 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) -> Option {
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) -> (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> {
+ 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) -> impl IntoElement {
let project = self.project.read(cx);
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index 9f655a686a..fce2a8906a 100644
--- a/crates/ui/src/components/icon.rs
+++ b/crates/ui/src/components/icon.rs
@@ -128,6 +128,7 @@ pub enum IconName {
AiBedrock,
AiAnthropicHosted,
AiDeepSeek,
+ AiEdit,
AiGoogle,
AiLmStudio,
AiMistral,
diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs
index 779c730d9b..6ac97d5045 100644
--- a/crates/workspace/src/workspace.rs
+++ b/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,