Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

174 lines
5.1 KiB
Go

// ABOUTME: `session get <id>` subcommand — prints session detail
// ABOUTME: in human or JSON format.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"github.com/spf13/cobra"
"go.kenn.io/agentsview/internal/parser"
"go.kenn.io/agentsview/internal/service"
)
func newSessionGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <id>",
Short: "Get session metadata and signals",
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
svc, cleanup, err := resolveService(cmd)
if err != nil {
return err
}
defer cleanup()
detail, err := lookupSessionWithPrefixes(
cmd.Context(), svc, args[0],
)
if err != nil {
return err
}
if detail == nil {
return fmt.Errorf("session %s not found", args[0])
}
if outputFormat(cmd) == "json" {
return json.NewEncoder(cmd.OutOrStdout()).Encode(detail)
}
return printSessionDetailHuman(cmd.OutOrStdout(), detail)
},
}
}
// resolveServiceSessionID returns the canonical session ID matching id,
// accommodating bare UUIDs by retrying with each registered agent
// prefix (codex:, copilot:, gemini:, ...) when the exact lookup
// misses. Stored IDs are prefixed for non-Claude agents, so a user
// copying a UUID from a session file name would otherwise see a
// confusing "not found" error. Returns an error whose message
// begins with "session not found:" when no match exists — callers
// get a clear failure instead of silent empty output.
func resolveServiceSessionID(
ctx context.Context,
svc service.SessionService,
id string,
) (string, error) {
detail, err := svc.Get(ctx, id)
if err != nil {
return "", err
}
if detail != nil {
return id, nil
}
// If the user already supplied a known agent-prefixed ID or
// a host-prefixed remote ID ("host~..."), don't second-guess
// them — the exact lookup is authoritative. Some raw IDs
// (Kimi/Kimi Code, OpenClaw) contain colons before the agent
// prefix is added, so an arbitrary colon is not enough to
// classify the input as canonical.
if isCanonicalServiceSessionID(id) {
return "", fmt.Errorf("session not found: %s", id)
}
for _, def := range parser.Registry {
if def.IDPrefix == "" {
continue
}
candidate := def.IDPrefix + id
detail, err := svc.Get(ctx, candidate)
if err != nil {
return "", err
}
if detail != nil {
return candidate, nil
}
}
return "", fmt.Errorf("session not found: %s", id)
}
func isCanonicalServiceSessionID(id string) bool {
if strings.Contains(id, "~") {
return true
}
_, rawID := parser.StripHostPrefix(id)
for _, def := range parser.Registry {
if def.IDPrefix != "" && strings.HasPrefix(rawID, def.IDPrefix) {
return true
}
}
return false
}
// lookupSessionWithPrefixes fetches a session detail, trying agent
// prefixes for bare UUIDs. Preserved as a thin wrapper around
// resolveServiceSessionID + svc.Get so `session get` can keep its
// existing "return nil on not-found" semantics (which render the
// "session %s not found" error at the command boundary).
func lookupSessionWithPrefixes(
ctx context.Context,
svc service.SessionService,
id string,
) (*service.SessionDetail, error) {
resolved, err := resolveServiceSessionID(ctx, svc, id)
if err != nil {
if strings.HasPrefix(err.Error(), "session not found:") {
return nil, nil
}
return nil, err
}
return svc.Get(ctx, resolved)
}
// printSessionDetailHuman writes a compact key/value summary of
// the session's core fields. Optional *string/*int fields render
// as "-" when nil.
func printSessionDetailHuman(w io.Writer, s *service.SessionDetail) error {
label := func(name string) string {
return fmt.Sprintf("%-14s", name+":")
}
name := s.ID
if s.DisplayName != nil && *s.DisplayName != "" {
name = *s.DisplayName
}
fmt.Fprintf(w, "%s %s\n", label("ID"), sanitizeTerminal(s.ID))
fmt.Fprintf(w, "%s %s\n", label("Name"), sanitizeTerminal(name))
fmt.Fprintf(w, "%s %s\n", label("Project"), sanitizeTerminal(s.Project))
fmt.Fprintf(w, "%s %s\n", label("Agent"), sanitizeTerminal(s.Agent))
fmt.Fprintf(w, "%s %s\n", label("Machine"), sanitizeTerminal(s.Machine))
fmt.Fprintf(w, "%s %s\n",
label("Started At"), sanitizeTerminal(derefStringOrDash(s.StartedAt)))
fmt.Fprintf(w, "%s %s\n",
label("Ended At"), sanitizeTerminal(derefStringOrDash(s.EndedAt)))
fmt.Fprintf(w, "%s %d/%d\n",
label("Messages"), s.UserMessageCount, s.MessageCount)
if s.Outcome != "" {
fmt.Fprintf(w, "%s %s [%s]\n", label("Outcome"),
sanitizeTerminal(s.Outcome), sanitizeTerminal(s.OutcomeConfidence))
}
if s.HealthScore != nil {
grade := "-"
if s.HealthGrade != nil && *s.HealthGrade != "" {
grade = *s.HealthGrade
}
fmt.Fprintf(w, "%s %d (%s)\n",
label("Health"), *s.HealthScore, sanitizeTerminal(grade))
} else {
fmt.Fprintf(w, "%s -\n", label("Health"))
}
if s.SecretLeakCount > 0 {
fmt.Fprintf(w, "%s %d\n", label("Secrets"), s.SecretLeakCount)
}
return nil
}
// derefStringOrDash returns *p or "-" when p is nil or empty.
func derefStringOrDash(p *string) string {
if p == nil || *p == "" {
return "-"
}
return *p
}