Skip to content

feat(cursor): auto-scroll during streaming (Phase 2) #20

@StanAngeloff

Description

@StanAngeloff

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

  1. 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.
  2. 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).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions