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:
177
render.go
Normal file
177
render.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
)
|
||||
|
||||
// renderMarkdown renders markdown text to styled terminal output at the given width.
|
||||
func renderMarkdown(text string, width int) string {
|
||||
if width < 20 {
|
||||
width = 20
|
||||
}
|
||||
r, err := glamour.NewTermRenderer(
|
||||
glamour.WithStandardStyle("dracula"),
|
||||
glamour.WithWordWrap(width),
|
||||
)
|
||||
if err != nil {
|
||||
return text
|
||||
}
|
||||
out, err := r.Render(text)
|
||||
if err != nil {
|
||||
return text
|
||||
}
|
||||
// Glamour adds leading/trailing newlines — trim them
|
||||
return strings.Trim(out, "\n")
|
||||
}
|
||||
|
||||
// renderEvent converts an Event into display lines for the viewport.
|
||||
func renderEvent(e *Event, agentColorIdx int, showTokens bool, width int) []string {
|
||||
timeStr := dimStyle.Render(e.parsed.time.Format("15:04:05"))
|
||||
agentTag := agentStyle(agentColorIdx).Render(e.AgentID[:min(7, len(e.AgentID))])
|
||||
|
||||
// Max width for detail text (account for timestamp + agent + icon + padding)
|
||||
detailWidth := width - 30
|
||||
if detailWidth < 40 {
|
||||
detailWidth = 40
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case "user":
|
||||
return renderUserEvent(e, timeStr, agentTag, agentColorIdx, detailWidth)
|
||||
case "assistant":
|
||||
return renderAssistantEvent(e, timeStr, agentTag, agentColorIdx, showTokens, detailWidth)
|
||||
case "progress":
|
||||
// Hidden by default — could add toggle later
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderUserEvent(e *Event, timeStr, agentTag string, colorIdx int, detailWidth int) []string {
|
||||
var msg messageEnvelope
|
||||
if err := json.Unmarshal(e.Message, &msg); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Continuation line prefix: spaces to align under the detail text
|
||||
contPrefix := " " + strings.Repeat(" ", 8+2+7+2+8) + " "
|
||||
|
||||
// Try as string first (teammate message or user text)
|
||||
var contentStr string
|
||||
if err := json.Unmarshal(msg.Content, &contentStr); err == nil {
|
||||
if strings.Contains(contentStr, "<teammate-message") {
|
||||
summary := extractTeammateSummary(contentStr)
|
||||
wrapped := wrapText(summary, detailWidth, contPrefix)
|
||||
lines := []string{
|
||||
fmt.Sprintf(" %s %s %s %s",
|
||||
timeStr, agentTag,
|
||||
taskStyle.Render("← TASK:"),
|
||||
wrapped[0]),
|
||||
}
|
||||
for _, cont := range wrapped[1:] {
|
||||
lines = append(lines, contPrefix+cont)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
wrapped := wrapText(contentStr, detailWidth, contPrefix)
|
||||
lines := []string{
|
||||
fmt.Sprintf(" %s %s %s %s",
|
||||
timeStr, agentTag,
|
||||
taskStyle.Render("← user:"),
|
||||
wrapped[0]),
|
||||
}
|
||||
for _, cont := range wrapped[1:] {
|
||||
lines = append(lines, contPrefix+cont)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// Tool results — skip, they're noise
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderAssistantEvent(e *Event, timeStr, agentTag string, colorIdx int, showTokens bool, detailWidth int) []string {
|
||||
var msg messageEnvelope
|
||||
if err := json.Unmarshal(e.Message, &msg); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var blocks []contentBlock
|
||||
if err := json.Unmarshal(msg.Content, &blocks); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Continuation line prefix: spaces to align under the detail text
|
||||
contPrefix := " " + strings.Repeat(" ", 8+2+7+2+4) + " "
|
||||
|
||||
var lines []string
|
||||
for _, b := range blocks {
|
||||
switch b.Type {
|
||||
case "text":
|
||||
text := strings.TrimSpace(b.Text)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Render markdown with glamour
|
||||
rendered := renderMarkdown(text, detailWidth)
|
||||
mdLines := strings.Split(rendered, "\n")
|
||||
|
||||
// First line gets the header (timestamp + agent + icon)
|
||||
header := fmt.Sprintf(" %s %s %s ",
|
||||
timeStr, agentTag,
|
||||
textStyle.Render("▪"))
|
||||
if len(mdLines) > 0 {
|
||||
lines = append(lines, header+mdLines[0])
|
||||
}
|
||||
for _, ml := range mdLines[1:] {
|
||||
lines = append(lines, contPrefix+ml)
|
||||
}
|
||||
|
||||
case "tool_use":
|
||||
icon := toolIcon(b.Name)
|
||||
detail := getToolDetail(b.Name, b.Input)
|
||||
detailStr := ""
|
||||
var contLines []string
|
||||
if detail != "" {
|
||||
toolDetailWidth := detailWidth - len(b.Name) - 5
|
||||
wrapped := wrapText(detail, toolDetailWidth, contPrefix)
|
||||
detailStr = " " + dimStyle.Render(wrapped[0])
|
||||
for _, cont := range wrapped[1:] {
|
||||
contLines = append(contLines, contPrefix+dimStyle.Render(cont))
|
||||
}
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf(" %s %s %s%s",
|
||||
timeStr, agentTag,
|
||||
toolStyle.Render(icon+" "+b.Name),
|
||||
detailStr))
|
||||
lines = append(lines, contLines...)
|
||||
}
|
||||
}
|
||||
|
||||
// Token usage
|
||||
if showTokens && msg.Usage != nil && msg.Usage.OutputTokens > 0 {
|
||||
parts := []string{}
|
||||
if msg.Usage.OutputTokens > 0 {
|
||||
parts = append(parts, fmt.Sprintf("out:%d", msg.Usage.OutputTokens))
|
||||
}
|
||||
if msg.Usage.CacheReadInputTokens > 0 {
|
||||
parts = append(parts, fmt.Sprintf("cache_r:%d", msg.Usage.CacheReadInputTokens))
|
||||
}
|
||||
if msg.Usage.CacheCreationInputTokens > 0 {
|
||||
parts = append(parts, fmt.Sprintf("cache_w:%d", msg.Usage.CacheCreationInputTokens))
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
lines = append(lines, fmt.Sprintf(" %s %s %s",
|
||||
dimStyle.Render(" "),
|
||||
agentStyle(colorIdx).Render(e.AgentID[:min(7, len(e.AgentID))]),
|
||||
dimStyle.Render("tokens: "+strings.Join(parts, " | "))))
|
||||
}
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
Reference in New Issue
Block a user