From 7bb481360a928bce85f9aacbf87370629eb715b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 05:16:03 +0000 Subject: [PATCH 1/4] Initial plan From f176ffd8235f8d1c45a004218ac65e9ea41c16b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 05:21:45 +0000 Subject: [PATCH 2/4] Recover spinner goroutine panics Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/console/spinner.go | 8 ++++++ pkg/console/spinner_test.go | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/pkg/console/spinner.go b/pkg/console/spinner.go index 8b902efc877..a3a509d1798 100644 --- a/pkg/console/spinner.go +++ b/pkg/console/spinner.go @@ -139,6 +139,14 @@ func (s *SpinnerWrapper) Start() { spinnerLog.Print("Starting spinner") go func() { defer s.wg.Done() + defer func() { + if r := recover(); r != nil { + spinnerLog.Printf("Panic in spinner program (recovered): %v", r) + } + s.mu.Lock() + s.running = false + s.mu.Unlock() + }() _, _ = s.program.Run() }() } diff --git a/pkg/console/spinner_test.go b/pkg/console/spinner_test.go index 1766fd13a97..0d0f382ec9c 100644 --- a/pkg/console/spinner_test.go +++ b/pkg/console/spinner_test.go @@ -3,9 +3,12 @@ package console import ( + "io" "os" "testing" "time" + + tea "charm.land/bubbletea/v2" ) func TestNewSpinner(t *testing.T) { @@ -200,6 +203,58 @@ func TestSpinnerStopWithoutStart(t *testing.T) { spinner.StopWithMessage("Message") } +type panicInitModel struct{} + +func (panicInitModel) Init() tea.Cmd { + panic("spinner test panic") +} + +func (m panicInitModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + return m, nil +} + +func (panicInitModel) View() tea.View { return tea.View{} } + +func TestSpinnerStartRecoversFromProgramPanic(t *testing.T) { + newPanicProgram := func() *tea.Program { + return tea.NewProgram( + panicInitModel{}, + tea.WithOutput(io.Discard), + tea.WithoutRenderer(), + tea.WithInput(nil), + ) + } + + spinner := &SpinnerWrapper{ + enabled: true, + program: newPanicProgram(), + } + + spinner.Start() + waitForSpinnerStopped(t, spinner) + + spinner.program = newPanicProgram() + spinner.Start() + waitForSpinnerStopped(t, spinner) +} + +func waitForSpinnerStopped(t *testing.T, spinner *SpinnerWrapper) { + t.Helper() + + deadline := time.Now().Add(time.Second) + for time.Now().Before(deadline) { + spinner.mu.Lock() + running := spinner.running + spinner.mu.Unlock() + if !running { + return + } + time.Sleep(10 * time.Millisecond) + } + + t.Fatal("spinner did not reset running state after panic") +} + // TestSpinnerStopBeforeStartRaceCondition tests that calling Stop immediately // after Start (before the goroutine initializes) does not cause a deadlock. // This reproduces the issue from https://github.com/github/gh-aw/issues/XXX From 80f9ad33aafed2d226d0ac7f9f9f4fe5c779b057 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 05:22:27 +0000 Subject: [PATCH 3/4] Clarify spinner running-state reset Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/console/spinner.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/console/spinner.go b/pkg/console/spinner.go index a3a509d1798..7b0be8e944f 100644 --- a/pkg/console/spinner.go +++ b/pkg/console/spinner.go @@ -142,12 +142,15 @@ func (s *SpinnerWrapper) Start() { defer func() { if r := recover(); r != nil { spinnerLog.Printf("Panic in spinner program (recovered): %v", r) + s.mu.Lock() + s.running = false + s.mu.Unlock() } - s.mu.Lock() - s.running = false - s.mu.Unlock() }() _, _ = s.program.Run() + s.mu.Lock() + s.running = false + s.mu.Unlock() }() } } From 21d4a40dc160bdf995c17d4db09a866288e53e57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 9 May 2026 05:23:08 +0000 Subject: [PATCH 4/4] Tighten spinner cleanup flow Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/console/spinner.go | 11 +++++------ pkg/console/spinner_test.go | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pkg/console/spinner.go b/pkg/console/spinner.go index 7b0be8e944f..1d8d6adaf2b 100644 --- a/pkg/console/spinner.go +++ b/pkg/console/spinner.go @@ -139,18 +139,17 @@ func (s *SpinnerWrapper) Start() { spinnerLog.Print("Starting spinner") go func() { defer s.wg.Done() + defer func() { + s.mu.Lock() + s.running = false + s.mu.Unlock() + }() defer func() { if r := recover(); r != nil { spinnerLog.Printf("Panic in spinner program (recovered): %v", r) - s.mu.Lock() - s.running = false - s.mu.Unlock() } }() _, _ = s.program.Run() - s.mu.Lock() - s.running = false - s.mu.Unlock() }() } } diff --git a/pkg/console/spinner_test.go b/pkg/console/spinner_test.go index 0d0f382ec9c..e3f0ab2ea98 100644 --- a/pkg/console/spinner_test.go +++ b/pkg/console/spinner_test.go @@ -244,11 +244,11 @@ func waitForSpinnerStopped(t *testing.T, spinner *SpinnerWrapper) { deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) { spinner.mu.Lock() - running := spinner.running - spinner.mu.Unlock() - if !running { + if !spinner.running { + spinner.mu.Unlock() return } + spinner.mu.Unlock() time.Sleep(10 * time.Millisecond) }