10ab9efc80
Refactor new/resume/open to use session.Run() which wraps child process launch with pre/post manifest capture and append-only session logging to logs/sessions.log. Bump version to 0.2.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
123 lines
3.0 KiB
Go
123 lines
3.0 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
|
|
}
|