Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions internal/hooks/postcommit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"log/slog"
"os"
"path/filepath"
"strings"
"time"

"github.com/partio-io/cli/internal/agent/claude"
"github.com/partio-io/cli/internal/attribution"
"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.
Expand Down Expand Up @@ -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
}
66 changes: 66 additions & 0 deletions internal/session/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
)

// Manager handles session lifecycle transitions.
Expand All @@ -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
}
16 changes: 9 additions & 7 deletions internal/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down