Context
This is Phase 2 of the cursor engine work, building directly on the centralized cursor infrastructure introduced in #19.
Blocked by: #19
Problem
During streaming, Flemma appends content to the buffer via nvim_buf_set_lines but never scrolls the viewport. The user sees the spinner, then nothing moves until the response completes, at which point the cursor jumps to the bottom. There is no "follow the output as it streams in" behavior.
Design Intent
A per-buffer auto_scroll boolean flag on buffer_state:
- Set to
true when the user initiates a send (<C-]>). The cursor engine enters "tail mode" — on each on_content chunk (or debounced via the existing idle timer), the viewport scrolls to the bottom of the buffer to follow streaming output.
- Cleared to
false when the user presses any navigation key (detected via CursorMoved where the new position is NOT the last line — i.e., the user deliberately moved away from the bottom). This is the "breakaway" — the user escapes tail mode and Flemma stops scrolling.
- Restored to
true when the user manually navigates back to the last line of the buffer (e.g., presses G). This is "re-attach" — the user opted back into following the stream.
Why the cursor engine (Phase 1) enables this
- Single choke point: All scroll-to-bottom requests go through
cursor.request_move(). The auto_scroll flag can gate whether streaming scroll requests are honored or suppressed — no scattered code paths to coordinate.
- Idle timer infrastructure: The
CursorMoved autocmd and per-buffer timer already exist. Detecting "user navigated away" vs. "system moved cursor" is possible because all system moves go through request_move() (which can set an internal system_move_in_progress guard so the CursorMoved handler doesn't misinterpret system moves as user breakaway).
- Extmark tracking: Not directly needed for streaming scroll (target is always "bottom"), but the
bottom = true flag in MoveOpts already supports this semantic.
Key implementation considerations
- The
on_content callback in core.lua would call cursor.request_move(bufnr, { bottom = true }) (non-forced) on each chunk, gated by buffer_state.auto_scroll == true.
- The
CursorMoved autocmd in cursor.lua needs to distinguish user-initiated movement from system-initiated movement. The cursor engine can set a short-lived guard flag (buffer_state.cursor_system_move = true) around execute_move() calls, which the CursorMoved handler checks before interpreting movement as breakaway.
- Re-attach detection: when
CursorMoved fires and the cursor is on the last line (or within scrolloff lines of the end), restore auto_scroll = true.
- The
auto_scroll flag should respect force — a force=true move during streaming (e.g., user presses G) should both move the cursor AND restore auto-scroll.
- Consider debouncing streaming scroll requests —
on_content can fire very rapidly. The idle timer might be too slow (100ms) for smooth following. A separate, shorter timer (16-33ms, ~30-60fps) or coalescing via vim.schedule may be needed.
Actionable next step
Create a design document for auto-scroll streaming. The cursor engine's request_move, idle timer, CursorMoved autocmd, and buffer_state infrastructure are all prerequisites — they are in place after #19 lands.
Context
This is Phase 2 of the cursor engine work, building directly on the centralized cursor infrastructure introduced in #19.
Blocked by: #19
Problem
During streaming, Flemma appends content to the buffer via
nvim_buf_set_linesbut never scrolls the viewport. The user sees the spinner, then nothing moves until the response completes, at which point the cursor jumps to the bottom. There is no "follow the output as it streams in" behavior.Design Intent
A per-buffer
auto_scrollboolean flag onbuffer_state:truewhen the user initiates a send (<C-]>). The cursor engine enters "tail mode" — on eachon_contentchunk (or debounced via the existing idle timer), the viewport scrolls to the bottom of the buffer to follow streaming output.falsewhen the user presses any navigation key (detected viaCursorMovedwhere the new position is NOT the last line — i.e., the user deliberately moved away from the bottom). This is the "breakaway" — the user escapes tail mode and Flemma stops scrolling.truewhen the user manually navigates back to the last line of the buffer (e.g., pressesG). This is "re-attach" — the user opted back into following the stream.Why the cursor engine (Phase 1) enables this
cursor.request_move(). Theauto_scrollflag can gate whether streaming scroll requests are honored or suppressed — no scattered code paths to coordinate.CursorMovedautocmd and per-buffer timer already exist. Detecting "user navigated away" vs. "system moved cursor" is possible because all system moves go throughrequest_move()(which can set an internalsystem_move_in_progressguard so theCursorMovedhandler doesn't misinterpret system moves as user breakaway).bottom = trueflag inMoveOptsalready supports this semantic.Key implementation considerations
on_contentcallback incore.luawould callcursor.request_move(bufnr, { bottom = true })(non-forced) on each chunk, gated bybuffer_state.auto_scroll == true.CursorMovedautocmd incursor.luaneeds to distinguish user-initiated movement from system-initiated movement. The cursor engine can set a short-lived guard flag (buffer_state.cursor_system_move = true) aroundexecute_move()calls, which theCursorMovedhandler checks before interpreting movement as breakaway.CursorMovedfires and the cursor is on the last line (or withinscrollofflines of the end), restoreauto_scroll = true.auto_scrollflag should respectforce— aforce=truemove during streaming (e.g., user pressesG) should both move the cursor AND restore auto-scroll.on_contentcan fire very rapidly. The idle timer might be too slow (100ms) for smooth following. A separate, shorter timer (16-33ms, ~30-60fps) or coalescing viavim.schedulemay be needed.Actionable next step
Create a design document for auto-scroll streaming. The cursor engine's
request_move, idle timer,CursorMovedautocmd, andbuffer_stateinfrastructure are all prerequisites — they are in place after #19 lands.