on main help sign in
feat(cli/session): show skill metadata (#2541)
commitb68823b0aef7e7b913b461529096d17ad4d36599 authorChristian Rocha <christian@rocha.is> dateTue 31 Mar 2026, 16:24 (7 months ago) parent480e0f1
1 file changed+69−9
internal/cmd/session.go+69−9
internal/cmd/session.go+69−9
@@ -9,12 +9,14 @@ import (
99 "os"
1010 "os/exec"
1111 "runtime"
12+ "sort"
1213 "strings"
1314 "syscall"
1415 "time"
1516
1617 "charm.land/lipgloss/v2"
1718 "github.com/charmbracelet/colorprofile"
19+ "github.com/charmbracelet/crush/internal/agent/tools"
1820 "github.com/charmbracelet/crush/internal/config"
1921 "github.com/charmbracelet/crush/internal/db"
2022 "github.com/charmbracelet/crush/internal/event"
@@ -397,6 +399,7 @@ func messagePtrs(msgs []message.Message) []*message.Message {
397399}
398400
399401func outputSessionJSON(w io.Writer, sess session.Session, msgs []*message.Message) error {
402+ skills := extractSkillsFromMessages(msgs)
400403 output := sessionShowOutput{
401404 Meta: sessionShowMeta{
402405 ID: session.HashID(sess.ID),
@@ -408,6 +411,7 @@ func outputSessionJSON(w io.Writer, sess session.Session, msgs []*message.Messag
408411 PromptTokens: sess.PromptTokens,
409412 CompletionTokens: sess.CompletionTokens,
410413 TotalTokens: sess.PromptTokens + sess.CompletionTokens,
414+ Skills: skills,
411415 },
412416 Messages: make([]sessionShowMessage, len(msgs)),
413417 }
@@ -444,6 +448,8 @@ func outputSessionHuman(ctx context.Context, sess session.Session, msgs []*messa
444448 hash := session.HashID(sess.ID)[:12]
445449 created := time.Unix(sess.CreatedAt, 0).Format("Mon Jan 2 15:04:05 2006 -0700")
446450
451+ skills := extractSkillsFromMessages(msgs)
452+
447453 // Render to buffer to determine actual height
448454 var buf strings.Builder
449455
@@ -451,6 +457,19 @@ func outputSessionHuman(ctx context.Context, sess session.Session, msgs []*messa
451457 fmt.Fprintln(&buf, keyStyle.Render("UUID: ")+valStyle.Render(sess.ID))
452458 fmt.Fprintln(&buf, keyStyle.Render("Title: ")+valStyle.Render(sess.Title))
453459 fmt.Fprintln(&buf, keyStyle.Render("Date: ")+valStyle.Render(created))
460+ if len(skills) > 0 {
461+ skillNames := make([]string, len(skills))
462+ for i, s := range skills {
463+ timestamp := time.Unix(sess.CreatedAt, 0).Format("15:04:05 -0700")
464+ if s.LoadedAt != "" {
465+ if t, err := time.Parse(time.RFC3339, s.LoadedAt); err == nil {
466+ timestamp = t.Format("15:04:05 -0700")
467+ }
468+ }
469+ skillNames[i] = fmt.Sprintf("%s (%s)", s.Name, timestamp)
470+ }
471+ fmt.Fprintln(&buf, keyStyle.Render("Skills: ")+valStyle.Render(strings.Join(skillNames, ", ")))
472+ }
454473 fmt.Fprintln(&buf)
455474
456475 first := true
@@ -539,15 +558,22 @@ func sessionWriter(ctx context.Context, contentHeight int) (io.Writer, func(), b
539558}
540559
541560type sessionShowMeta struct {
542− ID string `json:"id"`
543− UUID string `json:"uuid"`
544− Title string `json:"title"`
545− Created string `json:"created"`
546− Modified string `json:"modified"`
547− Cost float64 `json:"cost"`
548− PromptTokens int64 `json:"prompt_tokens"`
549− CompletionTokens int64 `json:"completion_tokens"`
550− TotalTokens int64 `json:"total_tokens"`
561+ ID string `json:"id"`
562+ UUID string `json:"uuid"`
563+ Title string `json:"title"`
564+ Created string `json:"created"`
565+ Modified string `json:"modified"`
566+ Cost float64 `json:"cost"`
567+ PromptTokens int64 `json:"prompt_tokens"`
568+ CompletionTokens int64 `json:"completion_tokens"`
569+ TotalTokens int64 `json:"total_tokens"`
570+ Skills []sessionShowSkill `json:"skills,omitempty"`
571+}
572+
573+type sessionShowSkill struct {
574+ Name string `json:"name"`
575+ Description string `json:"description"`
576+ LoadedAt string `json:"loaded_at"`
551577}
552578
553579type sessionShowMessage struct {
@@ -592,6 +618,40 @@ type sessionShowPart struct {
592618 Time int64 `json:"time,omitempty"`
593619}
594620
621+func extractSkillsFromMessages(msgs []*message.Message) []sessionShowSkill {
622+ var skills []sessionShowSkill
623+ seen := make(map[string]bool)
624+
625+ for _, msg := range msgs {
626+ for _, part := range msg.Parts {
627+ if tr, ok := part.(message.ToolResult); ok && tr.Metadata != "" {
628+ var meta tools.ViewResponseMetadata
629+ if err := json.Unmarshal([]byte(tr.Metadata), &meta); err == nil {
630+ if meta.ResourceType == tools.ViewResourceSkill && meta.ResourceName != "" {
631+ if !seen[meta.ResourceName] {
632+ seen[meta.ResourceName] = true
633+ skills = append(skills, sessionShowSkill{
634+ Name: meta.ResourceName,
635+ Description: meta.ResourceDescription,
636+ LoadedAt: time.Unix(msg.CreatedAt, 0).Format(time.RFC3339),
637+ })
638+ }
639+ }
640+ }
641+ }
642+ }
643+ }
644+
645+ sort.Slice(skills, func(i, j int) bool {
646+ if skills[i].LoadedAt == skills[j].LoadedAt {
647+ return skills[i].Name < skills[j].Name
648+ }
649+ return skills[i].LoadedAt < skills[j].LoadedAt
650+ })
651+
652+ return skills
653+}
654+
595655func convertParts(parts []message.ContentPart) []sessionShowPart {
596656 result := make([]sessionShowPart, 0, len(parts))
597657 for _, part := range parts {