Initial commit: agent-tui
Bubble Tea TUI for tailing Claude Code subagent JSONL logs. Features: - Tabbed interface with one tab per agent, sorted by most recent activity - Markdown rendering via glamour (Dracula theme) - Session discovery and filtering with -s flag - Auto-discovers subagents dir from session ID prefix - Live tailing with follow mode, mouse scroll support - Agent name resolution from team config files - Tail-from-bottom: only reads last 200 lines per file on startup
This commit is contained in:
110
session.go
Normal file
110
session.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SessionInfo holds metadata about a discovered session.
|
||||
type SessionInfo struct {
|
||||
ID string
|
||||
AgentCount int
|
||||
EventCount int
|
||||
FirstSeen time.Time
|
||||
LastSeen time.Time
|
||||
Agents map[string]string // agentID -> slug
|
||||
}
|
||||
|
||||
// DiscoverSessions scans all JSONL files in a directory and groups them by sessionId.
|
||||
// Returns sessions sorted by LastSeen descending (most recent first).
|
||||
func DiscoverSessions(dir string) ([]SessionInfo, error) {
|
||||
matches, err := filepath.Glob(filepath.Join(dir, "*.jsonl"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessions := make(map[string]*SessionInfo)
|
||||
|
||||
for _, path := range matches {
|
||||
scanFileForSessions(path, sessions)
|
||||
}
|
||||
|
||||
// Convert to slice and sort by LastSeen descending
|
||||
result := make([]SessionInfo, 0, len(sessions))
|
||||
for _, s := range sessions {
|
||||
s.AgentCount = len(s.Agents)
|
||||
result = append(result, *s)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].LastSeen.After(result[j].LastSeen)
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// scanFileForSessions reads a JSONL file and registers sessions found.
|
||||
// It reads the first and last few lines for speed on large files.
|
||||
func scanFileForSessions(path string, sessions map[string]*SessionInfo) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 512*1024), 512*1024)
|
||||
|
||||
for scanner.Scan() {
|
||||
var evt struct {
|
||||
SessionID string `json:"sessionId"`
|
||||
AgentID string `json:"agentId"`
|
||||
Slug string `json:"slug"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil {
|
||||
continue
|
||||
}
|
||||
if evt.SessionID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
s, ok := sessions[evt.SessionID]
|
||||
if !ok {
|
||||
s = &SessionInfo{
|
||||
ID: evt.SessionID,
|
||||
Agents: make(map[string]string),
|
||||
}
|
||||
sessions[evt.SessionID] = s
|
||||
}
|
||||
|
||||
s.EventCount++
|
||||
|
||||
ts := parseTimestamp(evt.Timestamp)
|
||||
if !ts.IsZero() {
|
||||
if s.FirstSeen.IsZero() || ts.Before(s.FirstSeen) {
|
||||
s.FirstSeen = ts
|
||||
}
|
||||
if ts.After(s.LastSeen) {
|
||||
s.LastSeen = ts
|
||||
}
|
||||
}
|
||||
|
||||
if evt.AgentID != "" {
|
||||
if _, exists := s.Agents[evt.AgentID]; !exists {
|
||||
s.Agents[evt.AgentID] = evt.Slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shortSessionID returns the first 8 chars of a session UUID.
|
||||
func shortSessionID(id string) string {
|
||||
if len(id) > 8 {
|
||||
return id[:8]
|
||||
}
|
||||
return id
|
||||
}
|
||||
Reference in New Issue
Block a user