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 }