@@ -13,6 +13,7 @@ import (
1313 "github.com/charmbracelet/crush/internal/config"
1414 "github.com/charmbracelet/crush/internal/csync"
1515 "github.com/charmbracelet/crush/internal/lsp"
16+ "github.com/charmbracelet/crush/internal/skills"
1617 "github.com/stretchr/testify/require"
1718)
1819
@@ -22,7 +23,7 @@ func TestCrushInfo_MinimalConfig(t *testing.T) {
2223 cfg := config.NewTestStore(&config.Config{
2324 Providers: csync.NewMap[string, config.ProviderConfig](),
2425 })
25− output := buildCrushInfo(cfg, nil)
26+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
2627 require.NotContains(t, output, "[providers]")
2728 require.NotContains(t, output, "[lsp]")
2829 require.NotContains(t, output, "[mcp]")
@@ -38,7 +39,7 @@ func TestCrushInfo_ConfigFiles(t *testing.T) {
3839 "/home/user/.config/crush/crush.json",
3940 "/project/.crush/crush.json",
4041 )
41− output := buildCrushInfo(cfg, nil)
42+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
4243 require.Contains(t, output, "[config_files]")
4344 require.Contains(t, output, "/home/user/.config/crush/crush.json")
4445 require.Contains(t, output, "/project/.crush/crush.json")
@@ -54,7 +55,7 @@ func TestCrushInfo_Models(t *testing.T) {
5455 },
5556 Providers: csync.NewMap[string, config.ProviderConfig](),
5657 })
57− output := buildCrushInfo(cfg, nil)
58+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
5859 require.Contains(t, output, "[model]")
5960 require.Contains(t, output, "large = claude-sonnet-4-20250514 (anthropic)")
6061 require.Contains(t, output, "small = claude-haiku-3-20250307 (anthropic)")
@@ -68,7 +69,7 @@ func TestCrushInfo_Providers(t *testing.T) {
6869 providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
6970
7071 cfg := config.NewTestStore(&config.Config{Providers: providers})
71− output := buildCrushInfo(cfg, nil)
72+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
7273 require.Contains(t, output, "[providers]")
7374 anthropicIdx := strings.Index(output, "anthropic = enabled")
7475 openaiIdx := strings.Index(output, "openai = enabled")
@@ -87,7 +88,7 @@ func TestCrushInfo_DisabledProvidersOmitted(t *testing.T) {
8788 providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
8889
8990 cfg := config.NewTestStore(&config.Config{Providers: providers})
90− output := buildCrushInfo(cfg, nil)
91+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
9192 require.Contains(t, output, "anthropic = enabled")
9293 require.NotContains(t, output, "openai")
9394}
@@ -107,7 +108,7 @@ func TestCrushInfo_LSPStates(t *testing.T) {
107108 mgr.Clients().Set("pyright", errorClient)
108109
109110 cfg := config.NewTestStore(&config.Config{Providers: csync.NewMap[string, config.ProviderConfig]()})
110− output := buildCrushInfo(cfg, mgr)
111+ output := buildCrushInfo(cfg, mgr, nil, nil, nil)
111112 require.Contains(t, output, "[lsp]")
112113 require.Contains(t, output, "gopls = ready")
113114 require.Contains(t, output, "pyright = error")
@@ -158,7 +159,7 @@ func TestCrushInfo_YoloMode(t *testing.T) {
158159 })
159160 cfg.Overrides().SkipPermissionRequests = true
160161
161− output := buildCrushInfo(cfg, nil)
162+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
162163 require.Contains(t, output, "[permissions]")
163164 require.Contains(t, output, "mode = yolo")
164165}
@@ -171,7 +172,7 @@ func TestCrushInfo_AllowedTools(t *testing.T) {
171172 Permissions: &config.Permissions{AllowedTools: []string{"edit:write", "bash"}},
172173 })
173174
174− output := buildCrushInfo(cfg, nil)
175+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
175176 require.Contains(t, output, "[permissions]")
176177 require.Contains(t, output, "allowed_tools = bash, edit:write")
177178}
@@ -184,7 +185,7 @@ func TestCrushInfo_DisabledTools(t *testing.T) {
184185 Options: &config.Options{DisabledTools: []string{"sourcegraph", "agentic_fetch"}},
185186 })
186187
187− output := buildCrushInfo(cfg, nil)
188+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
188189 require.Contains(t, output, "[tools]")
189190 require.Contains(t, output, "disabled = agentic_fetch, sourcegraph")
190191}
@@ -201,7 +202,7 @@ func TestCrushInfo_Options(t *testing.T) {
201202 },
202203 })
203204
204− output := buildCrushInfo(cfg, nil)
205+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
205206 require.Contains(t, output, "[options]")
206207 require.Contains(t, output, "auto_lsp = true")
207208 require.Contains(t, output, "auto_summarize = false")
@@ -216,14 +217,14 @@ func TestCrushInfo_AutoSummarizeInversion(t *testing.T) {
216217 Providers: csync.NewMap[string, config.ProviderConfig](),
217218 Options: &config.Options{DisableAutoSummarize: true},
218219 })
219− outputFalse := buildCrushInfo(cfgFalse, nil)
220+ outputFalse := buildCrushInfo(cfgFalse, nil, nil, nil, nil)
220221 require.Contains(t, outputFalse, "auto_summarize = false")
221222
222223 cfgTrue := config.NewTestStore(&config.Config{
223224 Providers: csync.NewMap[string, config.ProviderConfig](),
224225 Options: &config.Options{DisableAutoSummarize: false},
225226 })
226− outputTrue := buildCrushInfo(cfgTrue, nil)
227+ outputTrue := buildCrushInfo(cfgTrue, nil, nil, nil, nil)
227228 require.Contains(t, outputTrue, "auto_summarize = true")
228229}
229230
@@ -237,7 +238,7 @@ func TestCrushInfo_NoSecrets(t *testing.T) {
237238 })
238239
239240 cfg := config.NewTestStore(&config.Config{Providers: providers})
240− output := buildCrushInfo(cfg, nil)
241+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
241242 require.NotContains(t, output, "sk-super-secret-key-12345")
242243 require.NotContains(t, output, "secret")
243244 require.Contains(t, output, "openai = enabled (8 models)")
@@ -273,7 +274,7 @@ func TestCrushInfo_DeterministicOrdering(t *testing.T) {
273274 zMcpIdx := strings.Index(mcpOutput, "z-mcp = connected")
274275 require.Less(t, aMcpIdx, zMcpIdx)
275276
276− output := buildCrushInfo(cfg, nil)
277+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
277278
278279 alphaIdx := strings.Index(output, "alpha = enabled")
279280 middleIdx := strings.Index(output, "middle = enabled")
@@ -294,11 +295,12 @@ func TestCrushInfo_EmptySectionsOmitted(t *testing.T) {
294295 Options: &config.Options{},
295296 })
296297
297− output := buildCrushInfo(cfg, nil)
298+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
298299 require.NotContains(t, output, "[tools]")
299300 require.NotContains(t, output, "[permissions]")
300301 require.NotContains(t, output, "[lsp]")
301302 require.NotContains(t, output, "[mcp]")
303+ require.NotContains(t, output, "[skills]")
302304}
303305
304306func TestCrushInfo_ConfigStaleness_Clean(t *testing.T) {
@@ -315,7 +317,7 @@ func TestCrushInfo_ConfigStaleness_Clean(t *testing.T) {
315317 // Capture snapshot (normally done in Load)
316318 store.CaptureStalenessSnapshot([]string{configPath})
317319
318− output := buildCrushInfo(store, nil)
320+ output := buildCrushInfo(store, nil, nil, nil, nil)
319321 require.Contains(t, output, "[config]")
320322 require.Contains(t, output, "dirty = false")
321323 require.NotContains(t, output, "changed_paths")
@@ -340,7 +342,7 @@ func TestCrushInfo_ConfigStaleness_Dirty(t *testing.T) {
340342 time.Sleep(10 * time.Millisecond)
341343 require.NoError(t, os.WriteFile(configPath, []byte(`{"debug": true}`), 0o600))
342344
343− output := buildCrushInfo(store, nil)
345+ output := buildCrushInfo(store, nil, nil, nil, nil)
344346 require.Contains(t, output, "[config]")
345347 require.Contains(t, output, "dirty = true")
346348 require.Contains(t, output, "changed_paths")
@@ -364,9 +366,110 @@ func TestCrushInfo_ConfigStaleness_MissingPath(t *testing.T) {
364366 // Delete file to trigger missing state
365367 require.NoError(t, os.Remove(configPath))
366368
367− output := buildCrushInfo(store, nil)
369+ output := buildCrushInfo(store, nil, nil, nil, nil)
368370 require.Contains(t, output, "[config]")
369371 require.Contains(t, output, "dirty = true")
370372 require.Contains(t, output, "missing_paths")
371373 require.Contains(t, output, configPath)
372374}
375+
376+func TestCrushInfo_Skills_NoSkills(t *testing.T) {
377+ t.Parallel()
378+
379+ cfg := config.NewTestStore(&config.Config{
380+ Providers: csync.NewMap[string, config.ProviderConfig](),
381+ })
382+ output := buildCrushInfo(cfg, nil, nil, nil, nil)
383+ require.NotContains(t, output, "[skills]")
384+}
385+
386+func TestCrushInfo_Skills_MixedLoadedUnloaded(t *testing.T) {
387+ t.Parallel()
388+
389+ allSkills := []*skills.Skill{
390+ {Name: "go-doc", Builtin: false},
391+ {Name: "bash", Builtin: false},
392+ {Name: "crush-config", Builtin: true},
393+ }
394+ activeSkills := allSkills
395+
396+ tracker := skills.NewTracker(activeSkills)
397+ tracker.MarkLoaded("bash")
398+ tracker.MarkLoaded("crush-config")
399+
400+ cfg := config.NewTestStore(&config.Config{
401+ Providers: csync.NewMap[string, config.ProviderConfig](),
402+ })
403+ output := buildCrushInfo(cfg, nil, allSkills, activeSkills, tracker)
404+ require.Contains(t, output, "[skills]")
405+ require.Contains(t, output, "bash = user, loaded")
406+ require.Contains(t, output, "crush-config = builtin, loaded")
407+ require.Contains(t, output, "go-doc = user, unloaded")
408+}
409+
410+func TestCrushInfo_Skills_DisabledSkills(t *testing.T) {
411+ t.Parallel()
412+
413+ allSkills := []*skills.Skill{
414+ {Name: "bash", Builtin: false},
415+ {Name: "crush-config", Builtin: true},
416+ {Name: "image-convert", Builtin: false},
417+ }
418+ activeSkills := []*skills.Skill{
419+ {Name: "bash", Builtin: false},
420+ {Name: "crush-config", Builtin: true},
421+ }
422+
423+ tracker := skills.NewTracker(activeSkills)
424+
425+ cfg := config.NewTestStore(&config.Config{
426+ Providers: csync.NewMap[string, config.ProviderConfig](),
427+ Options: &config.Options{DisabledSkills: []string{"image-convert"}},
428+ })
429+ output := buildCrushInfo(cfg, nil, allSkills, activeSkills, tracker)
430+ require.Contains(t, output, "[skills]")
431+ require.Contains(t, output, "bash = user, unloaded")
432+ require.Contains(t, output, "crush-config = builtin, unloaded")
433+ require.Contains(t, output, "image-convert = user, disabled")
434+}
435+
436+func TestCrushInfo_Skills_Ordering(t *testing.T) {
437+ t.Parallel()
438+
439+ allSkills := []*skills.Skill{
440+ {Name: "z-skill", Builtin: false},
441+ {Name: "a-skill", Builtin: true},
442+ {Name: "m-skill", Builtin: false},
443+ }
444+ activeSkills := allSkills
445+ tracker := skills.NewTracker(activeSkills)
446+
447+ cfg := config.NewTestStore(&config.Config{
448+ Providers: csync.NewMap[string, config.ProviderConfig](),
449+ })
450+ output := buildCrushInfo(cfg, nil, allSkills, activeSkills, tracker)
451+
452+ aIdx := strings.Index(output, "a-skill")
453+ mIdx := strings.Index(output, "m-skill")
454+ zIdx := strings.Index(output, "z-skill")
455+ require.Less(t, aIdx, mIdx)
456+ require.Less(t, mIdx, zIdx)
457+}
458+
459+func TestCrushInfo_Skills_BuiltinOrigin(t *testing.T) {
460+ t.Parallel()
461+
462+ allSkills := []*skills.Skill{
463+ {Name: "crush-config", Builtin: true},
464+ {Name: "my-skill", Builtin: false},
465+ }
466+ activeSkills := allSkills
467+ tracker := skills.NewTracker(activeSkills)
468+
469+ cfg := config.NewTestStore(&config.Config{
470+ Providers: csync.NewMap[string, config.ProviderConfig](),
471+ })
472+ output := buildCrushInfo(cfg, nil, allSkills, activeSkills, tracker)
473+ require.Contains(t, output, "crush-config = builtin, unloaded")
474+ require.Contains(t, output, "my-skill = user, unloaded")
475+}