Don't scroll a view up to fill blank space while its content is loading

The layout scrolls a view up if its origin is past the bottom of its
content, to avoid showing blank space (e.g. after a resize). But it measures
content height by the lines loaded so far, and command/pty tasks load
asynchronously. So when a view is re-rendered while scrolled down, the layout
would yank it to the top because only a fraction of the content has been read
yet, then leave it there once loading finished.

Track whether a command task is actively reading (set synchronously when the
task is created, so a layout pass in between sees it; cleared at EOF, but not
when stopped, since that means a newer task is taking over) and skip the
scroll-up clamp for such views. onEndOfInput already re-clamps once loading
completes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-08-15 15:30:27 +02:00
co-authored by Claude Opus 5
parent 85cff19a2a
commit fbadbbf99a
4 changed files with 42 additions and 1 deletions
+25
View File
@@ -78,6 +78,11 @@ type ViewBufferManager struct {
taskKey string
onNewKey func()
// Whether a command task is currently reading content into the view. While
// this is true the content is still growing, so callers (e.g. the layout)
// must not clamp the view's scroll position to the amount loaded so far.
loading atomic.Bool
// beforeStart is the function that is called before starting a new task
beforeStart func()
refreshView func()
@@ -162,6 +167,21 @@ func (self *ViewBufferManager) ReadLines(totalLines int) {
}
}
// IsLoading reports whether a command task is currently reading content into the
// view, meaning the content is still growing.
func (self *ViewBufferManager) IsLoading() bool {
return self.loading.Load()
}
// StartLoading marks the view as loading content. It must be called
// synchronously when a command/pty task is started, before the task's goroutine
// runs, so that a layout pass happening in between doesn't clamp the scroll
// position to the not-yet-loaded content. It is cleared when the task reaches
// the end of its input.
func (self *ViewBufferManager) StartLoading() {
self.loading.Store(true)
}
func (self *ViewBufferManager) ReadToEnd(then func()) {
if ch := self.readLines.Load(); ch != nil {
readLines := *ch
@@ -379,6 +399,11 @@ func (self *ViewBufferManager) NewCmdTask(start func() (Cmd, io.Reader), prefix
// whether to scroll) and sets the origin, both of which
// are UI-thread-only, so run it there.
_ = self.onUIThread(self.onEndOfInput)
// The content is fully loaded now, so it's safe again for the
// layout to clamp the scroll position to it. We deliberately
// don't clear this when stopped (rather than EOF'd), because that
// means a newer task is taking over and is still loading.
self.loading.Store(false)
callThen()
// Any read requests that were queued while we were reading are
// now trivially satisfied, since we've read everything. Fire