diff --git a/internal/skills/catalog.go b/internal/skills/catalog.go index 6dd6c9e3e8..bc0465788c 100644 --- a/internal/skills/catalog.go +++ b/internal/skills/catalog.go @@ -54,7 +54,7 @@ func Catalog(active []*Skill, skillPaths []string, workingDir string) []CatalogE Description: skill.Description, Label: label, Source: source, - UserInvocable: skill.UserInvocable, + UserInvocable: skill.IsUserInvocable(), }) } return entries diff --git a/internal/skills/skills.go b/internal/skills/skills.go index 9b431214c8..eb30c6c3b3 100644 --- a/internal/skills/skills.go +++ b/internal/skills/skills.go @@ -38,7 +38,7 @@ var ( type Skill struct { Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` - UserInvocable bool `yaml:"user-invocable" json:"user_invocable"` + UserInvocable *bool `yaml:"user-invocable,omitempty" json:"user_invocable,omitempty"` DisableModelInvocation bool `yaml:"disable-model-invocation" json:"disable_model_invocation"` License string `yaml:"license,omitempty" json:"license,omitempty"` Compatibility string `yaml:"compatibility,omitempty" json:"compatibility,omitempty"` @@ -49,6 +49,13 @@ type Skill struct { Builtin bool `yaml:"-" json:"builtin"` } +// IsUserInvocable reports whether the skill should appear in the command +// palette. When the user-invocable frontmatter field is absent, skills +// default to user-invocable to make the feature opt-out. +func (s *Skill) IsUserInvocable() bool { + return s.UserInvocable == nil || *s.UserInvocable +} + // DiscoveryState represents the outcome of discovering a single skill file. type DiscoveryState int diff --git a/internal/skills/skills_test.go b/internal/skills/skills_test.go index fc78b8dc62..32d40a64c5 100644 --- a/internal/skills/skills_test.go +++ b/internal/skills/skills_test.go @@ -400,6 +400,41 @@ func TestParseContent_NoFrontmatter(t *testing.T) { require.Error(t, err) } +func TestIsUserInvocable_DefaultTrue(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + yaml string + want bool + }{ + { + name: "absent defaults to true", + yaml: "---\nname: a\ndescription: x.\n---\n", + want: true, + }, + { + name: "explicit true", + yaml: "---\nname: a\ndescription: x.\nuser-invocable: true\n---\n", + want: true, + }, + { + name: "explicit false", + yaml: "---\nname: a\ndescription: x.\nuser-invocable: false\n---\n", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + skill, err := ParseContent([]byte(tt.yaml)) + require.NoError(t, err) + require.Equal(t, tt.want, skill.IsUserInvocable()) + }) + } +} + func TestDiscoverBuiltin(t *testing.T) { t.Parallel()