Summary
After vertico (or any minibuffer-resize) closes, a TUI ghostel buffer
can scroll to point-min or to a spot well above the viewport,
leaving the live viewport hidden. A follow-up scroll-conservatively
auto-scroll often salvages it halfway — the prompt ends up pinned
to the bottom of the window with the rows below the cursor (common
in TUIs) clipped off.
Repro
- Open a ghostel buffer running a TUI whose cursor sits above the
viewport bottom (Claude Code, opencode, codex). Scrollback
should be populated (tested with ~20–230 KB).
- Let it sit so the window is anchored on the viewport.
M-x and let vertico open and close (any minibuffer that
triggers a ghostel window resize — I saw a width change here, but
height-only minibuffer resizes should behave the same).
- Observe: the window-start jumps above (or below) the viewport
instead of following the live TUI.
Diagnosis
ghostel-debug-start log across the buggy redraw:
REDRAW: 2.0ms force=t→nil … vs=17054→17062 anchor=17054→17062
wins-before: ws=16314 we=19068 wp=16385 body=57
wins-after: ws=1 we=2923 wp=16393 body=57
RESIZE: 57x57 → 80x57 (3.3ms)
Sequence:
- Prior redraw ends with the window anchored:
ws = vs = anchor = 17054. The window is not in ghostel--scroll-positions.
- Between redraws Emacs redisplay drifts
window-start below the
anchor. In my case wp (the live cursor) is at 16385 — above the
viewport, as happens for TUIs whose cursor isn't on the last
rendered row. When the minibuffer-triggered resize shrinks the
body keep-point-visible moves ws forward so wp stays on
screen, landing at 16314 (below the 17054 anchor).
- Resize fires → force redraw.
ghostel--window-anchored-p checks ws (16314) ≥ anchor (17054)
→ nil → window classified as non-anchored.
ghostel--capture-window-state saves a ws-key for the blank line
at 16314.
- Redraw runs.
ghostel--restore-scrollback-window calls ghostel--find-line-pos
on that key — the blank lines match at point-min, so ws is
set to 1.
- Emacs's
scroll-conservatively=101 then auto-scrolls forward to
bring wp back on screen, overshooting the anchor and producing
the "prompt on bottom, nothing visible below" symptom.
ghostel--correct-mangled-scroll-positions already handles the same
drift for windows that were previously scrolled — it walks
ghostel--scroll-positions, which doesn't contain anchored windows,
so the bug only surfaces for windows that were on the viewport.
Proposed fix
During a force-triggered redraw (resize, clear-scrollback,
copy-mode-exit, snap-to-input), a window that wasn't recorded as
scrolled at the prior redraw should stay anchored even if Emacs
drifted window-start below the anchor in between:
(defvar ghostel--redraw-force-active nil
"Dynamically bound to t inside a force-triggered `ghostel--delayed-redraw'.")
(defsubst ghostel--window-anchored-p (win)
(let ((anchor ghostel--last-anchor-position))
(or ghostel--snap-requested
(null anchor)
(>= (window-start win) anchor)
(and ghostel--redraw-force-active
(not (assq win ghostel--scroll-positions))))))
ghostel--delayed-redraw wraps its body in (let ((ghostel--redraw-force-active ghostel--force-next-redraw)) …)
before clearing ghostel--force-next-redraw.
Edge case: if the user genuinely scrolls into scrollback and a
force redraw (resize) fires before any output-driven redraw records
the window in ghostel--scroll-positions, the new rule snaps them
back. In practice there's usually intervening output, so this should
be rare. An alternative is to push windows into
ghostel--scroll-positions from the mouse-wheel intercept path.
Workaround (no upstream change)
For anyone hitting this before a fix lands, advising the resize hook
to restore the anchor works without touching internals:
(defun my/ghostel--restore-anchor-before-resize (process _windows)
(let ((buffer (process-buffer process)))
(when (buffer-live-p buffer)
(with-current-buffer buffer
(when (and (bound-and-true-p ghostel--term)
ghostel--last-anchor-position)
(let ((anchor ghostel--last-anchor-position)
(scrolled ghostel--scroll-positions))
(dolist (win (get-buffer-window-list buffer nil t))
(when (and (< (window-start win) anchor)
(not (assq win scrolled)))
(set-window-start win anchor t)))))))))
(advice-add #'ghostel--window-adjust-process-window-size
:before #'my/ghostel--restore-anchor-before-resize)
Environment
- ghostel 0.15.0 (commit 9846c64)
- Emacs 30.x, macOS (Darwin 25.3.0)
scroll-conservatively 101 (ghostel default)
Summary
After vertico (or any minibuffer-resize) closes, a TUI ghostel buffer
can scroll to
point-minor to a spot well above the viewport,leaving the live viewport hidden. A follow-up
scroll-conservativelyauto-scroll often salvages it halfway — the prompt ends up pinned
to the bottom of the window with the rows below the cursor (common
in TUIs) clipped off.
Repro
viewport bottom (Claude Code, opencode, codex). Scrollback
should be populated (tested with ~20–230 KB).
M-xand let vertico open and close (any minibuffer thattriggers a ghostel window resize — I saw a width change here, but
height-only minibuffer resizes should behave the same).
instead of following the live TUI.
Diagnosis
ghostel-debug-startlog across the buggy redraw:Sequence:
ws = vs = anchor = 17054. The window is not inghostel--scroll-positions.window-startbelow theanchor. In my case
wp(the live cursor) is at 16385 — above theviewport, as happens for TUIs whose cursor isn't on the last
rendered row. When the minibuffer-triggered resize shrinks the
body
keep-point-visiblemoveswsforward sowpstays onscreen, landing at 16314 (below the 17054 anchor).
ghostel--window-anchored-pchecksws (16314) ≥ anchor (17054)→ nil → window classified as non-anchored.
ghostel--capture-window-statesaves a ws-key for the blank lineat 16314.
ghostel--restore-scrollback-windowcallsghostel--find-line-poson that key — the blank lines match at
point-min, sowsisset to 1.
scroll-conservatively=101then auto-scrolls forward tobring
wpback on screen, overshooting the anchor and producingthe "prompt on bottom, nothing visible below" symptom.
ghostel--correct-mangled-scroll-positionsalready handles the samedrift for windows that were previously scrolled — it walks
ghostel--scroll-positions, which doesn't contain anchored windows,so the bug only surfaces for windows that were on the viewport.
Proposed fix
During a force-triggered redraw (resize, clear-scrollback,
copy-mode-exit, snap-to-input), a window that wasn't recorded as
scrolled at the prior redraw should stay anchored even if Emacs
drifted
window-startbelow the anchor in between:ghostel--delayed-redrawwraps its body in(let ((ghostel--redraw-force-active ghostel--force-next-redraw)) …)before clearing
ghostel--force-next-redraw.Edge case: if the user genuinely scrolls into scrollback and a
force redraw (resize) fires before any output-driven redraw records
the window in
ghostel--scroll-positions, the new rule snaps themback. In practice there's usually intervening output, so this should
be rare. An alternative is to push windows into
ghostel--scroll-positionsfrom the mouse-wheel intercept path.Workaround (no upstream change)
For anyone hitting this before a fix lands, advising the resize hook
to restore the anchor works without touching internals:
Environment
scroll-conservatively101 (ghostel default)