feat(ui): indicate when skills are loaded

Christian Rocha created

Change summary

internal/agent/tools/view.go | 33 +++++++++++++++++++++++++++------
internal/ui/chat/file.go     |  6 ++++++
internal/ui/chat/tools.go    | 19 +++++++++++++++----
internal/ui/styles/styles.go |  6 ++++--
4 files changed, 52 insertions(+), 12 deletions(-)

Detailed changes

internal/agent/tools/view.go 🔗

@@ -17,6 +17,7 @@ import (
 	"github.com/charmbracelet/crush/internal/filetracker"
 	"github.com/charmbracelet/crush/internal/lsp"
 	"github.com/charmbracelet/crush/internal/permission"
+	"github.com/charmbracelet/crush/internal/skills"
 )
 
 //go:embed view.md
@@ -34,9 +35,19 @@ type ViewPermissionsParams struct {
 	Limit    int    `json:"limit"`
 }
 
+type ViewResourceType string
+
+const (
+	ViewResourceUnset ViewResourceType = ""
+	ViewResourceSkill ViewResourceType = "skill"
+)
+
 type ViewResponseMetadata struct {
-	FilePath string `json:"file_path"`
-	Content  string `json:"content"`
+	FilePath            string           `json:"file_path"`
+	Content             string           `json:"content"`
+	ResourceType        ViewResourceType `json:"resource_type,omitempty"`
+	ResourceName        string           `json:"resource_name,omitempty"`
+	ResourceDescription string           `json:"resource_description,omitempty"`
 }
 
 const (
@@ -196,12 +207,22 @@ func NewViewTool(
 			output += "\n</file>\n"
 			output += getDiagnostics(filePath, lspManager)
 			filetracker.RecordRead(ctx, sessionID, filePath)
+
+			meta := ViewResponseMetadata{
+				FilePath: filePath,
+				Content:  content,
+			}
+			if isSkillFile {
+				if skill, err := skills.Parse(filePath); err == nil {
+					meta.ResourceType = ViewResourceSkill
+					meta.ResourceName = skill.Name
+					meta.ResourceDescription = skill.Description
+				}
+			}
+
 			return fantasy.WithResponseMetadata(
 				fantasy.NewTextResponse(output),
-				ViewResponseMetadata{
-					FilePath: filePath,
-					Content:  content,
-				},
+				meta,
 			), nil
 		})
 }

internal/ui/chat/file.go 🔗

@@ -82,6 +82,12 @@ func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *
 		content = meta.Content
 	}
 
+	// Handle skill content.
+	if meta.ResourceType == tools.ViewResourceSkill {
+		body := toolOutputSkillContent(sty, meta.ResourceName, meta.ResourceDescription)
+		return joinToolParts(header, body)
+	}
+
 	if content == "" {
 		return header
 	}

internal/ui/chat/tools.go 🔗

@@ -625,10 +625,21 @@ func toolOutputImageContent(sty *styles.Styles, data, mediaType string) string {
 
 	return sty.Tool.Body.Render(fmt.Sprintf(
 		"%s %s %s %s",
-		sty.Tool.ResourceLoadedText.String(),
-		sty.Tool.ResourceLoadedIndicator.String(),
-		sty.Base.Render(mediaType),
-		sty.Subtle.Render(sizeStr),
+		sty.Tool.ResourceLoadedText.Render("Loaded Image"),
+		sty.Tool.ResourceLoadedIndicator.Render(styles.ArrowRightIcon),
+		sty.Tool.MediaType.Render(mediaType),
+		sty.Tool.ResourceSize.Render(sizeStr),
+	))
+}
+
+// toolOutputSkillContent renders a skill loaded indicator.
+func toolOutputSkillContent(sty *styles.Styles, name, description string) string {
+	return sty.Tool.Body.Render(fmt.Sprintf(
+		"%s %s %s %s",
+		sty.Tool.ResourceLoadedText.Render("Loaded Skill"),
+		sty.Tool.ResourceLoadedIndicator.Render(styles.ArrowRightIcon),
+		sty.Tool.ResourceName.Render(name),
+		sty.Tool.ResourceSize.Render(description),
 	))
 }
 

internal/ui/styles/styles.go 🔗

@@ -325,6 +325,7 @@ type Styles struct {
 		// Images and external resources
 		ResourceLoadedText      lipgloss.Style
 		ResourceLoadedIndicator lipgloss.Style
+		ResourceName            lipgloss.Style
 		ResourceSize            lipgloss.Style
 		MediaType               lipgloss.Style
 	}
@@ -1175,8 +1176,9 @@ func DefaultStyles() Styles {
 	s.Tool.MCPArrow = base.Foreground(blue).SetString(ArrowRightIcon)
 
 	// Loading indicators for images, skills
-	s.Tool.ResourceLoadedText = base.Foreground(green).SetString("Loaded")
-	s.Tool.ResourceLoadedIndicator = base.Foreground(greenDark).SetString(ArrowRightIcon)
+	s.Tool.ResourceLoadedText = base.Foreground(green)
+	s.Tool.ResourceLoadedIndicator = base.Foreground(greenDark)
+	s.Tool.ResourceName = base
 	s.Tool.MediaType = base
 	s.Tool.ResourceSize = base.Foreground(fgMuted)