feat: session lifecycle wrapper with manifest capture and session logging
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>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user