Skip to repository content702 lines · 26.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:27:28.012Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
commit_modal.rs
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