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, " 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 }