Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:25:31.493Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

commit_modal.rs

702 lines · 26.9 KB · rust
1use crate::branch_picker::{self, BranchList};
2use crate::git_panel::{
3    GitPanel, commit_message_editor, commit_title_exceeds_limit, git_commit_editor_style,
4};
5use crate::git_panel_settings::GitPanelSettings;
6use git::{Amend, Commit, GenerateCommitMessage, Signoff, SkipHooks};
7use project::DisableAiSettings;
8use settings::Settings;
9use ui::{
10    ButtonLike, ContextMenu, ContextMenuEntry, DocumentationSide, ElevationIndex, KeybindingHint,
11    PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*,
12};
13use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
14
15use editor::{Editor, EditorElement};
16use gpui::*;
17use util::ResultExt;
18use workspace::{
19    ModalView, Workspace,
20    dock::{Dock, PanelHandle},
21};
22
23// nate: It is a pain to get editors to size correctly and not overflow.
24//
25// this can get replaced with a simple flex layout with more time/a more thoughtful approach.
26#[derive(Debug, Clone, Copy)]
27pub struct ModalContainerProperties {
28    pub modal_width: f32,
29    pub editor_height: f32,
30    pub footer_height: f32,
31    pub container_padding: f32,
32    pub modal_border_radius: f32,
33}
34
35impl ModalContainerProperties {
36    pub fn new(window: &Window, preferred_char_width: usize) -> Self {
37        let container_padding = 5.0;
38
39        // Calculate width based on character width
40        let mut modal_width = 460.0;
41        let style = window.text_style();
42        let font_id = window.text_system().resolve_font(&style.font());
43        let font_size = style.font_size.to_pixels(window.rem_size());
44
45        if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
46            modal_width =
47                f32::from(preferred_char_width as f32 * em_width + px(container_padding * 2.0));
48        }
49
50        Self {
51            modal_width,
52            editor_height: 300.0,
53            footer_height: 24.0,
54            container_padding,
55            modal_border_radius: 12.0,
56        }
57    }
58
59    pub fn editor_border_radius(&self) -> Pixels {
60        px(self.modal_border_radius - self.container_padding / 2.0)
61    }
62}
63
64pub struct CommitModal {
65    git_panel: Entity<GitPanel>,
66    commit_editor: Entity<Editor>,
67    restore_dock: RestoreDock,
68    properties: ModalContainerProperties,
69    branch_list_handle: PopoverMenuHandle<BranchList>,
70    commit_menu_handle: PopoverMenuHandle<ContextMenu>,
71}
72
73impl Focusable for CommitModal {
74    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
75        self.commit_editor.focus_handle(cx)
76    }
77}
78
79impl EventEmitter<DismissEvent> for CommitModal {}
80impl ModalView for CommitModal {
81    fn on_before_dismiss(
82        &mut self,
83        window: &mut Window,
84        cx: &mut Context<Self>,
85    ) -> workspace::DismissDecision {
86        self.git_panel.update(cx, |git_panel, cx| {
87            git_panel.set_modal_open(false, cx);
88        });
89        self.restore_dock
90            .dock
91            .update(cx, |dock, cx| {
92                if let Some(active_index) = self.restore_dock.active_index {
93                    dock.activate_panel(active_index, window, cx)
94                }
95                dock.set_open(self.restore_dock.is_open, window, cx)
96            })
97            .log_err();
98        workspace::DismissDecision::Dismiss(true)
99    }
100}
101
102struct RestoreDock {
103    dock: WeakEntity<Dock>,
104    is_open: bool,
105    active_index: Option<usize>,
106}
107
108pub enum ForceMode {
109    Amend,
110    Commit,
111}
112
113impl CommitModal {
114    pub fn register(workspace: &mut Workspace) {
115        workspace.register_action(|workspace, _: &Commit, window, cx| {
116            CommitModal::toggle(workspace, Some(ForceMode::Commit), window, cx);
117        });
118        workspace.register_action(|workspace, _: &Amend, window, cx| {
119            CommitModal::toggle(workspace, Some(ForceMode::Amend), window, cx);
120        });
121    }
122
123    pub fn toggle(
124        workspace: &mut Workspace,
125        force_mode: Option<ForceMode>,
126        window: &mut Window,
127        cx: &mut Context<Workspace>,
128    ) {
129        let Some(git_panel) = workspace.panel::<GitPanel>(cx) else {
130            return;
131        };
132
133        git_panel.update(cx, |git_panel, cx| {
134            if let Some(force_mode) = force_mode {
135                match force_mode {
136                    ForceMode::Amend => {
137                        if git_panel
138                            .active_repository
139                            .as_ref()
140                            .and_then(|repo| repo.read(cx).head_commit.as_ref())
141                            .is_some()
142                            && !git_panel.amend_pending()
143                        {
144                            git_panel.set_amend_pending(true, cx);
145                            git_panel.load_last_commit_message(cx);
146                        }
147                    }
148                    ForceMode::Commit => {
149                        if git_panel.amend_pending() {
150                            git_panel.set_amend_pending(false, cx);
151                        }
152                    }
153                }
154            }
155            git_panel.set_modal_open(true, cx);
156            git_panel.load_local_committer(cx);
157        });
158
159        let dock = workspace.dock_at_position(git_panel.position(window, cx));
160        let is_open = dock.read(cx).is_open();
161        let active_index = dock.read(cx).active_panel_index();
162        let dock = dock.downgrade();
163        let restore_dock_position = RestoreDock {
164            dock,
165            is_open,
166            active_index,
167        };
168
169        workspace.open_panel::<GitPanel>(window, cx);
170        workspace.toggle_modal(window, cx, move |window, cx| {
171            CommitModal::new(git_panel, restore_dock_position, window, cx)
172        })
173    }
174
175    fn new(
176        git_panel: Entity<GitPanel>,
177        restore_dock: RestoreDock,
178        window: &mut Window,
179        cx: &mut Context<Self>,
180    ) -> Self {
181        let panel = git_panel.read(cx);
182        let suggested_commit_message = panel.suggest_commit_message(cx);
183
184        let commit_editor = git_panel.update(cx, |git_panel, cx| {
185            git_panel.set_modal_open(true, cx);
186            let buffer = git_panel.commit_message_buffer(cx);
187            let panel_editor = git_panel.commit_editor.clone();
188            let project = git_panel.project.clone();
189
190            cx.new(|cx| {
191                let mut editor =
192                    commit_message_editor(buffer, None, project.clone(), false, window, cx);
193                editor.sync_selections(panel_editor, cx).detach();
194
195                editor
196            })
197        });
198
199        let commit_message = commit_editor.read(cx).text(cx);
200
201        if let Some(suggested_commit_message) = suggested_commit_message
202            && commit_message.is_empty()
203        {
204            commit_editor.update(cx, |editor, cx| {
205                editor.set_placeholder_text(&suggested_commit_message, window, cx);
206            });
207        }
208
209        let focus_handle = commit_editor.focus_handle(cx);
210
211        cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
212            if !this.branch_list_handle.is_focused(window, cx)
213                && !this.commit_menu_handle.is_focused(window, cx)
214            {
215                cx.emit(DismissEvent);
216            }
217        })
218        .detach();
219
220        let properties = ModalContainerProperties::new(window, 50);
221
222        Self {
223            git_panel,
224            commit_editor,
225            restore_dock,
226            properties,
227            branch_list_handle: PopoverMenuHandle::default(),
228            commit_menu_handle: PopoverMenuHandle::default(),
229        }
230    }
231
232    fn commit_editor_element(&self, _window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
233        let settings = theme_settings::ThemeSettings::get_global(cx);
234        let editor_style = git_commit_editor_style(settings.git_commit_buffer_font_size(cx), cx);
235        EditorElement::new(&self.commit_editor, editor_style)
236    }
237
238    pub fn render_commit_editor(
239        &self,
240        window: &mut Window,
241        cx: &mut Context<Self>,
242    ) -> impl IntoElement {
243        let properties = self.properties;
244        let padding_t = 3.0;
245        let padding_b = 6.0;
246        // magic number for editor not to overflow the container??
247        let extra_space_hack = 1.5 * window.line_height();
248
249        v_flex()
250            .h(px(properties.editor_height + padding_b + padding_t) + extra_space_hack)
251            .w_full()
252            .flex_none()
253            .rounded(properties.editor_border_radius())
254            .overflow_hidden()
255            .px_1p5()
256            .pt(px(padding_t))
257            .pb(px(padding_b))
258            .child(
259                div()
260                    .h(px(properties.editor_height))
261                    .w_full()
262                    .child(self.commit_editor_element(window, cx)),
263            )
264    }
265
266    fn render_git_commit_menu(
267        &self,
268        id: impl Into<ElementId>,
269        keybinding_target: Option<FocusHandle>,
270        disabled: bool,
271    ) -> impl IntoElement {
272        let menu_open = self.commit_menu_handle.is_deployed();
273
274        PopoverMenu::new(id.into())
275            .with_handle(self.commit_menu_handle.clone())
276            .trigger(
277                crate::render_split_button_chevron_trigger(
278                    "modal-commit-split-button-right",
279                    menu_open,
280                )
281                .disabled(disabled),
282            )
283            .menu({
284                let git_panel_entity = self.git_panel.clone();
285                move |window, cx| {
286                    let git_panel = git_panel_entity.read(cx);
287                    let amend_enabled = git_panel.amend_pending();
288                    let signoff_enabled = git_panel.signoff_enabled();
289                    let skip_hooks_enabled = git_panel.skip_hooks_enabled();
290                    let has_previous_commit = git_panel.head_commit(cx).is_some();
291
292                    Some(ContextMenu::build(window, cx, |context_menu, _, _| {
293                        context_menu
294                            .when_some(keybinding_target.clone(), |el, keybinding_target| {
295                                el.context(keybinding_target)
296                            })
297                            .when(has_previous_commit, |this| {
298                                this.toggleable_entry(
299                                    "Amend",
300                                    amend_enabled,
301                                    IconPosition::Start,
302                                    Some(Box::new(Amend)),
303                                    {
304                                        let git_panel = git_panel_entity.downgrade();
305                                        move |_, cx| {
306                                            git_panel
307                                                .update(cx, |git_panel, cx| {
308                                                    git_panel.toggle_amend_pending(cx);
309                                                })
310                                                .ok();
311                                        }
312                                    },
313                                )
314                            })
315                            .toggleable_entry(
316                                "Signoff",
317                                signoff_enabled,
318                                IconPosition::Start,
319                                Some(Box::new(Signoff)),
320                                {
321                                    let git_panel = git_panel_entity.clone();
322                                    move |window, cx| {
323                                        git_panel.update(cx, |git_panel, cx| {
324                                            git_panel.toggle_signoff_enabled(&Signoff, window, cx);
325                                        })
326                                    }
327                                },
328                            )
329                            .item(
330                                ContextMenuEntry::new("Skip Hooks")
331                                    .toggleable(IconPosition::Start, skip_hooks_enabled)
332                                    .action(Box::new(SkipHooks))
333                                    .handler(move |window, cx| {
334                                        window.dispatch_action(Box::new(SkipHooks), cx)
335                                    })
336                                    .documentation_aside(DocumentationSide::Left, |_| {
337                                        Label::new("git commit --no-verify").into_any_element()
338                                    }),
339                            )
340                    }))
341                }
342            })
343            .offset(gpui::Point {
344                x: px(0.),
345                y: px(2.),
346            })
347            .anchor(Anchor::TopRight)
348    }
349
350    pub fn render_footer(&self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
351        let (
352            can_commit,
353            tooltip,
354            commit_label,
355            co_authors,
356            generate_commit_message,
357            active_repo,
358            commit_options,
359            workspace,
360            is_generating,
361        ) = self.git_panel.update(cx, |git_panel, cx| {
362            let (can_commit, tooltip) = git_panel.configure_commit_button(cx);
363            let title = git_panel.commit_button_title();
364            let co_authors = git_panel.render_co_authors(cx);
365            let generate_commit_message = git_panel.render_generate_commit_message_button(cx);
366            let active_repo = git_panel.active_repository.clone();
367            let commit_options = git_panel.commit_options();
368            let is_generating = git_panel.is_generating_commit_message();
369            (
370                can_commit,
371                tooltip,
372                title,
373                co_authors,
374                generate_commit_message,
375                active_repo,
376                commit_options,
377                git_panel.workspace.clone(),
378                is_generating,
379            )
380        });
381
382        let branch = active_repo
383            .as_ref()
384            .and_then(|repo| repo.read(cx).branch.as_ref())
385            .map(|b| b.name().to_owned())
386            .unwrap_or_else(|| "<no branch>".to_owned());
387
388        let branch_picker_button = Button::new("branch_picker_button", branch)
389            .label_size(LabelSize::Small)
390            .start_icon(
391                Icon::new(IconName::GitBranch)
392                    .size(IconSize::Small)
393                    .color(Color::Placeholder),
394            )
395            .on_click(cx.listener(|_, _, window, cx| {
396                window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
397            }));
398
399        let branch_picker = PopoverMenu::new("popover-button")
400            .menu(move |window, cx| {
401                Some(branch_picker::popover(
402                    workspace.clone(),
403                    false,
404                    active_repo.clone(),
405                    window,
406                    cx,
407                ))
408            })
409            .with_handle(self.branch_list_handle.clone())
410            .trigger_with_tooltip(
411                branch_picker_button,
412                Tooltip::for_action_title("Switch Branch", &zed_actions::git::Branch),
413            )
414            .anchor(Anchor::BottomLeft)
415            .offset(gpui::Point {
416                x: px(0.0),
417                y: px(-2.0),
418            });
419
420        let focus_handle = self.focus_handle(cx);
421
422        let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, cx).map(|close_kb| {
423            KeybindingHint::new(close_kb, cx.theme().colors().editor_background).suffix("Cancel")
424        });
425
426        h_flex()
427            .group("commit_editor_footer")
428            .w_full()
429            .h(px(self.properties.footer_height))
430            .w_full()
431            .gap_1()
432            .flex_none()
433            .justify_between()
434            .child(
435                h_flex()
436                    .gap_1()
437                    .flex_shrink_1()
438                    .overflow_x_hidden()
439                    .child(
440                        h_flex()
441                            .flex_shrink_1()
442                            .overflow_x_hidden()
443                            .child(branch_picker),
444                    )
445                    .children(generate_commit_message)
446                    .children(co_authors),
447            )
448            .child(
449                h_flex()
450                    .gap_4()
451                    .child(close_kb_hint)
452                    .child(SplitButton::new(
453                        ButtonLike::new_rounded_left(format!("split-button-left-{}", commit_label))
454                            .layer(ElevationIndex::ModalSurface)
455                            .size(ButtonSize::Compact)
456                            .disabled(!can_commit)
457                            .child(Label::new(commit_label).size(LabelSize::Small).mr_0p5())
458                            .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
459                                telemetry::event!("Git Committed", source = "Git Modal");
460                                this.git_panel.update(cx, |git_panel, cx| {
461                                    let options = git_panel.commit_options();
462                                    git_panel.commit_changes(options, window, cx)
463                                });
464                                cx.emit(DismissEvent);
465                            }))
466                            .tooltip({
467                                let focus_handle = focus_handle.clone();
468                                move |_window, cx| {
469                                    if can_commit {
470                                        Tooltip::with_meta_in(
471                                            tooltip,
472                                            Some(&git::Commit),
473                                            format!(
474                                                "git commit{}{}{}",
475                                                if commit_options.amend { " --amend" } else { "" },
476                                                if commit_options.signoff {
477                                                    " --signoff"
478                                                } else {
479                                                    ""
480                                                },
481                                                if commit_options.no_verify {
482                                                    " --no-verify"
483                                                } else {
484                                                    ""
485                                                }
486                                            ),
487                                            &focus_handle.clone(),
488                                            cx,
489                                        )
490                                    } else {
491                                        Tooltip::simple(tooltip, cx)
492                                    }
493                                }
494                            }),
495                        self.render_git_commit_menu(
496                            format!("split-button-right-{}", commit_label),
497                            Some(focus_handle),
498                            is_generating,
499                        )
500                        .into_any_element(),
501                    )),
502            )
503    }
504
505    fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
506        if self.git_panel.read(cx).amend_pending() {
507            self.git_panel
508                .update(cx, |git_panel, cx| git_panel.set_amend_pending(false, cx));
509        } else {
510            cx.emit(DismissEvent);
511        }
512    }
513
514    fn on_commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
515        let is_amend = self.git_panel.read(cx).amend_pending();
516        let did_execute = self.git_panel.update(cx, |git_panel, cx| {
517            git_panel.commit(&self.commit_editor.focus_handle(cx), window, cx)
518        });
519        if did_execute {
520            if is_amend {
521                telemetry::event!("Git Amended", source = "Git Modal");
522            } else {
523                telemetry::event!("Git Committed", source = "Git Modal");
524            }
525            cx.emit(DismissEvent);
526        }
527    }
528
529    fn on_toggle_skip_hooks(
530        &mut self,
531        action: &git::SkipHooks,
532        window: &mut Window,
533        cx: &mut Context<Self>,
534    ) {
535        self.git_panel.update(cx, |git_panel, cx| {
536            git_panel.toggle_skip_hooks(action, window, cx)
537        });
538    }
539
540    fn on_amend(&mut self, _: &git::Amend, window: &mut Window, cx: &mut Context<Self>) {
541        if self.git_panel.update(cx, |git_panel, cx| {
542            git_panel.amend(&self.commit_editor.focus_handle(cx), window, cx)
543        }) {
544            telemetry::event!("Git Amended", source = "Git Modal");
545            cx.emit(DismissEvent);
546        }
547    }
548
549    fn toggle_branch_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
550        if self.branch_list_handle.is_focused(window, cx) {
551            self.focus_handle(cx).focus(window, cx)
552        } else {
553            self.branch_list_handle.toggle(window, cx);
554        }
555    }
556
557    fn increase_font_size(
558        &mut self,
559        action: &IncreaseBufferFontSize,
560        window: &mut Window,
561        cx: &mut Context<Self>,
562    ) {
563        self.git_panel.update(cx, |git_panel, cx| {
564            git_panel.increase_font_size(action, window, cx);
565        });
566    }
567
568    fn decrease_font_size(
569        &mut self,
570        action: &DecreaseBufferFontSize,
571        window: &mut Window,
572        cx: &mut Context<Self>,
573    ) {
574        self.git_panel.update(cx, |git_panel, cx| {
575            git_panel.decrease_font_size(action, window, cx);
576        });
577    }
578
579    fn reset_font_size(
580        &mut self,
581        action: &ResetBufferFontSize,
582        window: &mut Window,
583        cx: &mut Context<Self>,
584    ) {
585        self.git_panel.update(cx, |git_panel, cx| {
586            git_panel.reset_font_size(action, window, cx);
587        });
588    }
589}
590
591impl Render for CommitModal {
592    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
593        let properties = self.properties;
594        let width = px(properties.modal_width);
595        let container_padding = px(properties.container_padding);
596        let border_radius = properties.modal_border_radius;
597        let editor_focus_handle = self.commit_editor.focus_handle(cx);
598
599        let max_title_length = GitPanelSettings::get_global(cx).commit_title_max_length;
600        let title_exceeds_limit = if max_title_length > 0 {
601            self.commit_editor
602                .read(cx)
603                .text(cx)
604                .lines()
605                .next()
606                .is_some_and(|title| commit_title_exceeds_limit(title, max_title_length))
607        } else {
608            false
609        };
610
611        v_flex()
612            .id("commit-modal")
613            .key_context("GitCommit")
614            .on_action(cx.listener(Self::dismiss))
615            .on_action(cx.listener(Self::on_commit))
616            .on_action(cx.listener(Self::on_toggle_skip_hooks))
617            .on_action(cx.listener(Self::on_amend))
618            .on_action(cx.listener(Self::increase_font_size))
619            .on_action(cx.listener(Self::decrease_font_size))
620            .on_action(cx.listener(Self::reset_font_size))
621            .when(!DisableAiSettings::get_global(cx).disable_ai, |this| {
622                this.on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| {
623                    this.git_panel.update(cx, |panel, cx| {
624                        panel.generate_commit_message(cx);
625                    })
626                }))
627            })
628            .on_action(
629                cx.listener(|this, _: &zed_actions::git::Branch, window, cx| {
630                    this.toggle_branch_selector(window, cx);
631                }),
632            )
633            .on_action(
634                cx.listener(|this, _: &zed_actions::git::CheckoutBranch, window, cx| {
635                    this.toggle_branch_selector(window, cx);
636                }),
637            )
638            .on_action(
639                cx.listener(|this, _: &zed_actions::git::Switch, window, cx| {
640                    this.toggle_branch_selector(window, cx);
641                }),
642            )
643            .w(width)
644            .min_h_112()
645            .p(container_padding)
646            .elevation_3(cx)
647            .overflow_hidden()
648            .flex_none()
649            .relative()
650            .bg(cx.theme().colors().elevated_surface_background)
651            .rounded(px(border_radius))
652            .border_1()
653            .border_color(cx.theme().colors().border)
654            .child(
655                v_flex()
656                    .id("editor-container")
657                    .cursor_text()
658                    .p_2()
659                    .size_full()
660                    .gap_2()
661                    .justify_between()
662                    .rounded(properties.editor_border_radius())
663                    .overflow_hidden()
664                    .bg(cx.theme().colors().editor_background)
665                    .border_1()
666                    .border_color(if title_exceeds_limit {
667                        cx.theme().status().warning_border
668                    } else {
669                        cx.theme().colors().border_variant
670                    })
671                    .on_click(cx.listener(move |_, _: &ClickEvent, window, cx| {
672                        window.focus(&editor_focus_handle, cx);
673                    }))
674                    .child(self.render_commit_editor(window, cx))
675                    .when(title_exceeds_limit, |this| {
676                        this.child(
677                            h_flex()
678                                .absolute()
679                                .bottom_12()
680                                .w_full()
681                                .py_1()
682                                .px_2()
683                                .gap_1()
684                                .justify_center()
685                                .child(
686                                    Icon::new(IconName::Warning)
687                                        .size(IconSize::XSmall)
688                                        .color(Color::Warning),
689                                )
690                                .child(
691                                    Label::new(format!(
692                                        "Commit message title exceeds {max_title_length}-character limit."
693                                    ))
694                                    .size(LabelSize::Small),
695                                ),
696                        )
697                    })
698                    .child(self.render_footer(window, cx)),
699            )
700    }
701}
702
Served at tenant.openagents/omega Member data and write actions are omitted.