agent: Block agent v2 feature on stable release channel (#53849)

Based on a partial revert of: 7bcdb12b4c

The main difference is instead of a feature flag we now check
`ReleaseChannel::Stable` != current_release_channel when the UI checks
if agent v2 (sidebar) features should be enabled.

Self-Review Checklist:

- [ ] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [ ] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [ ] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- N/A or Added/Fixed/Improved ...

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Anthony EidandMax Brunsfeld authored and GitHub committed 2026-04-13 16:50:13 -07:00
1 parent 092fedc42c
commit 2065099bc8
9 files changed
+231 -130

No files matched your search

Generated
+1 -1
View File
@@ -21659,7 +21659,6 @@ dependencies = [
"collections",
"component",
"db",
"feature_flags",
"fs",
"futures 0.3.32",
"git",
@@ -21675,6 +21674,7 @@ dependencies = [
"postage",
"pretty_assertions",
"project",
"release_channel",
"remote",
"schemars",
"serde",
+101 -5
View File
@@ -71,6 +71,7 @@ use project::git_store::{GitStoreEvent, RepositoryEvent};
use project::project_settings::ProjectSettings;
use project::{Project, ProjectPath, Worktree, WorktreePaths, linked_worktree_short_name};
use prompt_store::{PromptStore, UserPromptId};
use release_channel::ReleaseChannel;
use remote::RemoteConnectionOptions;
use rules_library::{RulesLibrary, open_rules_library};
use settings::TerminalDockPosition;
@@ -93,6 +94,10 @@ const AGENT_PANEL_KEY: &str = "agent_panel";
const MIN_PANEL_WIDTH: Pixels = px(300.);
const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
fn agent_v2_enabled(cx: &App) -> bool {
!matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable))
}
/// Maximum number of idle threads kept in the agent panel's retained list.
/// Set as a GPUI global to override; otherwise defaults to 5.
pub struct MaxIdleRetainedThreads(pub usize);
@@ -1006,10 +1011,10 @@ impl AgentPanel {
StartThreadIn::LocalProject => true,
StartThreadIn::NewWorktree { .. } => {
let project = panel.project.read(cx);
!project.is_via_collab()
agent_v2_enabled(cx) && !project.is_via_collab()
}
StartThreadIn::LinkedWorktree { path, .. } => {
path.exists()
agent_v2_enabled(cx) && path.exists()
}
};
if is_valid {
@@ -2547,6 +2552,9 @@ impl AgentPanel {
let new_target = match action {
StartThreadIn::LocalProject => StartThreadIn::LocalProject,
StartThreadIn::NewWorktree { .. } => {
if !agent_v2_enabled(cx) {
return;
}
if !self.project_has_git_repository(cx) {
log::error!(
"set_start_thread_in: cannot use worktree mode without a git repository"
@@ -2562,6 +2570,9 @@ impl AgentPanel {
action.clone()
}
StartThreadIn::LinkedWorktree { .. } => {
if !agent_v2_enabled(cx) {
return;
}
if !self.project_has_git_repository(cx) {
log::error!(
"set_start_thread_in: cannot use LinkedWorktree without a git repository"
@@ -2586,6 +2597,10 @@ impl AgentPanel {
}
fn cycle_start_thread_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !agent_v2_enabled(cx) {
return;
}
let next = match &self.start_thread_in {
StartThreadIn::LocalProject => StartThreadIn::NewWorktree {
worktree_name: None,
@@ -2600,6 +2615,15 @@ impl AgentPanel {
fn reset_start_thread_in_to_default(&mut self, cx: &mut Context<Self>) {
use settings::{NewThreadLocation, Settings};
if !agent_v2_enabled(cx) {
if self.start_thread_in != StartThreadIn::LocalProject {
self.start_thread_in = StartThreadIn::LocalProject;
self.serialize(cx);
cx.notify();
}
return;
}
let default = AgentSettings::get_global(cx).new_thread_location;
let start_thread_in = match default {
NewThreadLocation::LocalProject => StartThreadIn::LocalProject,
@@ -2622,6 +2646,15 @@ impl AgentPanel {
}
fn sync_start_thread_in_with_git_state(&mut self, cx: &mut Context<Self>) {
if !agent_v2_enabled(cx) {
if self.start_thread_in != StartThreadIn::LocalProject {
self.start_thread_in = StartThreadIn::LocalProject;
self.serialize(cx);
cx.notify();
}
return;
}
if matches!(self.start_thread_in, StartThreadIn::LocalProject) {
return;
}
@@ -4108,6 +4141,47 @@ impl AgentPanel {
})
}
fn render_recent_entries_menu(
&self,
icon: IconName,
corner: Corner,
cx: &mut Context<Self>,
) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
PopoverMenu::new("agent-nav-menu")
.trigger_with_tooltip(
IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
{
move |_window, cx| {
Tooltip::for_action_in(
"Toggle Recently Updated Threads",
&ToggleNavigationMenu,
&focus_handle,
cx,
)
}
},
)
.anchor(corner)
.with_handle(self.agent_navigation_menu_handle.clone())
.menu({
let menu = self.agent_navigation_menu.clone();
move |window, cx| {
telemetry::event!("View Thread History Clicked");
if let Some(menu) = menu.as_ref() {
menu.update(cx, |_, cx| {
cx.defer_in(window, |menu, window, cx| {
menu.rebuild(window, cx);
});
})
}
menu.clone()
}
})
}
fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
@@ -4493,6 +4567,8 @@ impl AgentPanel {
selected_agent.into_any_element()
};
let show_history_menu = self.has_history_for_selected_agent(cx);
let agent_v2_enabled = agent_v2_enabled(cx);
let is_empty_state = !self.active_thread_has_messages(cx);
let is_in_history_or_config = self.is_history_or_configuration_visible();
@@ -4514,7 +4590,7 @@ impl AgentPanel {
}))
};
let use_v2_empty_toolbar = is_empty_state && !is_in_history_or_config;
let use_v2_empty_toolbar = agent_v2_enabled && is_empty_state && !is_in_history_or_config;
let max_content_width = AgentSettings::get_global(cx).max_content_width;
@@ -4584,11 +4660,17 @@ impl AgentPanel {
.pl(DynamicSpacing::Base04.rems(cx))
.child(agent_selector_menu)
.when(
has_visible_worktrees && self.project_has_git_repository(cx),
agent_v2_enabled
&& has_visible_worktrees
&& self.project_has_git_repository(cx),
|this| this.child(self.render_start_thread_in_selector(cx)),
)
.when(
matches!(self.start_thread_in, StartThreadIn::NewWorktree { .. }),
agent_v2_enabled
&& matches!(
self.start_thread_in,
StartThreadIn::NewWorktree { .. }
),
|this| this.child(self.render_new_worktree_branch_selector(cx)),
),
)
@@ -4599,6 +4681,13 @@ impl AgentPanel {
.gap_1()
.pl_1()
.pr_1()
.when(show_history_menu && !agent_v2_enabled, |this| {
this.child(self.render_recent_entries_menu(
IconName::MenuAltTemp,
Corner::TopRight,
cx,
))
})
.child(full_screen_button)
.child(self.render_panel_options_menu(window, cx)),
)
@@ -4644,6 +4733,13 @@ impl AgentPanel {
.pl_1()
.pr_1()
.child(new_thread_menu)
.when(show_history_menu && !agent_v2_enabled, |this| {
this.child(self.render_recent_entries_menu(
IconName::MenuAltTemp,
Corner::TopRight,
cx,
))
})
.child(full_screen_button)
.child(self.render_panel_options_menu(window, cx)),
)
+37 -4
View File
@@ -48,7 +48,7 @@ use agent_settings::{AgentProfileId, AgentSettings};
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use gpui::{Action, App, Context, Entity, SharedString, Window, actions};
use gpui::{Action, App, Context, Entity, SharedString, UpdateGlobal as _, Window, actions};
use language::{
LanguageRegistry,
language_settings::{AllLanguageSettings, EditPredictionProvider},
@@ -58,9 +58,10 @@ use language_model::{
};
use project::{AgentId, DisableAiSettings};
use prompt_store::PromptBuilder;
use release_channel::ReleaseChannel;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{LanguageModelSelection, Settings as _, SettingsStore};
use settings::{DockPosition, DockSide, LanguageModelSelection, Settings as _, SettingsStore};
use std::any::TypeId;
use workspace::Workspace;
@@ -514,7 +515,34 @@ pub fn init(
})
.detach();
maybe_backfill_editor_layout(fs, is_new_install, cx);
let agent_v2_enabled = agent_v2_enabled(cx);
if agent_v2_enabled {
maybe_backfill_editor_layout(fs, is_new_install, cx);
}
SettingsStore::update_global(cx, |store, cx| {
store.update_default_settings(cx, |defaults| {
if agent_v2_enabled {
defaults.agent.get_or_insert_default().dock = Some(DockPosition::Left);
defaults.project_panel.get_or_insert_default().dock = Some(DockSide::Right);
defaults.outline_panel.get_or_insert_default().dock = Some(DockSide::Right);
defaults.collaboration_panel.get_or_insert_default().dock =
Some(DockPosition::Right);
defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Right);
} else {
defaults.agent.get_or_insert_default().dock = Some(DockPosition::Right);
defaults.project_panel.get_or_insert_default().dock = Some(DockSide::Left);
defaults.outline_panel.get_or_insert_default().dock = Some(DockSide::Left);
defaults.collaboration_panel.get_or_insert_default().dock =
Some(DockPosition::Left);
defaults.git_panel.get_or_insert_default().dock = Some(DockPosition::Left);
}
});
});
}
fn agent_v2_enabled(cx: &App) -> bool {
!matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable))
}
fn maybe_backfill_editor_layout(fs: Arc<dyn Fs>, is_new_install: bool, cx: &mut App) {
@@ -542,6 +570,7 @@ fn maybe_backfill_editor_layout(fs: Arc<dyn Fs>, is_new_install: bool, cx: &mut
fn update_command_palette_filter(cx: &mut App) {
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
let agent_enabled = AgentSettings::get_global(cx).enabled;
let agent_v2_enabled = agent_v2_enabled(cx);
let edit_prediction_provider = AllLanguageSettings::get_global(cx)
.edit_predictions
@@ -611,7 +640,11 @@ fn update_command_palette_filter(cx: &mut App) {
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
}
filter.show_namespace("multi_workspace");
if agent_v2_enabled {
filter.show_namespace("multi_workspace");
} else {
filter.hide_namespace("multi_workspace");
}
});
}
+51 -50
View File
@@ -196,59 +196,60 @@ impl Dismissable for ParallelAgentAnnouncement {
}
fn announcement_for_version(version: &Version, cx: &App) -> Option<AnnouncementContent> {
match (version.major, version.minor, version.patch) {
(0, _, _) => {
if ParallelAgentAnnouncement::dismissed(cx) {
None
} else {
let fs = <dyn Fs>::global(cx);
Some(AnnouncementContent {
heading: "Introducing Parallel Agents".into(),
description: "Run multiple agent threads simultaneously across projects."
.into(),
bullet_items: vec![
"Use your favorite agents in parallel".into(),
"Optionally isolate agents using worktrees".into(),
"Combine multiple projects in one window".into(),
],
primary_action_label: "Try Now".into(),
primary_action_url: None,
primary_action_callback: Some(Arc::new(move |window, cx| {
let already_agent_layout =
matches!(AgentSettings::get_layout(cx), WindowLayout::Agent(_));
let version_with_parallel_agents = match ReleaseChannel::global(cx) {
ReleaseChannel::Stable => Version::new(0, 233, 0),
ReleaseChannel::Dev | ReleaseChannel::Nightly | ReleaseChannel::Preview => {
Version::new(0, 232, 0)
}
};
let update;
if !already_agent_layout {
update = Some(AgentSettings::set_layout(
WindowLayout::Agent(None),
fs.clone(),
cx,
));
} else {
update = None;
if *version >= version_with_parallel_agents && !ParallelAgentAnnouncement::dismissed(cx) {
let fs = <dyn Fs>::global(cx);
Some(AnnouncementContent {
heading: "Introducing Parallel Agents".into(),
description: "Run multiple agent threads simultaneously across projects.".into(),
bullet_items: vec![
"Use your favorite agents in parallel".into(),
"Optionally isolate agents using worktrees".into(),
"Combine multiple projects in one window".into(),
],
primary_action_label: "Try Now".into(),
primary_action_url: None,
primary_action_callback: Some(Arc::new(move |window, cx| {
let already_agent_layout =
matches!(AgentSettings::get_layout(cx), WindowLayout::Agent(_));
let update;
if !already_agent_layout {
update = Some(AgentSettings::set_layout(
WindowLayout::Agent(None),
fs.clone(),
cx,
));
} else {
update = None;
}
window
.spawn(cx, async move |cx| {
if let Some(update) = update {
update.await.ok();
}
window
.spawn(cx, async move |cx| {
if let Some(update) = update {
update.await.ok();
}
cx.update(|window, cx| {
window.dispatch_action(Box::new(FocusWorkspaceSidebar), cx);
window.dispatch_action(Box::new(FocusAgent), cx);
})
})
.detach();
})),
on_dismiss: Some(Arc::new(|cx| {
ParallelAgentAnnouncement::set_dismissed(true, cx)
})),
secondary_action_url: Some("https://zed.dev/blog/".into()),
})
}
}
_ => None,
cx.update(|window, cx| {
window.dispatch_action(Box::new(FocusWorkspaceSidebar), cx);
window.dispatch_action(Box::new(FocusAgent), cx);
})
})
.detach();
})),
on_dismiss: Some(Arc::new(|cx| {
ParallelAgentAnnouncement::set_dismissed(true, cx)
})),
secondary_action_url: Some("https://zed.dev/blog/".into()),
})
} else {
None
}
}
+10
View File
@@ -12,6 +12,16 @@ impl FeatureFlag for PanicFeatureFlag {
const NAME: &'static str = "panic";
}
pub struct AgentV2FeatureFlag;
impl FeatureFlag for AgentV2FeatureFlag {
const NAME: &'static str = "agent-v2";
fn enabled_for_staff() -> bool {
true
}
}
/// A feature flag for granting access to beta ACP features.
///
/// We reuse this feature flag for new betas, so don't delete it if it is not currently in use.
+26 -23
View File
@@ -1,5 +1,6 @@
use gpui::{Action as _, App};
use itertools::Itertools as _;
use release_channel::ReleaseChannel;
use settings::{
AudioInputDeviceName, AudioOutputDeviceName, LanguageSettingsContent, SemanticTokens,
SettingsContent,
@@ -7281,7 +7282,7 @@ fn ai_page(cx: &App) -> SettingsPage {
]
}
fn agent_configuration_section(_cx: &App) -> Box<[SettingsPageItem]> {
fn agent_configuration_section(cx: &App) -> Box<[SettingsPageItem]> {
let mut items = vec![
SettingsPageItem::SectionHeader("Agent Configuration"),
SettingsPageItem::SubPageLink(SubPageLink {
@@ -7295,28 +7296,30 @@ fn ai_page(cx: &App) -> SettingsPage {
}),
];
items.push(SettingsPageItem::SettingItem(SettingItem {
title: "New Thread Location",
description: "Whether to start a new thread in the current local project or in a new Git worktree.",
field: Box::new(SettingField {
json_path: Some("agent.new_thread_location"),
pick: |settings_content| {
settings_content
.agent
.as_ref()?
.new_thread_location
.as_ref()
},
write: |settings_content, value| {
settings_content
.agent
.get_or_insert_default()
.new_thread_location = value;
},
}),
metadata: None,
files: USER,
}));
if !matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable)) {
items.push(SettingsPageItem::SettingItem(SettingItem {
title: "New Thread Location",
description: "Whether to start a new thread in the current local project or in a new Git worktree.",
field: Box::new(SettingField {
json_path: Some("agent.new_thread_location"),
pick: |settings_content| {
settings_content
.agent
.as_ref()?
.new_thread_location
.as_ref()
},
write: |settings_content, value| {
settings_content
.agent
.get_or_insert_default()
.new_thread_location = value;
},
}),
metadata: None,
files: USER,
}));
}
items.extend([
SettingsPageItem::SettingItem(SettingItem {
+1 -1
View File
@@ -36,7 +36,6 @@ clock.workspace = true
collections.workspace = true
component.workspace = true
db.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
@@ -51,6 +50,7 @@ node_runtime.workspace = true
parking_lot.workspace = true
postage.workspace = true
project.workspace = true
release_channel.workspace = true
remote.workspace = true
schemars.workspace = true
serde.workspace = true
+3 -1
View File
@@ -7,6 +7,7 @@ use gpui::{
};
pub use project::ProjectGroupKey;
use project::{DirectoryLister, DisableAiSettings, Project};
use release_channel::ReleaseChannel;
use remote::RemoteConnectionOptions;
use settings::Settings;
pub use settings::SidebarSide;
@@ -397,7 +398,8 @@ impl MultiWorkspace {
}
pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
!DisableAiSettings::get_global(cx).disable_ai
!matches!(ReleaseChannel::try_global(cx), Some(ReleaseChannel::Stable))
&& !DisableAiSettings::get_global(cx).disable_ai
}
pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+1 -45
View File
@@ -2507,7 +2507,7 @@ mod tests {
read_multi_workspace_state,
},
};
use feature_flags::FeatureFlagAppExt;
use gpui::AppContext as _;
use pretty_assertions::assert_eq;
use project::Project;
@@ -2527,10 +2527,6 @@ mod tests {
async fn test_multi_workspace_serializes_on_add_and_remove(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
});
let fs = fs::FakeFs::new(cx.executor());
let project1 = Project::test(fs.clone(), [], cx).await;
let project2 = Project::test(fs.clone(), [], cx).await;
@@ -4088,10 +4084,6 @@ mod tests {
async fn test_flush_serialization_completes_before_quit(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
});
let fs = fs::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
@@ -4132,10 +4124,6 @@ mod tests {
async fn test_create_workspace_serialization(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
});
let fs = fs::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
@@ -4188,10 +4176,6 @@ mod tests {
async fn test_remove_workspace_clears_session_binding(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
});
let fs = fs::FakeFs::new(cx.executor());
let dir = unique_test_dir(&fs, "remove").await;
let project1 = Project::test(fs.clone(), [], cx).await;
@@ -4279,10 +4263,6 @@ mod tests {
async fn test_remove_workspace_not_restored_as_zombie(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
});
let fs = fs::FakeFs::new(cx.executor());
let dir1 = tempfile::TempDir::with_prefix("zombie_test1").unwrap();
let dir2 = tempfile::TempDir::with_prefix("zombie_test2").unwrap();
@@ -4385,10 +4365,6 @@ mod tests {
async fn test_pending_removal_tasks_drained_on_flush(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
});
let fs = fs::FakeFs::new(cx.executor());
let dir = unique_test_dir(&fs, "pending-removal").await;
let project1 = Project::test(fs.clone(), [], cx).await;
@@ -4489,10 +4465,6 @@ mod tests {
async fn test_create_workspace_bounds_observer_uses_fresh_id(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
});
let fs = fs::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [], cx).await;
@@ -4545,10 +4517,6 @@ mod tests {
async fn test_flush_serialization_writes_bounds(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
});
let fs = fs::FakeFs::new(cx.executor());
let dir = tempfile::TempDir::with_prefix("flush_bounds_test").unwrap();
fs.insert_tree(dir.path(), json!({})).await;
@@ -4704,10 +4672,6 @@ mod tests {
) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
});
let fs = fs::FakeFs::new(cx.executor());
// Main git repo at /repo
@@ -4887,10 +4851,6 @@ mod tests {
#[gpui::test]
async fn test_remove_project_group_falls_back_to_neighbor(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
cx.update_flags(true, vec!["agent-v2".to_string()]);
});
let fs = fs::FakeFs::new(cx.executor());
let dir_a = unique_test_dir(&fs, "group-a").await;
@@ -5002,10 +4962,6 @@ mod tests {
#[gpui::test]
async fn test_remove_fallback_skips_excluded_workspaces(cx: &mut gpui::TestAppContext) {
crate::tests::init_test(cx);
cx.update(|cx| {
cx.set_staff(true);
cx.update_flags(true, vec!["agent-v2".to_string()]);
});
let fs = fs::FakeFs::new(cx.executor());
let dir = unique_test_dir(&fs, "shared").await;