feat: reserve built-in agent slash commands

This commit is contained in:
ParthSareen
2026-07-22 13:44:49 -07:00
parent 2b9c4999a8
commit 77b1e63a88
5 changed files with 103 additions and 0 deletions

View File

@@ -325,6 +325,31 @@ func (c *SkillCatalog) Diagnostics() []error {
return append([]error(nil), c.diagnostics...)
}
// ExcludeNames removes skills whose names are reserved by a caller. It returns
// the excluded names in sorted order.
func (c *SkillCatalog) ExcludeNames(names []string) []string {
if c == nil {
return nil
}
reserved := make(map[string]struct{}, len(names))
for _, name := range names {
name = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(name)), "/")
if name != "" {
reserved[name] = struct{}{}
}
}
var excluded []string
for name := range c.skills {
if _, ok := reserved[name]; !ok {
continue
}
delete(c.skills, name)
excluded = append(excluded, name)
}
sort.Strings(excluded)
return excluded
}
func (c *SkillCatalog) Load(name string) (Skill, error) {
name = strings.TrimSpace(name)
if !skillName.MatchString(name) {

View File

@@ -257,6 +257,30 @@ func TestLoadDefaultSkillsPrecedenceAndCollisions(t *testing.T) {
}
}
func TestSkillCatalogExcludeNames(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{"release-notes", "system", "exit"} {
writeCatalogSkill(t, dir, name, "instructions")
}
catalog, err := DiscoverSkills(dir)
if err != nil {
t.Fatal(err)
}
if got, want := strings.Join(catalog.ExcludeNames([]string{"/system", "EXIT"}), ","), "exit,system"; got != want {
t.Fatalf("excluded skills = %q, want %q", got, want)
}
if _, err := catalog.Load("system"); err == nil {
t.Fatal("excluded system skill should not load")
}
if _, err := catalog.Load("exit"); err == nil {
t.Fatal("excluded exit skill should not load")
}
if _, err := catalog.Load("release-notes"); err != nil {
t.Fatalf("non-conflicting skill should remain available: %v", err)
}
}
func TestSkillContentListsDirectoryAndResources(t *testing.T) {
root := t.TempDir()
skillDir := filepath.Join(root, "pdf-processing")

View File

@@ -87,6 +87,9 @@ func GenerateAgentTUI(cmd *cobra.Command, client *api.Client, opts agentTUIOptio
if err != nil {
return fmt.Errorf("load agent skills: %w", err)
}
if ignored := skillCatalog.ExcludeNames(agentchat.BuiltinSlashCommandNames()); len(ignored) > 0 {
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m ignoring agent skill(s): %s\n", strings.Join(ignored, ", "))
}
for _, diagnostic := range skillCatalog.Diagnostics() {
fmt.Fprintf(os.Stderr, "\033[1mwarning:\033[0m ignored invalid agent skill: %v\n", diagnostic)
}

View File

@@ -95,6 +95,39 @@ func TestAgentSkillSystemContextRequiresAvailableEnabledSkillTool(t *testing.T)
}
}
func TestAgentSkillCommandCollisionsAreIgnored(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{"release-notes", "system", "exit"} {
if err := os.Mkdir(filepath.Join(dir, name), 0o755); err != nil {
t.Fatal(err)
}
content := "---\nname: " + name + "\ndescription: Test skill.\n---\nInstructions."
if err := os.WriteFile(filepath.Join(dir, name, "SKILL.md"), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
catalog, err := coreagent.DiscoverSkills(dir)
if err != nil {
t.Fatal(err)
}
ignored := catalog.ExcludeNames(agentchat.BuiltinSlashCommandNames())
if got, want := strings.Join(ignored, ", "), "exit, system"; got != want {
t.Fatalf("ignored skills = %q, want %q", got, want)
}
if _, err := catalog.Load("release-notes"); err != nil {
t.Fatalf("non-conflicting skill should remain available: %v", err)
}
if context := catalog.SystemContext(); !strings.Contains(context, "release-notes: Test skill.") || strings.Contains(context, "system: Test skill.") || strings.Contains(context, "exit: Test skill.") {
t.Fatalf("skill context = %q", context)
}
for _, name := range []string{"system", "exit"} {
if _, err := catalog.Load(name); err == nil {
t.Fatalf("conflicting skill %q should be ignored", name)
}
}
}
func TestAgentSelectionItemsUseLaunchSections(t *testing.T) {
items := agentSelectionItems([]agentchat.ModelOption{
{Name: "glm-5.2:cloud", Description: "cloud", Recommended: true, Cloud: true},

View File

@@ -64,6 +64,24 @@ var chatSlashCommands = []chatSlashCommand{
{name: "/save", usage: "/save <filename>", description: "save request JSON; saved as <filename>.json"},
}
// BuiltinSlashCommandNames returns the names reserved by built-in slash
// commands, including aliases.
func BuiltinSlashCommandNames() []string {
names := make(map[string]struct{})
for _, command := range chatSlashCommands {
names[strings.TrimPrefix(command.name, "/")] = struct{}{}
for _, alias := range command.aliases {
names[strings.TrimPrefix(alias, "/")] = struct{}{}
}
}
reserved := make([]string, 0, len(names))
for name := range names {
reserved = append(reserved, name)
}
sort.Strings(reserved)
return reserved
}
func (m *chatModel) handleSubmit() (tea.Model, tea.Cmd) {
m.syncInputPlaceholders()
input := strings.TrimSpace(string(m.input))