From 7c68cd6159dd52d750112fd17b44f4437d86f99c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:21:18 +0000 Subject: [PATCH 1/3] Initial plan From 1071ae088ec3af32a3644d88ac8190615159bd32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:26:29 +0000 Subject: [PATCH 2/3] Fix ETA display during initial sync by reducing warmup period for clones Co-authored-by: Dids <3455449+Dids@users.noreply.github.com> --- internal/tui/screens/sync_progress.go | 12 +- internal/tui/screens/sync_progress_test.go | 127 +++++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/internal/tui/screens/sync_progress.go b/internal/tui/screens/sync_progress.go index 6bf2382..f360b89 100644 --- a/internal/tui/screens/sync_progress.go +++ b/internal/tui/screens/sync_progress.go @@ -327,9 +327,15 @@ func (s *SyncProgressScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) { s.lastETADoneCount = done // Only calculate ETA after warmup period - minSamples := min(10, s.totalRepos/10) - if minSamples < 3 { - minSamples = 3 + // For initial sync (mostly clones), we can show ETA sooner since each operation takes longer + // For incremental sync (mostly updates), wait for more samples for accuracy + minSamples := 1 // Start with 1 for initial sync + if s.cloned == 0 && done > 0 { + // Incremental sync (no clones yet) - use more conservative warmup + minSamples = min(10, s.totalRepos/10) + if minSamples < 3 { + minSamples = 3 + } } if done >= minSamples { diff --git a/internal/tui/screens/sync_progress_test.go b/internal/tui/screens/sync_progress_test.go index a3d28da..ba28ec5 100644 --- a/internal/tui/screens/sync_progress_test.go +++ b/internal/tui/screens/sync_progress_test.go @@ -559,3 +559,130 @@ func TestTeaModelInterface(t *testing.T) { // This is a compile-time check that SyncProgressScreen implements tea.Model var _ tea.Model = (*SyncProgressScreen)(nil) } + +func TestETACalculationDuringInitialSync(t *testing.T) { + // Test that ETA is calculated and displayed during initial sync (clones) + // after just 1 repository completes + + t.Run("ETA calculated after 1 clone completes", func(t *testing.T) { + screen := &SyncProgressScreen{ + totalRepos: 10, + cloned: 1, // First clone completed + updated: 0, + upToDate: 0, + reposCompleted: 1, + lastETADoneCount: 0, + lastDisplayedETA: 0, + startTime: time.Now().Add(-10 * time.Second), // Started 10 seconds ago + } + + done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + + // Simulate the ETA calculation logic for initial sync + if done > screen.lastETADoneCount { + screen.lastETADoneCount = done + + // Initial sync logic (cloned > 0 means we're doing clones) + minSamples := 1 // For initial sync with clones + if screen.cloned == 0 && done > 0 { + // Incremental sync - more conservative + minSamples = 3 + } + + if done >= minSamples { + elapsed := time.Since(screen.startTime) + avgPerRepo := elapsed / time.Duration(done) + remaining := screen.totalRepos - done + newETA := avgPerRepo * time.Duration(remaining) + screen.lastDisplayedETA = newETA + } + } + + // Verify ETA was calculated + assert.Greater(t, screen.lastDisplayedETA, time.Duration(0), "ETA should be calculated after 1 clone") + assert.Equal(t, 1, screen.lastETADoneCount, "lastETADoneCount should be updated") + + // Verify ETA is reasonable (should be roughly 90 seconds for 9 remaining repos at 10s/repo) + expectedETA := 90 * time.Second + assert.InDelta(t, expectedETA.Seconds(), screen.lastDisplayedETA.Seconds(), 5.0, + "ETA should be approximately 90 seconds") + }) + + t.Run("ETA not calculated until 3 updates complete for incremental sync", func(t *testing.T) { + screen := &SyncProgressScreen{ + totalRepos: 10, + cloned: 0, // No clones, incremental sync + updated: 2, // Only 2 updates so far + upToDate: 0, + reposCompleted: 2, + lastETADoneCount: 0, + lastDisplayedETA: 0, + startTime: time.Now().Add(-2 * time.Second), + } + + done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + + // Simulate the ETA calculation logic + if done > screen.lastETADoneCount { + screen.lastETADoneCount = done + + // Incremental sync logic (no clones) + minSamples := 1 + if screen.cloned == 0 && done > 0 { + // Incremental sync - more conservative + minSamples = 3 + } + + if done >= minSamples { + elapsed := time.Since(screen.startTime) + avgPerRepo := elapsed / time.Duration(done) + remaining := screen.totalRepos - done + newETA := avgPerRepo * time.Duration(remaining) + screen.lastDisplayedETA = newETA + } + } + + // Verify ETA was NOT calculated (need 3 samples for incremental sync) + assert.Equal(t, time.Duration(0), screen.lastDisplayedETA, + "ETA should not be calculated with only 2 updates in incremental sync") + assert.Equal(t, 2, screen.lastETADoneCount, "lastETADoneCount should still be updated") + }) + + t.Run("ETA calculated after 3 updates in incremental sync", func(t *testing.T) { + screen := &SyncProgressScreen{ + totalRepos: 10, + cloned: 0, // No clones, incremental sync + updated: 3, // 3 updates completed + upToDate: 0, + reposCompleted: 3, + lastETADoneCount: 0, + lastDisplayedETA: 0, + startTime: time.Now().Add(-6 * time.Second), + } + + done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + + // Simulate the ETA calculation logic + if done > screen.lastETADoneCount { + screen.lastETADoneCount = done + + // Incremental sync logic + minSamples := 1 + if screen.cloned == 0 && done > 0 { + minSamples = 3 + } + + if done >= minSamples { + elapsed := time.Since(screen.startTime) + avgPerRepo := elapsed / time.Duration(done) + remaining := screen.totalRepos - done + newETA := avgPerRepo * time.Duration(remaining) + screen.lastDisplayedETA = newETA + } + } + + // Verify ETA was calculated + assert.Greater(t, screen.lastDisplayedETA, time.Duration(0), + "ETA should be calculated after 3 updates in incremental sync") + }) +} From 8891c42d0c9f2611be3cb122345dd0fa60a14f97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:29:42 +0000 Subject: [PATCH 3/3] Fix progress calculation to include archived repos in done count Co-authored-by: Dids <3455449+Dids@users.noreply.github.com> --- internal/tui/screens/sync_progress.go | 4 ++-- internal/tui/screens/sync_progress_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/tui/screens/sync_progress.go b/internal/tui/screens/sync_progress.go index f360b89..db7dafc 100644 --- a/internal/tui/screens/sync_progress.go +++ b/internal/tui/screens/sync_progress.go @@ -318,7 +318,7 @@ func (s *SyncProgressScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Update progress bar and ETA (skip for complete status - handled above) if s.totalRepos > 0 { - done := s.cloned + s.updated + s.upToDate + s.skipped + s.failed + done := s.cloned + s.updated + s.upToDate + s.skipped + s.failed + s.archived cmds = append(cmds, s.progress.SetPercent(float64(done)/float64(s.totalRepos))) // Recalculate ETA only when done count actually changes (repo completed) @@ -395,7 +395,7 @@ func (s *SyncProgressScreen) View() string { // Progress total := s.totalRepos - done := s.cloned + s.updated + s.upToDate + s.skipped + s.failed + done := s.cloned + s.updated + s.upToDate + s.skipped + s.failed + s.archived if total > 0 { pct := float64(done) / float64(total) content.WriteString(s.progress.ViewAs(pct)) diff --git a/internal/tui/screens/sync_progress_test.go b/internal/tui/screens/sync_progress_test.go index ba28ec5..31fb0ab 100644 --- a/internal/tui/screens/sync_progress_test.go +++ b/internal/tui/screens/sync_progress_test.go @@ -576,7 +576,7 @@ func TestETACalculationDuringInitialSync(t *testing.T) { startTime: time.Now().Add(-10 * time.Second), // Started 10 seconds ago } - done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + screen.archived // Simulate the ETA calculation logic for initial sync if done > screen.lastETADoneCount { @@ -620,7 +620,7 @@ func TestETACalculationDuringInitialSync(t *testing.T) { startTime: time.Now().Add(-2 * time.Second), } - done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + screen.archived // Simulate the ETA calculation logic if done > screen.lastETADoneCount { @@ -660,7 +660,7 @@ func TestETACalculationDuringInitialSync(t *testing.T) { startTime: time.Now().Add(-6 * time.Second), } - done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + screen.archived // Simulate the ETA calculation logic if done > screen.lastETADoneCount {