Detailed changes
@@ -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",
@@ -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)),
)
@@ -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");
+ }
});
}
@@ -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 update;
- if !already_agent_layout {
- update = Some(AgentSettings::set_layout(
- WindowLayout::Agent(None),
- fs.clone(),
- cx,
- ));
- } else {
- update = None;
- }
+ 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)
+ }
+ };
- window
- .spawn(cx, async move |cx| {
- if let Some(update) = update {
- update.await.ok();
- }
+ 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;
+ }
- 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,
+ 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()),
+ })
+ } else {
+ None
}
}
@@ -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.
@@ -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 {
@@ -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
@@ -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>) {
@@ -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;