// SPDX-Licence-Identifier: EUPL-1.2 package session import ( "html" "path" "time" core "dappco.re/go/core" ) // RenderHTML generates a self-contained HTML timeline from a session. // // Example: // err := session.RenderHTML(sess, "/tmp/session.html") func RenderHTML(sess *Session, outputPath string) error { if !hostFS.IsDir(path.Dir(outputPath)) { return core.E("RenderHTML", "parent directory does not exist", nil) } duration := sess.EndTime.Sub(sess.StartTime) toolCount := 0 errorCount := 0 for e := range sess.EventsSeq() { if e.Type == "tool_use" { toolCount++ if !e.Success { errorCount++ } } } b := core.NewBuilder() b.WriteString(core.Sprintf(` 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 { b.WriteString(core.Sprintf(` %d errors`, errorCount)) } b.WriteString(`
`) var i int for evt := range sess.EventsSeq() { toolClass := core.Lower(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) } b.WriteString(core.Sprintf(`
%s %s %s %s %s
`, errorClass, evt.Type, evt.Tool, html.EscapeString(core.Lower(core.Concat(evt.Input, " ", evt.Output))), i, i, evt.Timestamp.Format("15:04:05"), toolClass, html.EscapeString(toolLabel), html.EscapeString(truncate(evt.Input, 120)), durStr, statusIcon, i)) 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" } b.WriteString(core.Sprintf(`
%s
%s
`, label, html.EscapeString(evt.Input))) } if evt.Output != "" { outClass := "output" if !evt.Success { outClass = "output err" } b.WriteString(core.Sprintf(`
Output
%s
`, outClass, html.EscapeString(evt.Output))) } b.WriteString(`
`) i++ } b.WriteString(`
`) writeResult := hostFS.Write(outputPath, b.String()) if !writeResult.OK { return core.E("RenderHTML", "write html", resultError(writeResult)) } 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 core.Sprintf("%dms", d.Milliseconds()) } if d < time.Minute { return core.Sprintf("%.1fs", d.Seconds()) } if d < time.Hour { return core.Sprintf("%dm%ds", int(d.Minutes()), int(d.Seconds())%60) } return core.Sprintf("%dh%dm", int(d.Hours()), int(d.Minutes())%60) }