From f06280bf6cc1e43ce0391889521028667f8ac53f Mon Sep 17 00:00:00 2001 From: CnsMaple Date: Fri, 5 Jun 2026 14:17:42 +0800 Subject: [PATCH] feat(skills): default user-invocable to true MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a SKILL.md does not declare `user-invocable`, treat the skill as user-invocable so the feature is opt-out. Users who do not want a skill in the command palette can set `user-invocable: false` explicitly. Implementation switches `Skill.UserInvocable` to `*bool` to distinguish "absent" from "false" and adds an `IsUserInvocable` helper. The catalog and downstream `CatalogEntry` / `SkillInfo` keep a plain bool filled in from the helper, so no consumer changes are required. Refs: #2834 (initial user-invocable support), #2970 (skill picker UX), #2562 / #2467 (earlier command-palette dialogs). 💘 Generated with Crush Assisted-by: Crush:MiniMax-M3 --- internal/skills/catalog.go | 2 +- internal/skills/skills.go | 9 ++++++++- internal/skills/skills_test.go | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) 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()