package session import ( "fmt" "html" "os" "strings" "time" ) // RenderHTML generates a self-contained HTML timeline from a session. func RenderHTML(sess *Session, outputPath string) error { f, err := os.Create(outputPath) if err != nil { return fmt.Errorf("create html: %w", err) } defer f.Close() duration := sess.EndTime.Sub(sess.StartTime) toolCount := 0 errorCount := 0 for _, e := range sess.Events { if e.Type == "tool_use" { toolCount++ if !e.Success { errorCount++ } } } fmt.Fprintf(f, ` Session %s

Session %s

%s Duration: %s %d tool calls`, shortID(sess.ID), shortID(sess.ID), sess.StartTime.Format("2006-01-02 15:04:05"), formatDuration(duration), toolCount) if errorCount > 0 { fmt.Fprintf(f, ` %d errors`, errorCount) } fmt.Fprintf(f, `
`) for i, evt := range sess.Events { toolClass := strings.ToLower(evt.Tool) if evt.Type == "user" { toolClass = "user" } else if evt.Type == "assistant" { toolClass = "assistant" } errorClass := "" if !evt.Success && evt.Type == "tool_use" { errorClass = " error" } statusIcon := "" if evt.Type == "tool_use" { if evt.Success { statusIcon = `` } else { statusIcon = `` } } toolLabel := evt.Tool if evt.Type == "user" { toolLabel = "User" } else if evt.Type == "assistant" { toolLabel = "Claude" } durStr := "" if evt.Duration > 0 { durStr = formatDuration(evt.Duration) } fmt.Fprintf(f, `
%s %s %s %s %s
`, errorClass, evt.Type, evt.Tool, html.EscapeString(strings.ToLower(evt.Input+" "+evt.Output)), i, i, evt.Timestamp.Format("15:04:05"), toolClass, html.EscapeString(toolLabel), html.EscapeString(truncate(evt.Input, 120)), durStr, statusIcon) if evt.Input != "" { label := "Command" if evt.Type == "user" { label = "Message" } else if evt.Type == "assistant" { label = "Response" } else if evt.Tool == "Read" || evt.Tool == "Glob" || evt.Tool == "Grep" { label = "Target" } else if evt.Tool == "Edit" || evt.Tool == "Write" { label = "File" } fmt.Fprintf(f, `
%s
%s
`, label, html.EscapeString(evt.Input)) } if evt.Output != "" { outClass := "output" if !evt.Success { outClass = "output err" } fmt.Fprintf(f, `
Output
%s
`, outClass, html.EscapeString(evt.Output)) } fmt.Fprint(f, `
`) } fmt.Fprint(f, `
`) return nil } func shortID(id string) string { if len(id) > 8 { return id[:8] } return id } func formatDuration(d time.Duration) string { if d < time.Second { return fmt.Sprintf("%dms", d.Milliseconds()) } if d < time.Minute { return fmt.Sprintf("%.1fs", d.Seconds()) } if d < time.Hour { return fmt.Sprintf("%dm%ds", int(d.Minutes()), int(d.Seconds())%60) } return fmt.Sprintf("%dh%dm", int(d.Hours()), int(d.Minutes())%60) }