From b68823b0aef7e7b913b461529096d17ad4d36599 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Tue, 31 Mar 2026 16:24:11 -0400 Subject: [PATCH] feat(cli/session): show skill metadata (#2541) --- internal/cmd/session.go | 78 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/internal/cmd/session.go b/internal/cmd/session.go index d5fa973516b9b63a926cd4a30e81c3be8b4de43a..2d3793aec05d1701c67278e2dca08d8216398347 100644 --- a/internal/cmd/session.go +++ b/internal/cmd/session.go @@ -9,12 +9,14 @@ import ( "os" "os/exec" "runtime" + "sort" "strings" "syscall" "time" "charm.land/lipgloss/v2" "github.com/charmbracelet/colorprofile" + "github.com/charmbracelet/crush/internal/agent/tools" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/db" "github.com/charmbracelet/crush/internal/event" @@ -397,6 +399,7 @@ func messagePtrs(msgs []message.Message) []*message.Message { } func outputSessionJSON(w io.Writer, sess session.Session, msgs []*message.Message) error { + skills := extractSkillsFromMessages(msgs) output := sessionShowOutput{ Meta: sessionShowMeta{ ID: session.HashID(sess.ID), @@ -408,6 +411,7 @@ func outputSessionJSON(w io.Writer, sess session.Session, msgs []*message.Messag PromptTokens: sess.PromptTokens, CompletionTokens: sess.CompletionTokens, TotalTokens: sess.PromptTokens + sess.CompletionTokens, + Skills: skills, }, Messages: make([]sessionShowMessage, len(msgs)), } @@ -444,6 +448,8 @@ func outputSessionHuman(ctx context.Context, sess session.Session, msgs []*messa hash := session.HashID(sess.ID)[:12] created := time.Unix(sess.CreatedAt, 0).Format("Mon Jan 2 15:04:05 2006 -0700") + skills := extractSkillsFromMessages(msgs) + // Render to buffer to determine actual height var buf strings.Builder @@ -451,6 +457,19 @@ func outputSessionHuman(ctx context.Context, sess session.Session, msgs []*messa fmt.Fprintln(&buf, keyStyle.Render("UUID: ")+valStyle.Render(sess.ID)) fmt.Fprintln(&buf, keyStyle.Render("Title: ")+valStyle.Render(sess.Title)) fmt.Fprintln(&buf, keyStyle.Render("Date: ")+valStyle.Render(created)) + if len(skills) > 0 { + skillNames := make([]string, len(skills)) + for i, s := range skills { + timestamp := time.Unix(sess.CreatedAt, 0).Format("15:04:05 -0700") + if s.LoadedAt != "" { + if t, err := time.Parse(time.RFC3339, s.LoadedAt); err == nil { + timestamp = t.Format("15:04:05 -0700") + } + } + skillNames[i] = fmt.Sprintf("%s (%s)", s.Name, timestamp) + } + fmt.Fprintln(&buf, keyStyle.Render("Skills: ")+valStyle.Render(strings.Join(skillNames, ", "))) + } fmt.Fprintln(&buf) first := true @@ -539,15 +558,22 @@ func sessionWriter(ctx context.Context, contentHeight int) (io.Writer, func(), b } type sessionShowMeta struct { - ID string `json:"id"` - UUID string `json:"uuid"` - Title string `json:"title"` - Created string `json:"created"` - Modified string `json:"modified"` - Cost float64 `json:"cost"` - PromptTokens int64 `json:"prompt_tokens"` - CompletionTokens int64 `json:"completion_tokens"` - TotalTokens int64 `json:"total_tokens"` + ID string `json:"id"` + UUID string `json:"uuid"` + Title string `json:"title"` + Created string `json:"created"` + Modified string `json:"modified"` + Cost float64 `json:"cost"` + PromptTokens int64 `json:"prompt_tokens"` + CompletionTokens int64 `json:"completion_tokens"` + TotalTokens int64 `json:"total_tokens"` + Skills []sessionShowSkill `json:"skills,omitempty"` +} + +type sessionShowSkill struct { + Name string `json:"name"` + Description string `json:"description"` + LoadedAt string `json:"loaded_at"` } type sessionShowMessage struct { @@ -592,6 +618,40 @@ type sessionShowPart struct { Time int64 `json:"time,omitempty"` } +func extractSkillsFromMessages(msgs []*message.Message) []sessionShowSkill { + var skills []sessionShowSkill + seen := make(map[string]bool) + + for _, msg := range msgs { + for _, part := range msg.Parts { + if tr, ok := part.(message.ToolResult); ok && tr.Metadata != "" { + var meta tools.ViewResponseMetadata + if err := json.Unmarshal([]byte(tr.Metadata), &meta); err == nil { + if meta.ResourceType == tools.ViewResourceSkill && meta.ResourceName != "" { + if !seen[meta.ResourceName] { + seen[meta.ResourceName] = true + skills = append(skills, sessionShowSkill{ + Name: meta.ResourceName, + Description: meta.ResourceDescription, + LoadedAt: time.Unix(msg.CreatedAt, 0).Format(time.RFC3339), + }) + } + } + } + } + } + } + + sort.Slice(skills, func(i, j int) bool { + if skills[i].LoadedAt == skills[j].LoadedAt { + return skills[i].Name < skills[j].Name + } + return skills[i].LoadedAt < skills[j].LoadedAt + }) + + return skills +} + func convertParts(parts []message.ContentPart) []sessionShowPart { result := make([]sessionShowPart, 0, len(parts)) for _, part := range parts {