loop: add diff stats from sketch-base to HEAD in /state endpoint
Add lines added/removed statistics computed from sketch-base to current HEAD,
displayed in webui Diff mode button for quick change overview.
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s3f10ecf39df6b581k
diff --git a/loop/agent.go b/loop/agent.go
index a4d2505..561e986 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -130,6 +130,9 @@
OutsideHostname() string
OutsideWorkingDir() string
GitOrigin() string
+
+ // DiffStats returns the number of lines added and removed from sketch-base to HEAD
+ DiffStats() (int, int)
// OpenBrowser is a best-effort attempt to open a browser at url in outside sketch.
OpenBrowser(url string)
@@ -334,6 +337,8 @@
seenCommits map[string]bool // Track git commits we've already seen (by hash)
slug string // Human-readable session identifier
retryNumber int // Number to append when branch conflicts occur
+ linesAdded int // Lines added from sketch-base to HEAD
+ linesRemoved int // Lines removed from sketch-base to HEAD
}
func (ags *AgentGitState) SetSlug(slug string) {
@@ -357,6 +362,12 @@
ags.retryNumber++
}
+func (ags *AgentGitState) DiffStats() (int, int) {
+ ags.mu.Lock()
+ defer ags.mu.Unlock()
+ return ags.linesAdded, ags.linesRemoved
+}
+
// HasSeenCommits returns true if any commits have been processed
func (ags *AgentGitState) HasSeenCommits() bool {
ags.mu.Lock()
@@ -706,6 +717,11 @@
return a.gitOrigin
}
+// DiffStats returns the number of lines added and removed from sketch-base to HEAD
+func (a *Agent) DiffStats() (int, int) {
+ return a.gitState.DiffStats()
+}
+
func (a *Agent) OpenBrowser(url string) {
if !a.IsInContainer() {
browser.Open(url)
@@ -1903,6 +1919,16 @@
ags.lastSketch = sketch
}()
+ // Compute diff stats from baseRef to HEAD when HEAD changes
+ if added, removed, err := computeDiffStats(ctx, repoRoot, baseRef); err != nil {
+ // Log error but don't fail the entire operation
+ slog.WarnContext(ctx, "Failed to compute diff stats", "error", err)
+ } else {
+ // Set diff stats directly since we already hold the mutex
+ ags.linesAdded = added
+ ags.linesRemoved = removed
+ }
+
// Get new commits. Because it's possible that the agent does rebases, fixups, and
// so forth, we use, as our fixed point, the "initialCommit", and we limit ourselves
// to the last 100 commits.
@@ -2121,6 +2147,37 @@
return true
}
+// computeDiffStats computes the number of lines added and removed from baseRef to HEAD
+func computeDiffStats(ctx context.Context, repoRoot, baseRef string) (int, int, error) {
+ cmd := exec.CommandContext(ctx, "git", "diff", "--numstat", baseRef, "HEAD")
+ cmd.Dir = repoRoot
+ out, err := cmd.Output()
+ if err != nil {
+ return 0, 0, fmt.Errorf("git diff --numstat failed: %w", err)
+ }
+
+ var totalAdded, totalRemoved int
+ lines := strings.Split(strings.TrimSpace(string(out)), "\n")
+ for _, line := range lines {
+ if line == "" {
+ continue
+ }
+ parts := strings.Fields(line)
+ if len(parts) < 2 {
+ continue
+ }
+ // Format: <added>\t<removed>\t<filename>
+ if added, err := strconv.Atoi(parts[0]); err == nil {
+ totalAdded += added
+ }
+ if removed, err := strconv.Atoi(parts[1]); err == nil {
+ totalRemoved += removed
+ }
+ }
+
+ return totalAdded, totalRemoved, nil
+}
+
// getGitOrigin returns the URL of the git remote 'origin' if it exists
func getGitOrigin(ctx context.Context, dir string) string {
cmd := exec.CommandContext(ctx, "git", "config", "--get", "remote.origin.url")
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 609b546..3b93d3e 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -99,6 +99,8 @@
SkabandAddr string `json:"skaband_addr,omitempty"` // URL of the skaband server
LinkToGitHub bool `json:"link_to_github,omitempty"` // Enable GitHub branch linking in UI
SSHConnectionString string `json:"ssh_connection_string,omitempty"` // SSH connection string for container
+ DiffLinesAdded int `json:"diff_lines_added"` // Lines added from sketch-base to HEAD
+ DiffLinesRemoved int `json:"diff_lines_removed"` // Lines removed from sketch-base to HEAD
}
type InitRequest struct {
@@ -1279,6 +1281,9 @@
serverMessageCount := s.agent.MessageCount()
totalUsage := s.agent.TotalUsage()
+ // Get diff stats
+ diffAdded, diffRemoved := s.agent.DiffStats()
+
return State{
StateVersion: 2,
MessageCount: serverMessageCount,
@@ -1310,6 +1315,8 @@
SkabandAddr: s.agent.SkabandAddr(),
LinkToGitHub: s.agent.LinkToGitHub(),
SSHConnectionString: s.agent.SSHConnectionString(),
+ DiffLinesAdded: diffAdded,
+ DiffLinesRemoved: diffRemoved,
}
}
diff --git a/loop/server/loophttp_test.go b/loop/server/loophttp_test.go
index 531f961..d93c360 100644
--- a/loop/server/loophttp_test.go
+++ b/loop/server/loophttp_test.go
@@ -262,6 +262,7 @@
func (m *mockAgent) GetPortMonitor() *loop.PortMonitor { return loop.NewPortMonitor() }
func (m *mockAgent) SkabandAddr() string { return m.skabandAddr }
func (m *mockAgent) LinkToGitHub() bool { return false }
+func (m *mockAgent) DiffStats() (int, int) { return 0, 0 }
// TestSSEStream tests the SSE stream endpoint
func TestSSEStream(t *testing.T) {