Problem
If the user has scrolled up in the chat history and then a modal-class confirm prompt arrives — shell command (run_command / run_background), plan proposed, plan checkpoint, plan revision, or ask_choice — the modal mounts but is not visible: it renders at the bottom of the chat area, which is currently off-screen below the user's scroll position. From the user's point of view they're "stuck" — they can't see the options, and the picker has captured arrow keys so scroll-down inputs no longer scroll the viewport.
Root cause
The pauseGate listener in src/cli/ui/App.tsx:3038-3114 calls setPendingShell / setPendingPlan / setPendingCheckpoint / setPendingRevision / setPendingChoice directly. None of those mount paths reset the chat scroll position. The only chatScroll.jumpToBottom() call in the whole file is on the End key (App.tsx:1218).
chatScroll.jumpToBottom() sets pinned: true; the next setMaxScroll (triggered by the modal's useReserveRows shrinking the chat area) snaps scrollRows to the new bottom. So the existing primitives are sufficient — we just have to call them.
Proposed fix
Add chatScroll.jumpToBottom() once at the top of the pauseGate listener callback, before the switch (request.kind). Covers all five modal kinds with one line. chatScroll from useChatScrollActions() is store-backed and stable across renders, so the empty-deps useEffect stays correct.
Test
Add a test that mounts the App-level pauseGate listener wiring, scrolls up via the store, fires a pauseGate.request("run_command", …), and asserts pinned === true and scrollRows === maxScroll afterward.
Scope
src/cli/ui/App.tsx — one line, plus a tweak to the existing biome-ignore comment so it reads correctly with the new ref.
tests/ — one new file covering the modal-mount → scroll-reset invariant for all five modal kinds.
Good first issue territory.
Problem
If the user has scrolled up in the chat history and then a modal-class confirm prompt arrives — shell command (
run_command/run_background), plan proposed, plan checkpoint, plan revision, orask_choice— the modal mounts but is not visible: it renders at the bottom of the chat area, which is currently off-screen below the user's scroll position. From the user's point of view they're "stuck" — they can't see the options, and the picker has captured arrow keys so scroll-down inputs no longer scroll the viewport.Root cause
The pauseGate listener in
src/cli/ui/App.tsx:3038-3114callssetPendingShell/setPendingPlan/setPendingCheckpoint/setPendingRevision/setPendingChoicedirectly. None of those mount paths reset the chat scroll position. The onlychatScroll.jumpToBottom()call in the whole file is on the End key (App.tsx:1218).chatScroll.jumpToBottom()setspinned: true; the nextsetMaxScroll(triggered by the modal'suseReserveRowsshrinking the chat area) snapsscrollRowsto the new bottom. So the existing primitives are sufficient — we just have to call them.Proposed fix
Add
chatScroll.jumpToBottom()once at the top of the pauseGate listener callback, before theswitch (request.kind). Covers all five modal kinds with one line.chatScrollfromuseChatScrollActions()is store-backed and stable across renders, so the empty-deps useEffect stays correct.Test
Add a test that mounts the App-level pauseGate listener wiring, scrolls up via the store, fires a
pauseGate.request("run_command", …), and assertspinned === trueandscrollRows === maxScrollafterward.Scope
src/cli/ui/App.tsx— one line, plus a tweak to the existing biome-ignore comment so it reads correctly with the new ref.tests/— one new file covering the modal-mount → scroll-reset invariant for all five modal kinds.Good first issue territory.