package main import ( "flag" "fmt" "os" "path/filepath" "strings" tea "github.com/charmbracelet/bubbletea" ) // findSubagentsDir searches ~/.claude/projects for a session ID prefix // and returns the subagents directory path. func findSubagentsDir(sessionPrefix string) (string, error) { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("cannot determine home directory: %w", err) } projectsRoot := filepath.Join(home, ".claude", "projects") // Walk project dirs looking for a session dir matching the prefix projectDirs, err := os.ReadDir(projectsRoot) if err != nil { return "", fmt.Errorf("cannot read %s: %w", projectsRoot, err) } for _, pd := range projectDirs { if !pd.IsDir() { continue } projectPath := filepath.Join(projectsRoot, pd.Name()) sessionDirs, err := os.ReadDir(projectPath) if err != nil { continue } for _, sd := range sessionDirs { if !sd.IsDir() { continue } if strings.HasPrefix(sd.Name(), sessionPrefix) { subagents := filepath.Join(projectPath, sd.Name(), "subagents") if info, err := os.Stat(subagents); err == nil && info.IsDir() { return subagents, nil } } } } return "", fmt.Errorf("no subagents directory found for session %q", sessionPrefix) } func main() { sessionFlag := flag.String("s", "", "Session ID prefix. Searches ~/.claude/projects for matching subagents directory.") listSessions := flag.Bool("ls", false, "List all sessions and exit.") flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: agent-tui [flags] [directory]\n\n") fmt.Fprintf(os.Stderr, "Tail Claude Code subagent JSONL logs with a terminal UI.\n\n") fmt.Fprintf(os.Stderr, " agent-tui -s 1f02 Find session by ID prefix\n") fmt.Fprintf(os.Stderr, " agent-tui ./subagents/ Watch a specific directory\n") fmt.Fprintf(os.Stderr, " agent-tui -ls ./subagents/ List sessions in directory\n\n") fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() } flag.Parse() var dir string if *sessionFlag != "" && flag.NArg() == 0 { // Session ID mode: find the subagents dir automatically found, err := findSubagentsDir(*sessionFlag) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } dir = found fmt.Fprintf(os.Stderr, "Found: %s\n", dir) } else { // Directory mode dir = "." if flag.NArg() > 0 { dir = flag.Arg(0) } // Verify directory exists info, err := os.Stat(dir) if err != nil || !info.IsDir() { fmt.Fprintf(os.Stderr, "Error: %q is not a valid directory\n", dir) os.Exit(1) } } // Discover sessions sessions, err := DiscoverSessions(dir) if err != nil { fmt.Fprintf(os.Stderr, "Error scanning sessions: %v\n", err) os.Exit(1) } if len(sessions) == 0 { fmt.Fprintf(os.Stderr, "No JSONL files found in %q\n", dir) os.Exit(1) } // List sessions mode if *listSessions { fmt.Printf("Sessions in %s:\n\n", dir) for i, s := range sessions { marker := " " if i == 0 { marker = "→ " } fmt.Printf("%s%-8s %d agents %4d events %s – %s\n", marker, shortSessionID(s.ID), s.AgentCount, s.EventCount, s.FirstSeen.Format("15:04:05"), s.LastSeen.Format("15:04:05"), ) for agentID, slug := range s.Agents { fmt.Printf(" agent %s (%s)\n", agentID[:min(7, len(agentID))], slug) } } fmt.Printf("\n(→ = most recent, used by default)\n") return } // Select session — when using -s to find the dir, there's typically // only one session per subagents dir, so just use the first activeSession := sessions[0].ID watcher := NewWatcher(dir) if err := watcher.Start(); err != nil { fmt.Fprintf(os.Stderr, "Error starting watcher: %v\n", err) os.Exit(1) } m := newModel(dir, sessions, activeSession) m.watcher = watcher // Load initial events in bulk (already sorted by timestamp) for _, evt := range watcher.InitialEvents() { m.handleNewEvent(evt) } p := tea.NewProgram( m, tea.WithAltScreen(), tea.WithMouseCellMotion(), ) if _, err := p.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } }