Files
ctask/internal/session/log.go
T
typebasedio 75911faeeb fix: replace all non-ASCII characters with safe ASCII equivalents
Replace box-drawing characters (U+2500) in session log with ASCII dashes.
Replace em dashes (U+2014) in CLAUDE.md template with double hyphens.
Remove em dash from comment in run.go.
Add ASCII-guard tests for session log output and seed templates.
Prevents mojibake on Windows terminals that misinterpret UTF-8 as CP1252.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:15:02 -04:00

123 lines
2.9 KiB
Go

package session
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// SessionInfo holds data for a session log entry.
type SessionInfo struct {
Agent string
Mode string
StartTime time.Time
EndTime time.Time
Diff *ManifestDiff
}
// FormatDuration returns a human-readable duration string like "1h 14m 48s".
func FormatDuration(d time.Duration) string {
h := int(d.Hours())
m := int(d.Minutes()) % 60
s := int(d.Seconds()) % 60
if h > 0 {
return fmt.Sprintf("%dh %dm %ds", h, m, s)
}
if m > 0 {
return fmt.Sprintf("%dm %ds", m, s)
}
return fmt.Sprintf("%ds", s)
}
// FormatSessionEntry formats a session log entry as a human-readable string.
func FormatSessionEntry(info *SessionInfo) string {
var b strings.Builder
duration := info.EndTime.Sub(info.StartTime)
timeFmt := "2006-01-02 15:04:05"
fmt.Fprintf(&b, "-- Session %s --\n", info.StartTime.Format(timeFmt))
fmt.Fprintf(&b, "Agent: %s\n", info.Agent)
fmt.Fprintf(&b, "Mode: %s\n", info.Mode)
fmt.Fprintf(&b, "Start: %s\n", info.StartTime.Format(timeFmt))
fmt.Fprintf(&b, "End: %s\n", info.EndTime.Format(timeFmt))
fmt.Fprintf(&b, "Duration: %s\n", FormatDuration(duration))
b.WriteString("\nAdded:\n")
if len(info.Diff.Added) > 0 {
sorted := make([]string, len(info.Diff.Added))
copy(sorted, info.Diff.Added)
sort.Strings(sorted)
for _, f := range sorted {
fmt.Fprintf(&b, " %s\n", f)
}
} else {
b.WriteString(" (none)\n")
}
b.WriteString("\nModified:\n")
if len(info.Diff.Modified) > 0 {
sorted := make([]string, len(info.Diff.Modified))
copy(sorted, info.Diff.Modified)
sort.Strings(sorted)
for _, f := range sorted {
fmt.Fprintf(&b, " %s\n", f)
}
} else {
b.WriteString(" (none)\n")
}
b.WriteString("\nDeleted:\n")
if len(info.Diff.Deleted) > 0 {
sorted := make([]string, len(info.Diff.Deleted))
copy(sorted, info.Diff.Deleted)
sort.Strings(sorted)
for _, f := range sorted {
fmt.Fprintf(&b, " %s\n", f)
}
} else {
b.WriteString(" (none)\n")
}
notesChanged := NotesUpdated(info.Diff)
if notesChanged {
b.WriteString("\nNotes updated: yes\n")
} else {
b.WriteString("\nNotes updated: no\n")
}
// Short session with no changes note
noChanges := len(info.Diff.Added) == 0 && len(info.Diff.Modified) == 0 && len(info.Diff.Deleted) == 0
if duration < 5*time.Second && noChanges {
b.WriteString("(No changes detected)\n")
}
b.WriteString("--------------------------------\n")
return b.String()
}
// AppendSessionLog appends a session entry to logs/sessions.log.
func AppendSessionLog(wsDir string, info *SessionInfo) error {
logDir := filepath.Join(wsDir, "logs")
if err := os.MkdirAll(logDir, 0755); err != nil {
return err
}
logPath := filepath.Join(logDir, "sessions.log")
entry := FormatSessionEntry(info)
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString("\n" + entry)
return err
}