Add disable_ai check for "Agent Panel" MenuItem (#63580)

# Objective

- Removes Agent Panel menu item when AI disabled toggle is on.
- Fixes #63497

## Solution

- Use the global DisableAiSettings - disable_ai check in `app_menus.rs`.
To keep the order the same I
1. Check the disable AI setting
2. If true push to the `view_items` vec![]
3.  Extend the vec![] with the remaining Menu Items and Separators. 

## Testing

This can be tested visually:
 1. Open Zed with AI settings enabled 
 2. Click View when Zed is focused
3. Noting where Agent Panel is on the list and to make sure clicking it
opens up the Agent Panel.
 4. Open settings 
 5. Navigate to AI settings
 6. Toggle disable AI on
 7. Click View 
 
Expected: Agent Panel is gone from the menu. The opposite is also
expected.
 
- This was tested on macOS 26.6.2 (25G83).

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [] Tests cover the new/changed behavior 
To keep this PR simple, I did not add any testing to this, as there were
no tests in this file already.
- [x] Performance impact has been considered and is acceptable

## Showcase
<details>
  <summary>Click to view showcase</summary>




https://github.com/user-attachments/assets/50db44ac-6c11-4101-a064-fd4e3b285620



</details>

---

Release Notes:

- Fixed Agent Panel appearing in the "View" menu when disable AI
settings is on.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
shxmbles 2026-09-04 17:19:04 +00:00 • committed by GitHub
parent c91e24a971
commit c8c07aea5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 414 additions and 19 deletions

1
Cargo.lock generated
View file

@ -18587,6 +18587,7 @@ dependencies = [
"smallvec",
"telemetry",
"theme",
"theme_settings",
"ui",
"util",
"workspace",

View file

@ -4,7 +4,7 @@ use crate::NoopTextSystem;
use crate::PathPromptOptions;
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
DummyKeyboardMapper, ForegroundExecutor, Keymap, Platform, PlatformDisplay,
DummyKeyboardMapper, ForegroundExecutor, Keymap, OwnedMenu, Platform, PlatformDisplay,
PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SharedString,
SourceMetadata, SystemNotification, SystemNotificationResponse, Task, TestDisplay, TestWindow,
@ -45,6 +45,7 @@ pub(crate) struct TestPlatform {
RefCell<Option<oneshot::Sender<(Option<PathBuf>, Vec<std::ffi::OsString>)>>>,
headless_renderer_factory: Option<Box<dyn Fn() -> Option<Box<dyn PlatformHeadlessRenderer>>>>,
weak: Weak<Self>,
menus: RefCell<Vec<OwnedMenu>>,
}
#[derive(Clone)]
@ -159,6 +160,7 @@ impl TestPlatform {
system_notifications: Default::default(),
text_system,
headless_renderer_factory,
menus: Default::default(),
})
}
@ -573,7 +575,14 @@ impl Platform for TestPlatform {
self.system_notifications.borrow_mut().response_callback = Some(callback);
}
fn set_menus(&self, _menus: Vec<crate::Menu>, _keymap: &Keymap) {}
fn set_menus(&self, menus: Vec<crate::Menu>, _keymap: &Keymap) {
*self.menus.borrow_mut() = menus.into_iter().map(|menu| menu.owned()).collect()
}
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
Some(self.menus.borrow().clone())
}
fn set_dock_menu(&self, _menu: Vec<crate::MenuItem>, _keymap: &Keymap) {}
fn add_recent_document(&self, _paths: &Path) {}

View file

@ -74,5 +74,6 @@ remote = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
semver.workspace = true
settings = { workspace = true, features = ["test-support"] }
theme_settings.workspace = true
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View file

@ -1,4 +1,5 @@
use gpui::{Action, Entity, OwnedMenu, OwnedMenuItem, Subscription, actions};
use project::DisableAiSettings;
use settings::{Settings, SettingsStore};
use workspace::AccessibleMode;
@ -52,22 +53,42 @@ pub struct ApplicationMenu {
impl ApplicationMenu {
pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
let menus = cx.get_menus().unwrap_or_default();
// Re-render when settings change so toggling "accessible mode" expands or
// collapses the menu bar (see `all_menus_shown`).
let settings_subscription = cx.observe_global::<SettingsStore>(|_, cx| cx.notify());
let entries = Self::build_entries(menus);
let mut disable_ai = DisableAiSettings::get_global(cx).disable_ai;
let settings_subscription =
cx.observe_global::<SettingsStore>(move |application_menu, cx| {
let new_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
if new_disable_ai != disable_ai {
disable_ai = new_disable_ai;
for entry in &application_menu.entries {
if entry.handle.is_deployed() {
entry.handle.hide(cx);
}
}
let menus = cx.get_menus().unwrap_or_default();
application_menu.entries = Self::build_entries(menus);
}
cx.notify();
});
Self {
entries: menus
.into_iter()
.map(|menu| MenuEntry {
menu,
handle: PopoverMenuHandle::default(),
})
.collect(),
entries,
pending_menu_open: None,
_settings_subscription: settings_subscription,
}
}
fn build_entries(menus: Vec<OwnedMenu>) -> SmallVec<[MenuEntry; 8]> {
menus
.into_iter()
.map(|menu| MenuEntry {
menu,
handle: PopoverMenuHandle::default(),
})
.collect()
}
fn sanitize_menu_items(items: Vec<OwnedMenuItem>) -> Vec<OwnedMenuItem> {
let mut cleaned = Vec::new();
let mut last_was_separator = false;
@ -343,3 +364,200 @@ impl Render for ApplicationMenu {
})
}
}
#[cfg(test)]
mod tests {
use gpui::{Action, Menu, MenuItem, OwnedMenu, OwnedMenuItem, TestAppContext, UpdateGlobal};
use project::DisableAiSettings;
use settings::{Settings, SettingsStore};
use crate::application_menu::{ApplicationMenu, MenuEntry, OpenApplicationMenu};
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
DisableAiSettings::register(cx);
theme_settings::init(theme::LoadThemes::JustBase, cx);
});
}
fn owned_menu(name: &str, item_name: &str) -> OwnedMenu {
OwnedMenu {
name: name.into(),
disabled: false,
items: vec![OwnedMenuItem::Action {
name: item_name.to_string(),
action: OpenApplicationMenu(String::new()).boxed_clone(),
os_action: None,
checked: false,
disabled: false,
}],
}
}
fn has_item(entries: &[MenuEntry], menu_name: &str, item_name: &str) -> bool {
entries.iter().any(|entry| {
entry.menu.name == menu_name
&& entry.menu.items.iter().any(
|item| matches!(item, OwnedMenuItem::Action { name, .. } if name == item_name),
)
})
}
fn menu_entry<'a>(entries: &'a [MenuEntry], menu_name: &str) -> &'a MenuEntry {
entries
.iter()
.find(|entry| entry.menu.name == menu_name)
.expect("expected menu")
}
fn view_menu(include_agent_panel: bool) -> Menu {
let mut items = Vec::new();
if include_agent_panel {
items.push(MenuItem::action(
"Agent Panel",
OpenApplicationMenu(String::new()),
));
}
items.push(MenuItem::action(
"Diagnostics",
OpenApplicationMenu(String::new()),
));
Menu {
name: "View".into(),
items,
disabled: false,
}
}
#[test]
fn test_build_entries_reflects_current_menu_data() {
let with_agent_panel =
ApplicationMenu::build_entries(vec![owned_menu("View", "Agent Panel")]);
assert!(has_item(&with_agent_panel, "View", "Agent Panel"));
let without_agent_panel =
ApplicationMenu::build_entries(vec![owned_menu("View", "Diagnostics")]);
assert!(!has_item(&without_agent_panel, "View", "Agent Panel"));
}
#[gpui::test]
fn test_entries_refresh_on_settings_change(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
cx.set_menus(vec![view_menu(true)]);
});
let (app_menu, cx) = cx.add_window_view(|window, cx| ApplicationMenu::new(window, cx));
let view_handle = cx.update(|window, cx| {
let view_handle = app_menu.read_with(cx, |app_menu, _| {
menu_entry(&app_menu.entries, "View").handle.clone()
});
view_handle.show(window, cx);
view_handle
});
assert!(view_handle.is_deployed());
assert!(app_menu.read_with(cx, |app_menu, _| has_item(
&app_menu.entries,
"View",
"Agent Panel"
)));
assert!(app_menu.read_with(cx, |app_menu, _| has_item(
&app_menu.entries,
"View",
"Diagnostics"
)));
cx.update(|_, cx| {
cx.set_menus(vec![view_menu(false)]);
DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
});
assert!(!view_handle.is_deployed());
assert!(
!app_menu.read_with(cx, |app_menu, _| has_item(
&app_menu.entries,
"View",
"Agent Panel"
)),
"expected Agent Panel is not in the View menu"
);
assert!(app_menu.read_with(cx, |app_menu, _| has_item(
&app_menu.entries,
"View",
"Diagnostics"
)));
cx.update(|_, cx| {
cx.set_menus(vec![view_menu(true)]);
DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
});
assert!(app_menu.read_with(cx, |app_menu, _| has_item(
&app_menu.entries,
"View",
"Agent Panel"
)));
assert!(app_menu.read_with(cx, |app_menu, _| has_item(
&app_menu.entries,
"View",
"Diagnostics"
)));
}
#[gpui::test]
fn test_entries_preserve_deployed_menu_on_unrelated_settings_change(cx: &mut TestAppContext) {
init_test(cx);
cx.update(|cx| {
cx.set_menus(vec![
Menu::new("Zed").items([MenuItem::action(
"Settings",
OpenApplicationMenu(String::new()),
)]),
view_menu(true),
]);
});
let (app_menu, cx) = cx.add_window_view(|window, cx| ApplicationMenu::new(window, cx));
let zed_handle = cx.update(|window, cx| {
let zed_handle = app_menu.read_with(cx, |app_menu, _| {
menu_entry(&app_menu.entries, "Zed").handle.clone()
});
zed_handle.show(window, cx);
zed_handle
});
cx.run_until_parked();
assert!(zed_handle.is_deployed());
cx.update(|window, cx| {
let view_handle = app_menu.read_with(cx, |app_menu, _| {
menu_entry(&app_menu.entries, "View").handle.clone()
});
zed_handle.hide(cx);
view_handle.show(window, cx);
});
cx.run_until_parked();
assert!(!zed_handle.is_deployed());
assert!(app_menu.read_with(cx, |app_menu, _| {
menu_entry(&app_menu.entries, "View").handle.is_deployed()
}));
cx.update(|_, cx| {
SettingsStore::update_global(cx, |_, _| {});
});
cx.run_until_parked();
assert!(app_menu.read_with(cx, |app_menu, _| {
menu_entry(&app_menu.entries, "View").handle.is_deployed()
}));
}
}

View file

@ -2143,6 +2143,10 @@ pub fn handle_keymap_file_changes(
let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
let new_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
if new_disable_ai != old_disable_ai {
reload_menus(cx);
}
if new_base_keymap != old_base_keymap
|| new_vim_enabled != old_vim_enabled
|| new_helix_enabled != old_helix_enabled
@ -2313,6 +2317,11 @@ fn show_markdown_app_notification<F>(
})
}
fn reload_menus(cx: &mut App) {
let menus = app_menus(cx);
cx.set_menus(menus);
}
fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
cx.clear_key_bindings();
load_default_keymap(cx);
@ -2321,9 +2330,7 @@ fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
key_binding.set_meta(KeybindSource::User.meta());
}
cx.bind_keys(filter_disabled_ai_bindings(user_key_bindings, cx));
let menus = app_menus(cx);
cx.set_menus(menus);
reload_menus(cx);
// On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
#[cfg(not(target_os = "windows"))]
cx.set_dock_menu(vec![gpui::MenuItem::action(
@ -2865,8 +2872,8 @@ mod tests {
use extension::ExtensionHostProxy;
use fs::FakeFs;
use gpui::{
Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, Modifiers, TestAppContext,
UpdateGlobal, VisualTestContext, WindowHandle, actions, point, px,
Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, Modifiers, OwnedMenuItem,
TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions, point, px,
};
use http_client::BlockedHttpClient;
use language::LanguageRegistry;
@ -8146,4 +8153,155 @@ mod tests {
window
}
#[gpui::test]
fn test_reload_keymaps_rebuilds_menus(cx: &mut TestAppContext) {
init_keymap_test(cx);
cx.update(|cx| reload_keymaps(cx, Vec::new()));
cx.update(|cx| {
assert!(
has_view_item(cx, "Agent Panel"),
"expected Agent Panel in the View menu when AI is enabled"
);
assert!(
has_view_item(cx, "Diagnostics"),
"expected Diagnostics to be in View menu"
);
});
cx.update(|cx| {
SettingsStore::update_global(cx, |settings_store, cx| {
settings_store.update_user_settings(cx, |settings| {
settings.project.disable_ai = Some(SaturatingBool(true))
});
})
});
cx.update(|cx| {
reload_keymaps(cx, Vec::new());
});
cx.update(|cx| {
assert!(
!has_view_item(cx, "Agent Panel"),
"expected Agent Panel to be removed from the View menu after disabling AI"
);
assert!(
has_view_item(cx, "Diagnostics"),
"expected Diagnostics to remain in the View menu"
);
});
cx.update(|cx| {
SettingsStore::update_global(cx, |settings_store, cx| {
settings_store.update_user_settings(cx, |settings| {
settings.project.disable_ai = Some(SaturatingBool(false));
});
});
});
cx.update(|cx| reload_keymaps(cx, Vec::new()));
cx.update(|cx| {
assert!(
has_view_item(cx, "Agent Panel"),
"expected Agent Panel back in the View menu after re-enabling AI"
);
assert!(
has_view_item(cx, "Diagnostics"),
"expected Diagnostics to remain in the View menu"
);
});
}
#[gpui::test]
async fn test_disable_ai_menu_update_with_malformed_keymap(cx: &mut TestAppContext) {
let executor = cx.executor();
let app_state = init_keymap_test(cx);
cx.update(|cx| reload_keymaps(cx, Vec::new()));
cx.update(|cx| {
assert!(
has_view_item(cx, "Agent Panel"),
"expected Agent Panel before disabling AI"
);
assert!(
has_view_item(cx, "Diagnostics"),
"expected Diagnostics in the View menu"
);
});
app_state
.fs
.save(
"/keymap.json".as_ref(),
&r#"!@!@this is not valid json"#.into(),
Default::default(),
)
.await
.expect("failed to save malformed keymap.json to the fake fs");
executor.run_until_parked();
cx.update(|cx| {
let (keymap_rx, keymap_watcher) = watch_config_file(
&executor,
app_state.fs.clone(),
PathBuf::from("/keymap.json"),
);
watch_settings_files(app_state.fs.clone(), cx);
handle_keymap_file_changes(keymap_rx, keymap_watcher, cx);
});
executor.run_until_parked();
app_state
.fs
.save(
paths::settings_file(),
&r#"{"disable_ai": true}"#.into(),
Default::default(),
)
.await
.expect("failed to save disable_ai=true to the fake settings file");
executor.run_until_parked();
cx.update(|cx| {
assert!(
!has_view_item(cx, "Agent Panel"),
"expected Agent Panel removed even though the keymap file is malformed"
);
assert!(
has_view_item(cx, "Diagnostics"),
"expected Diagnostics to remain in the View menu"
);
});
app_state
.fs
.save(
paths::settings_file(),
&r#"{"disable_ai": false}"#.into(),
Default::default(),
)
.await
.expect("failed to save disable_ai=false to the fake settings file");
executor.run_until_parked();
cx.update(|cx| {
assert!(
has_view_item(cx, "Agent Panel"),
"expected Agent Panel added back even though the keymap file is malformed"
);
assert!(
has_view_item(cx, "Diagnostics"),
"expected Diagnostics to remain in the View menu"
);
});
}
fn has_view_item(cx: &mut App, item_name: &str) -> bool {
cx.get_menus()
.expect("reload_keymaps should populate the menu bar")
.iter()
.find(|menu| menu.name == "View")
.expect("expected a View menu")
.items
.iter()
.any(|item| matches!(item, OwnedMenuItem::Action { name, .. } if name == item_name))
}
}

View file

@ -1,6 +1,8 @@
use collab_ui::collab_panel;
use gpui::{App, Menu, MenuItem, OsAction};
use project::DisableAiSettings;
use release_channel::ReleaseChannel;
use settings::Settings;
use terminal_view::terminal_panel;
use zed_actions::{Quit, assistant, debug_panel, dev, git_panel, project_panel};
@ -43,12 +45,18 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
MenuItem::action("Collab Panel", collab_panel::ToggleFocus),
MenuItem::action("Terminal Panel", terminal_panel::Toggle),
MenuItem::action("Debugger Panel", debug_panel::ToggleFocus),
MenuItem::action("Agent Panel", assistant::ToggleFocus),
];
if !DisableAiSettings::get_global(cx).disable_ai {
view_items.push(MenuItem::action("Agent Panel", assistant::ToggleFocus));
}
view_items.extend([
MenuItem::action("Git Panel", git_panel::ToggleFocus),
MenuItem::separator(),
MenuItem::action("Diagnostics", diagnostics::Deploy),
MenuItem::separator(),
];
]);
if ReleaseChannel::try_global(cx) == Some(ReleaseChannel::Dev) {
view_items.push(MenuItem::action(