Derive sync vs async refresh from the calling thread

Whether a refresh should block or run in the background was controlled
by the Mode field, but that always lined up with the calling thread: a
UI-thread Refresh must not block the UI, while a RefreshFromWorker runs
on a worker where blocking is exactly what we want. Now that Then and
BatchUIUpdates work regardless of that choice, drop Mode from the
decision and key it off calledFromWorker instead:

  - Refresh (UI thread) runs its scopes and the finishing step (wait,
    batch flush, Then) on workers, so the caller returns immediately —
    what ASYNC used to mean.
  - RefreshFromWorker runs them on the calling worker, blocking it until
    everything is done — what SYNC used to mean.

Demos keep taking the blocking, inline path so everything still lands in
one deterministic frame.

In practice this flips the handful of RefreshFromWorker calls that
passed ASYNC — they now block their worker until the refresh finishes,
keeping the waiting-status spinner up until the UI actually updates —
and the many UI-thread refreshes that defaulted to SYNC, which no longer
freeze the UI thread while the git work runs. Mode now only feeds the
log line; the next commit removes it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-07-08 15:57:07 +02:00
parent 63bd2d98c0
commit 8580c78cc0

View file

@ -232,10 +232,13 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
refresh := func(name string, f func()) {
wg.Add(1)
// if we're in a demo we don't want any async refreshes because
// everything happens fast and it's better to have everything update
// in the one frame
if !self.c.InDemo() && options.Mode == types.ASYNC {
// A refresh issued from the UI thread must not block it, so its scopes
// run as their own worker tasks and the caller returns immediately (the
// finishing step below is dispatched to a worker too). A refresh issued
// from a worker blocks that worker instead, running its scopes as plain
// goroutines that it joins. In a demo we always take the blocking path
// so everything updates in a single, deterministic frame.
if !self.c.InDemo() && !calledFromWorker {
self.onWorker(env.background, func(t gocui.Task) error {
defer wg.Done()
f()
@ -436,7 +439,10 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
}
}
if options.Mode == types.SYNC {
// waitAndFinalize blocks until every scope is done. Run it inline when we're
// already on a worker (or in a demo, for a deterministic single frame); when
// we're on the UI thread, dispatch it to a worker so it doesn't block the UI.
if calledFromWorker || self.c.InDemo() {
waitAndFinalize()
} else {
self.onWorker(env.background, func(t gocui.Task) error {