diff --git a/internal/hooks/postcommit.go b/internal/hooks/postcommit.go index 0cd65b1..ee395b7 100644 --- a/internal/hooks/postcommit.go +++ b/internal/hooks/postcommit.go @@ -6,6 +6,7 @@ import ( "log/slog" "os" "path/filepath" + "strings" "time" "github.com/partio-io/cli/internal/agent/claude" @@ -13,6 +14,7 @@ import ( "github.com/partio-io/cli/internal/checkpoint" "github.com/partio-io/cli/internal/config" "github.com/partio-io/cli/internal/git" + "github.com/partio-io/cli/internal/session" ) // PostCommit runs post-commit hook logic. @@ -141,6 +143,28 @@ func runPostCommit(repoRoot string, cfg config.Config) error { return fmt.Errorf("writing checkpoint: %w", err) } + // Update session activity: count files changed in this commit + numstat, _ := git.DiffNumstat(commitHash) + filesChanged := countNumstatFiles(numstat) + mgr := session.NewManager(filepath.Join(repoRoot, config.PartioDir)) + if err := mgr.RecordActivity(filesChanged); err != nil { + slog.Warn("could not update session activity", "error", err) + } + slog.Debug("checkpoint created", "id", cpID, "agent_pct", attr.AgentPercent) return nil } + +// countNumstatFiles counts the number of files from git diff --numstat output. +func countNumstatFiles(numstat string) int { + if numstat == "" { + return 0 + } + count := 0 + for _, line := range strings.Split(numstat, "\n") { + if strings.TrimSpace(line) != "" { + count++ + } + } + return count +} diff --git a/internal/session/manager.go b/internal/session/manager.go index bf9a7c0..6a92d89 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -5,6 +5,9 @@ import ( "fmt" "os" "path/filepath" + "sort" + "strings" + "time" ) // Manager handles session lifecycle transitions. @@ -24,9 +27,72 @@ func (m *Manager) save(s *Session) error { if err != nil { return fmt.Errorf("marshaling session: %w", err) } + if err := os.WriteFile(m.idPath(s.ID), data, 0o644); err != nil { + return fmt.Errorf("saving session by id: %w", err) + } return os.WriteFile(m.currentPath(), data, 0o644) } func (m *Manager) currentPath() string { return filepath.Join(m.stateDir, "current.json") } + +func (m *Manager) idPath(id string) string { + return filepath.Join(m.stateDir, id+".json") +} + +// List returns all sessions sorted by most recent activity, newest first. +func (m *Manager) List() ([]*Session, error) { + entries, err := os.ReadDir(m.stateDir) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("reading sessions dir: %w", err) + } + + var sessions []*Session + for _, e := range entries { + if e.IsDir() || e.Name() == "current.json" || !strings.HasSuffix(e.Name(), ".json") { + continue + } + data, err := os.ReadFile(filepath.Join(m.stateDir, e.Name())) + if err != nil { + continue + } + var s Session + if err := json.Unmarshal(data, &s); err != nil { + continue + } + sessions = append(sessions, &s) + } + + sort.Slice(sessions, func(i, j int) bool { + ti := latestTime(sessions[i]) + tj := latestTime(sessions[j]) + return ti.After(tj) + }) + + return sessions, nil +} + +// RecordActivity updates a session's last activity time and cumulative files modified count. +func (m *Manager) RecordActivity(filesChanged int) error { + s, err := m.Current() + if err != nil || s == nil { + return err + } + s.UpdatedAt = time.Now() + s.FilesModified += filesChanged + return m.save(s) +} + +func latestTime(s *Session) time.Time { + if !s.EndedAt.IsZero() { + return s.EndedAt + } + if !s.UpdatedAt.IsZero() { + return s.UpdatedAt + } + return s.StartedAt +} diff --git a/internal/session/session.go b/internal/session/session.go index 67c234c..615e2a8 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -8,13 +8,15 @@ import ( // Session represents an AI agent coding session. type Session struct { - ID string `json:"id"` - Agent string `json:"agent"` - State State `json:"state"` - StartedAt time.Time `json:"started_at"` - EndedAt time.Time `json:"ended_at,omitempty"` - Branch string `json:"branch"` - SourceDir string `json:"source_dir"` + ID string `json:"id"` + Agent string `json:"agent"` + State State `json:"state"` + StartedAt time.Time `json:"started_at"` + EndedAt time.Time `json:"ended_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + Branch string `json:"branch"` + SourceDir string `json:"source_dir"` + FilesModified int `json:"files_modified,omitempty"` } // New creates a new session with a generated UUID.