From d36ce5155968b318d0ecd3d3b068699410deb332 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 10 Jul 2026 21:40:51 +0200 Subject: [PATCH] Capture suggestions inputs on the UI thread RefreshSuggestions dispatched to an AsyncHandler worker that read State.FindSuggestions and the prompt's TextArea (via GetPromptInput) from the worker goroutine. The main thread rewrites both in preparePromptPanel when it (re)creates a prompt panel, so an in-flight suggestions worker races those writes -- two data races surfaced under -race (filter_by_path/reword_commit_in_filtering_mode). Capture both on the UI thread (RefreshSuggestions is only ever called from UI-thread handlers) before dispatching to the worker. This is also more correct: we search for the input as it was when dispatched, which is what this request's AsyncHandler id corresponds to. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/gui/context/suggestions_context.go | 11 +++++++++-- pkg/gui/editors.go | 7 +++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index fb69b34d9..6f0b3eae6 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -81,10 +81,17 @@ func (self *SuggestionsContext) SetSuggestions(suggestions []*types.Suggestion) } func (self *SuggestionsContext) RefreshSuggestions() { + // Capture the suggestions function and the prompt input here, on the UI + // thread, rather than inside the worker below: the main thread rewrites both + // (State.FindSuggestions and the prompt's TextArea) when it (re)creates a + // prompt panel, so reading them from the worker races those writes. It's + // also more correct -- we search for the input as it was when dispatched, + // which is what this request's AsyncHandler id corresponds to. + findSuggestionsFn := self.State.FindSuggestions + promptInput := self.c.GetPromptInput() self.State.AsyncHandler.Do(func() func() { - findSuggestionsFn := self.State.FindSuggestions if findSuggestionsFn != nil { - suggestions := findSuggestionsFn(self.c.GetPromptInput()) + suggestions := findSuggestionsFn(promptInput) return func() { self.SetSuggestions(suggestions) } } return func() {} diff --git a/pkg/gui/editors.go b/pkg/gui/editors.go index 7d3a93de3..37eacf416 100644 --- a/pkg/gui/editors.go +++ b/pkg/gui/editors.go @@ -35,10 +35,13 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key) bool { v.RenderTextArea() suggestionsContext := gui.State.Contexts.Suggestions - if suggestionsContext.State.FindSuggestions != nil { + // Capture the suggestions function and the input here, on the UI thread; the + // main thread rewrites State.FindSuggestions when it (re)creates a prompt + // panel, so reading it from the worker below would race that write. + if findSuggestions := suggestionsContext.State.FindSuggestions; findSuggestions != nil { input := v.TextArea.GetContent() suggestionsContext.State.AsyncHandler.Do(func() func() { - suggestions := suggestionsContext.State.FindSuggestions(input) + suggestions := findSuggestions(input) return func() { suggestionsContext.SetSuggestions(suggestions) } }) }