Add Actions to open AGENTS.md (#57847)

<img width="620" height="172" alt="Screenshot 2026-05-27 at 12 08 26 PM"
src="https://github.com/user-attachments/assets/226b3d0c-003b-44ac-a16f-10af4f2952b3"
/>


Add command palette actions for opening global and project-specific
AGENTS.md files

Closes AI-324

Release Notes:

- Added commands to open global and project-specific AGENTS.md rules
This commit is contained in:
Richard Feldman 2026-05-27 12:08:52 -04:00 committed by GitHub
parent 32d0737318
commit bc6a483e5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 149 additions and 45 deletions

View file

@ -29,7 +29,8 @@ use zed_actions::{
ResolveConflictsWithAgent, ReviewBranchDiff,
},
assistant::{
CreateSkillFromUrl, FocusAgent, OpenRulesLibrary, OpenSkillCreator, Toggle, ToggleFocus,
CreateSkillFromUrl, FocusAgent, OpenGlobalAgentsMdRules, OpenProjectAgentsMdRules,
OpenRulesLibrary, OpenSkillCreator, Toggle, ToggleFocus,
},
};
@ -179,6 +180,60 @@ fn read_global_last_created_entry_kind(kvp: &KeyValueStore) -> Option<AgentPanel
.map(|entry| entry.entry_kind)
}
fn project_agents_md_path(
project: &Entity<Project>,
require_existing_file: bool,
cx: &App,
) -> Option<PathBuf> {
let rel_path = util::rel_path::RelPath::unix("AGENTS.md").ok()?;
project
.read(cx)
.visible_worktrees(cx)
.next()
.and_then(|worktree| {
let worktree = worktree.read(cx);
if require_existing_file {
let entry = worktree.entry_for_path(rel_path)?;
if !entry.is_file() {
return None;
}
}
Some(worktree.absolutize(rel_path))
})
}
fn open_global_rules(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
workspace
.open_abs_path(
paths::agents_file().clone(),
workspace::OpenOptions {
focus: Some(true),
..Default::default()
},
window,
cx,
)
.detach_and_log_err(cx);
}
fn open_project_rules(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
if let Some(path) = project_agents_md_path(workspace.project(), false, cx) {
workspace
.open_abs_path(
path,
workspace::OpenOptions {
focus: Some(true),
..Default::default()
},
window,
cx,
)
.detach_and_log_err(cx);
}
}
async fn write_global_last_created_entry_kind(kvp: KeyValueStore, entry_kind: AgentPanelEntryKind) {
if let Some(json) = serde_json::to_string(&LastCreatedEntryKind { entry_kind }).log_err() {
kvp.write_kvp(LAST_CREATED_ENTRY_KIND_KEY.to_string(), json)
@ -315,6 +370,12 @@ pub fn init(cx: &mut App) {
});
}
})
.register_action(|workspace, _: &OpenGlobalAgentsMdRules, window, cx| {
open_global_rules(workspace, window, cx);
})
.register_action(|workspace, _: &OpenProjectAgentsMdRules, window, cx| {
open_project_rules(workspace, window, cx);
})
.register_action(|workspace, action: &OpenSkillCreator, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@ -4862,21 +4923,7 @@ impl AgentPanel {
.active_conversation_view()
.is_some_and(|conversation_view| conversation_view.read(cx).supports_logout());
let project_agents_md_path: Option<PathBuf> = self
.project
.read(cx)
.visible_worktrees(cx)
.next()
.and_then(|worktree| {
let worktree = worktree.read(cx);
let rel_path = util::rel_path::RelPath::unix("AGENTS.md").ok()?;
let entry = worktree.entry_for_path(rel_path)?;
if entry.is_file() {
Some(worktree.absolutize(rel_path))
} else {
None
}
});
let project_agents_md_path = project_agents_md_path(&self.project, true, cx);
let global_agents_md_loaded = UserAgentsMd::global(cx)
.and_then(|md| md.content())
@ -4976,24 +5023,14 @@ impl AgentPanel {
move |window, cx| {
workspace
.update(cx, |workspace, cx| {
workspace
.open_abs_path(
paths::agents_file().clone(),
workspace::OpenOptions {
focus: Some(true),
..Default::default()
},
window,
cx,
)
.detach_and_log_err(cx);
open_global_rules(workspace, window, cx);
})
.log_err();
},
);
}
if let Some(path) = project_agents_md_path.clone() {
if project_agents_md_path.is_some() {
let workspace = workspace.clone();
menu = menu.custom_entry(
|_window, _cx| {
@ -5009,20 +5046,9 @@ impl AgentPanel {
.into_any_element()
},
move |window, cx| {
let path = path.clone();
workspace
.update(cx, |workspace, cx| {
workspace
.open_abs_path(
path,
workspace::OpenOptions {
focus: Some(true),
..Default::default()
},
window,
cx,
)
.detach_and_log_err(cx);
open_project_rules(workspace, window, cx);
})
.log_err();
},

View file

@ -909,6 +909,14 @@ mod tests {
!filter.is_hidden(&zed_actions::assistant::CreateSkillFromUrl),
"CreateSkillFromUrl should be visible by default"
);
assert!(
!filter.is_hidden(&zed_actions::assistant::OpenGlobalAgentsMdRules),
"OpenGlobalAgentsMdRules should be visible by default"
);
assert!(
!filter.is_hidden(&zed_actions::assistant::OpenProjectAgentsMdRules),
"OpenProjectAgentsMdRules should be visible by default"
);
});
// Disable agent
@ -932,6 +940,14 @@ mod tests {
filter.is_hidden(&NewTerminalThread),
"NewTerminalThread should be hidden when agent is disabled"
);
assert!(
filter.is_hidden(&zed_actions::assistant::OpenGlobalAgentsMdRules),
"OpenGlobalAgentsMdRules should be hidden when agent is disabled"
);
assert!(
filter.is_hidden(&zed_actions::assistant::OpenProjectAgentsMdRules),
"OpenProjectAgentsMdRules should be hidden when agent is disabled"
);
});
// Test EditPredictionProvider

View file

@ -695,26 +695,69 @@ impl PickerDelegate for CommandPaletteDelegate {
}
pub fn humanize_action_name(name: &str) -> String {
let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count();
let chars = name.chars().collect::<Vec<_>>();
let capacity = name.len() + chars.iter().filter(|c| c.is_uppercase()).count();
let mut result = String::with_capacity(capacity);
for char in name.chars() {
let mut index = 0;
while index < chars.len() {
let char = chars[index];
if char == ':' {
if result.ends_with(':') {
result.push(' ');
} else {
result.push(':');
}
index += 1;
} else if char == '_' {
result.push(' ');
index += 1;
} else if char.is_uppercase() {
if !result.ends_with(' ') {
result.push(' ');
let start = index;
index += 1;
while chars
.get(index)
.is_some_and(|next_char| next_char.is_uppercase())
{
index += 1;
}
let uppercase_run = &chars[start..index];
if uppercase_run.len() > 1 {
let split_before_last = chars
.get(index)
.is_some_and(|next_char| next_char.is_lowercase());
let acronym_end = if split_before_last {
uppercase_run.len() - 1
} else {
uppercase_run.len()
};
if acronym_end > 0 {
if !result.ends_with(' ') {
result.push(' ');
}
result.extend(&uppercase_run[..acronym_end]);
}
if split_before_last {
if !result.ends_with(' ') {
result.push(' ');
}
result.extend(uppercase_run[acronym_end].to_lowercase());
}
} else {
if !result.ends_with(' ') {
result.push(' ');
}
result.extend(char.to_lowercase());
}
result.extend(char.to_lowercase());
} else {
result.push(char);
index += 1;
}
}
result
}
@ -753,6 +796,19 @@ mod tests {
humanize_action_name("go_to_line::Deploy"),
"go to line: deploy"
);
assert_eq!(
humanize_action_name("agent::OpenGlobalAGENTS.mdRules"),
"agent: open global AGENTS.md rules"
);
assert_eq!(
humanize_action_name("agent::OpenProjectAGENTS.mdRules"),
"agent: open project AGENTS.md rules"
);
assert_eq!(humanize_action_name("editor::OpenURL"), "editor: open URL");
assert_eq!(
humanize_action_name("editor::OpenURLParser"),
"editor: open URL parser"
);
}
#[test]

View file

@ -580,6 +580,12 @@ pub mod assistant {
OpenSkillCreator,
/// Opens the skill creator window to import a skill from a GitHub URL.
CreateSkillFromUrl,
/// Opens the user-global AGENTS.md rules file.
#[action(name = "OpenGlobalAGENTS.mdRules")]
OpenGlobalAgentsMdRules,
/// Opens the project AGENTS.md rules file.
#[action(name = "OpenProjectAGENTS.mdRules")]
OpenProjectAgentsMdRules,
]
);