Detailed changes
@@ -1144,6 +1144,11 @@
//
// Default: false
"show_turn_stats": false,
+ // Whether to show the merge conflict indicator in the status bar
+ // that offers to resolve conflicts using the agent.
+ //
+ // Default: true
+ "show_merge_conflict_indicator": true,
},
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
@@ -595,6 +595,7 @@ mod tests {
message_editor_min_lines: 1,
tool_permissions,
show_turn_stats: false,
+ show_merge_conflict_indicator: true,
new_thread_location: Default::default(),
sidebar_side: Default::default(),
thinking_display: Default::default(),
@@ -176,6 +176,7 @@ pub struct AgentSettings {
pub use_modifier_to_send: bool,
pub message_editor_min_lines: usize,
pub show_turn_stats: bool,
+ pub show_merge_conflict_indicator: bool,
pub tool_permissions: ToolPermissions,
pub new_thread_location: NewThreadLocation,
}
@@ -629,6 +630,7 @@ impl Settings for AgentSettings {
use_modifier_to_send: agent.use_modifier_to_send.unwrap(),
message_editor_min_lines: agent.message_editor_min_lines.unwrap(),
show_turn_stats: agent.show_turn_stats.unwrap(),
+ show_merge_conflict_indicator: agent.show_merge_conflict_indicator.unwrap(),
tool_permissions: compile_tool_permissions(agent.tool_permissions),
new_thread_location: agent.new_thread_location.unwrap_or_default(),
}
@@ -734,6 +734,7 @@ mod tests {
message_editor_min_lines: 1,
tool_permissions: Default::default(),
show_turn_stats: false,
+ show_merge_conflict_indicator: true,
new_thread_location: Default::default(),
sidebar_side: Default::default(),
thinking_display: Default::default(),
@@ -816,13 +816,10 @@ impl ThreadView {
}
}
}));
- if self.parent_id.is_none() {
- self.suppress_merge_conflict_notification(cx);
- }
generation
}
- pub fn stop_turn(&mut self, generation: usize, cx: &mut Context<Self>) {
+ pub fn stop_turn(&mut self, generation: usize, _cx: &mut Context<Self>) {
if self.turn_fields.turn_generation != generation {
return;
}
@@ -833,25 +830,6 @@ impl ThreadView {
.map(|started| started.elapsed());
self.turn_fields.last_turn_tokens = self.turn_fields.turn_tokens.take();
self.turn_fields._turn_timer_task = None;
- if self.parent_id.is_none() {
- self.unsuppress_merge_conflict_notification(cx);
- }
- }
-
- fn suppress_merge_conflict_notification(&self, cx: &mut Context<Self>) {
- self.workspace
- .update(cx, |workspace, cx| {
- workspace.suppress_notification(&workspace::merge_conflict_notification_id(), cx);
- })
- .ok();
- }
-
- fn unsuppress_merge_conflict_notification(&self, cx: &mut Context<Self>) {
- self.workspace
- .update(cx, |workspace, _cx| {
- workspace.unsuppress(workspace::merge_conflict_notification_id());
- })
- .ok();
}
pub fn update_turn_tokens(&mut self, cx: &App) {
@@ -6,19 +6,19 @@ use editor::{
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
};
use gpui::{
- App, Context, DismissEvent, Entity, InteractiveElement as _, ParentElement as _, Subscription,
- Task, WeakEntity,
+ App, ClickEvent, Context, Empty, Entity, InteractiveElement as _, ParentElement as _,
+ Subscription, Task, WeakEntity,
};
use language::{Anchor, Buffer, BufferId};
use project::{
ConflictRegion, ConflictSet, ConflictSetUpdate, Project, ProjectItem as _,
- git_store::{GitStoreEvent, RepositoryEvent},
+ git_store::{GitStore, GitStoreEvent, RepositoryEvent},
};
use settings::Settings;
-use std::{cell::RefCell, ops::Range, rc::Rc, sync::Arc};
-use ui::{ActiveTheme, Divider, Element as _, Styled, Window, prelude::*};
+use std::{ops::Range, sync::Arc};
+use ui::{ButtonLike, Divider, Tooltip, prelude::*};
use util::{ResultExt as _, debug_panic, maybe};
-use workspace::{Workspace, notifications::simple_message_notification::MessageNotification};
+use workspace::{StatusItemView, Workspace, item::ItemHandle};
use zed_actions::agent::{
ConflictContent, ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent,
};
@@ -433,74 +433,6 @@ fn collect_conflicted_file_paths(project: &Project, cx: &App) -> Vec<String> {
paths
}
-pub(crate) fn register_conflict_notification(
- workspace: &mut Workspace,
- cx: &mut Context<Workspace>,
-) {
- let git_store = workspace.project().read(cx).git_store().clone();
-
- let last_shown_paths: Rc<RefCell<HashSet<String>>> = Rc::new(RefCell::new(HashSet::default()));
-
- cx.subscribe(&git_store, move |workspace, _git_store, event, cx| {
- let conflicts_changed = matches!(
- event,
- GitStoreEvent::ConflictsUpdated
- | GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::StatusesChanged, _)
- );
- if !AgentSettings::get_global(cx).enabled(cx) || !conflicts_changed {
- return;
- }
- let project = workspace.project().read(cx);
- if project.is_via_collab() {
- return;
- }
-
- if workspace.is_notification_suppressed(workspace::merge_conflict_notification_id()) {
- return;
- }
-
- let paths = collect_conflicted_file_paths(project, cx);
- let notification_id = workspace::merge_conflict_notification_id();
- let current_paths_set: HashSet<String> = paths.iter().cloned().collect();
-
- if paths.is_empty() {
- last_shown_paths.borrow_mut().clear();
- workspace.dismiss_notification(¬ification_id, cx);
- } else if *last_shown_paths.borrow() != current_paths_set {
- // Only show the notification if the set of conflicted paths has changed.
- // This prevents re-showing after the user dismisses it while working on the same conflicts.
- *last_shown_paths.borrow_mut() = current_paths_set;
- let file_count = paths.len();
- workspace.show_notification(notification_id, cx, |cx| {
- cx.new(|cx| {
- let message = format!(
- "{file_count} file{} have unresolved merge conflicts",
- if file_count == 1 { "" } else { "s" }
- );
-
- MessageNotification::new(message, cx)
- .primary_message("Resolve with Agent")
- .primary_icon(IconName::ZedAssistant)
- .primary_icon_color(Color::Muted)
- .primary_on_click({
- let paths = paths.clone();
- move |window, cx| {
- window.dispatch_action(
- Box::new(ResolveConflictedFilesWithAgent {
- conflicted_file_paths: paths.clone(),
- }),
- cx,
- );
- cx.emit(DismissEvent);
- }
- })
- })
- });
- }
- })
- .detach();
-}
-
pub(crate) fn resolve_conflict(
editor: WeakEntity<Editor>,
resolved_conflict: ConflictRegion,
@@ -573,3 +505,171 @@ pub(crate) fn resolve_conflict(
}
})
}
+
+pub struct MergeConflictIndicator {
+ project: Entity<Project>,
+ conflicted_paths: Vec<String>,
+ last_shown_paths: HashSet<String>,
+ dismissed: bool,
+ _subscription: Subscription,
+}
+
+impl MergeConflictIndicator {
+ pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
+ let project = workspace.project().clone();
+ let git_store = project.read(cx).git_store().clone();
+
+ let subscription = cx.subscribe(&git_store, Self::on_git_store_event);
+
+ let conflicted_paths = collect_conflicted_file_paths(project.read(cx), cx);
+ let last_shown_paths: HashSet<String> = conflicted_paths.iter().cloned().collect();
+
+ Self {
+ project,
+ conflicted_paths,
+ last_shown_paths,
+ dismissed: false,
+ _subscription: subscription,
+ }
+ }
+
+ fn on_git_store_event(
+ &mut self,
+ _git_store: Entity<GitStore>,
+ event: &GitStoreEvent,
+ cx: &mut Context<Self>,
+ ) {
+ let conflicts_changed = matches!(
+ event,
+ GitStoreEvent::ConflictsUpdated
+ | GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::StatusesChanged, _)
+ );
+
+ let agent_settings = AgentSettings::get_global(cx);
+ if !agent_settings.enabled(cx)
+ || !agent_settings.show_merge_conflict_indicator
+ || !conflicts_changed
+ {
+ return;
+ }
+
+ let project = self.project.read(cx);
+ if project.is_via_collab() {
+ return;
+ }
+
+ let paths = collect_conflicted_file_paths(project, cx);
+ let current_paths_set: HashSet<String> = paths.iter().cloned().collect();
+
+ if paths.is_empty() {
+ self.conflicted_paths.clear();
+ self.last_shown_paths.clear();
+ self.dismissed = false;
+ cx.notify();
+ } else if self.last_shown_paths != current_paths_set {
+ self.last_shown_paths = current_paths_set;
+ self.conflicted_paths = paths;
+ self.dismissed = false;
+ cx.notify();
+ }
+ }
+
+ fn resolve_with_agent(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ window.dispatch_action(
+ Box::new(ResolveConflictedFilesWithAgent {
+ conflicted_file_paths: self.conflicted_paths.clone(),
+ }),
+ cx,
+ );
+ self.dismissed = true;
+ cx.notify();
+ }
+
+ fn dismiss(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
+ self.dismissed = true;
+ cx.notify();
+ }
+}
+
+impl Render for MergeConflictIndicator {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let agent_settings = AgentSettings::get_global(cx);
+ if !agent_settings.enabled(cx)
+ || !agent_settings.show_merge_conflict_indicator
+ || self.conflicted_paths.is_empty()
+ || self.dismissed
+ {
+ return Empty.into_any_element();
+ }
+
+ let file_count = self.conflicted_paths.len();
+
+ let message: SharedString = format!(
+ "Resolve Merge Conflict{} with Agent",
+ if file_count == 1 { "" } else { "s" }
+ )
+ .into();
+
+ let tooltip_label: SharedString = format!(
+ "Found {} {} across the codebase",
+ file_count,
+ if file_count == 1 {
+ "conflict"
+ } else {
+ "conflicts"
+ }
+ )
+ .into();
+
+ let border_color = cx.theme().colors().text_accent.opacity(0.2);
+
+ h_flex()
+ .h(rems_from_px(22.))
+ .rounded_sm()
+ .border_1()
+ .border_color(border_color)
+ .child(
+ ButtonLike::new("update-button")
+ .child(
+ h_flex()
+ .h_full()
+ .gap_1()
+ .child(
+ Icon::new(IconName::GitMergeConflict)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new(message).size(LabelSize::Small)),
+ )
+ .tooltip(move |_, cx| {
+ Tooltip::with_meta(
+ tooltip_label.clone(),
+ None,
+ "Click to Resolve with Agent",
+ cx,
+ )
+ })
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.resolve_with_agent(window, cx);
+ })),
+ )
+ .child(
+ div().border_l_1().border_color(border_color).child(
+ IconButton::new("dismiss-merge-conflicts", IconName::Close)
+ .icon_size(IconSize::XSmall)
+ .on_click(cx.listener(Self::dismiss)),
+ ),
+ )
+ .into_any_element()
+ }
+}
+
+impl StatusItemView for MergeConflictIndicator {
+ fn set_active_pane_item(
+ &mut self,
+ _: Option<&dyn ItemHandle>,
+ _window: &mut Window,
+ _: &mut Context<Self>,
+ ) {
+ }
+}
@@ -47,6 +47,8 @@ pub mod stash_picker;
pub mod text_diff_view;
pub mod worktree_picker;
+pub use conflict_view::MergeConflictIndicator;
+
pub fn init(cx: &mut App) {
editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
commit_view::init(cx);
@@ -62,7 +64,6 @@ pub fn init(cx: &mut App) {
git_panel::register(workspace);
repository_selector::register(workspace);
git_picker::register(workspace);
- conflict_view::register_conflict_notification(workspace, cx);
let project = workspace.project().read(cx);
if project.is_read_only(cx) {
@@ -209,6 +209,11 @@ pub struct AgentSettingsContent {
///
/// Default: false
pub show_turn_stats: Option<bool>,
+ /// Whether to show the merge conflict indicator in the status bar
+ /// that offers to resolve conflicts using the agent.
+ ///
+ /// Default: true
+ pub show_merge_conflict_indicator: Option<bool>,
/// Per-tool permission rules for granular control over which tool actions
/// require confirmation.
///
@@ -7516,6 +7516,24 @@ fn ai_page(cx: &App) -> SettingsPage {
metadata: None,
files: USER,
}),
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "Show Merge Conflict Indicator",
+ description: "Whether to show the merge conflict indicator in the status bar that offers to resolve conflicts using the agent.",
+ field: Box::new(SettingField {
+ json_path: Some("agent.show_merge_conflict_indicator"),
+ pick: |settings_content| {
+ settings_content.agent.as_ref()?.show_merge_conflict_indicator.as_ref()
+ },
+ write: |settings_content, value| {
+ settings_content
+ .agent
+ .get_or_insert_default()
+ .show_merge_conflict_indicator = value;
+ },
+ }),
+ metadata: None,
+ files: USER,
+ }),
]);
items.into_boxed_slice()
@@ -7693,11 +7693,6 @@ impl GlobalAnyActiveCall {
}
}
-pub fn merge_conflict_notification_id() -> NotificationId {
- struct MergeConflictNotification;
- NotificationId::unique::<MergeConflictNotification>()
-}
-
/// Workspace-local view of a remote participant's location.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ParticipantLocation {
@@ -502,12 +502,15 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
let line_ending_indicator =
cx.new(|_| line_ending_selector::LineEndingIndicator::default());
+ let merge_conflict_indicator =
+ cx.new(|cx| git_ui::MergeConflictIndicator::new(workspace, cx));
workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_left_item(search_button, window, cx);
status_bar.add_left_item(lsp_button, window, cx);
status_bar.add_left_item(diagnostic_summary, window, cx);
status_bar.add_left_item(active_file_name, window, cx);
status_bar.add_left_item(activity_indicator, window, cx);
+ status_bar.add_left_item(merge_conflict_indicator, window, cx);
status_bar.add_right_item(edit_prediction_ui, window, cx);
status_bar.add_right_item(active_buffer_encoding, window, cx);
status_bar.add_right_item(active_buffer_language, window, cx);