Skip to content

fix(tui): Up/Down keys navigate history instead of scrolling content#79

Merged
yishuiliunian merged 2 commits intomainfrom
fix/up-down-key-history-navigation
Apr 4, 2026
Merged

fix(tui): Up/Down keys navigate history instead of scrolling content#79
yishuiliunian merged 2 commits intomainfrom
fix/up-down-key-history-navigation

Conversation

@yishuiliunian
Copy link
Copy Markdown
Contributor

Summary

  • Up/Down keys now navigate input history (or multiline cursor) instead of scrolling the content area
  • Content scrolling is exclusively handled by PageUp/PageDown
  • Input-mode keys auto-reset scroll_offset to 0 (except PageUp/PageDown/Tab/Esc)
  • Removed dead content_overflows field and render_progress return value

Changes

  • crates/loopal-tui/src/input/mod.rs — removed handle_up_key/handle_down_key, Up/Down now call handle_up/handle_down directly; added auto-scroll-to-bottom logic
  • crates/loopal-tui/src/input/navigation.rshandle_up/handle_down reset scroll_offset (covers Ctrl+P/N path)
  • crates/loopal-tui/src/views/progress/mod.rsrender_progress no longer returns overflow flag
  • crates/loopal-tui/src/render.rs — dropped content_overflows assignment
  • crates/loopal-tui/src/app/mod.rs — removed content_overflows field
  • crates/loopal-tui/tests/suite/input_scroll_test.rs — rewrote 12 tests for new behavior
  • crates/loopal-tui/tests/suite/input_scroll_edge_test.rs — 8 new edge-case tests (auto-reset, agent-busy, multiline boundary, Ctrl+P/N scroll reset)

Test plan

  • bazel test //crates/loopal-tui:loopal-tui_test — 20 scroll/history tests pass
  • bazel build //... --config=clippy — zero warnings
  • bazel build //... --config=rustfmt — format clean
  • bazel build //... — full workspace build passes
  • CI passes

…area

Up/Down were scrolling the content area instead of navigating input
history because `content_overflows` was almost always true after any
real conversation. Now Up/Down exclusively handle multiline cursor
movement and history navigation; content scrolling is PageUp/PageDown
only. Input-mode keys auto-reset scroll_offset to 0 (except
PageUp/PageDown/Tab/Esc) so the user always sees the input area when
interacting. Removed dead `content_overflows` field and render_progress
return value.
@yishuiliunian yishuiliunian merged commit 794e195 into main Apr 4, 2026
3 checks passed
@yishuiliunian yishuiliunian deleted the fix/up-down-key-history-navigation branch April 4, 2026 16:41
yishuiliunian added a commit that referenced this pull request Apr 5, 2026
…te session titles (#79)

Replace the text-based session list with a TUI picker subpage when
/resume is invoked without arguments. Filter out sub-agent sessions
using a global exclusion set so only root sessions are shown. Auto-set
session title from the first user message to make the picker useful.

Refactor picker key handling: extract generic Esc/Up/Down/Char/Backspace
into handle_generic_picker_key to eliminate duplication between model and
session pickers. Split rewind picker and session query methods into
separate files to stay within the 200-line limit.
yishuiliunian added a commit that referenced this pull request Apr 5, 2026
* feat(tui): add interactive session picker for /resume and auto-generate session titles (#79)

Replace the text-based session list with a TUI picker subpage when
/resume is invoked without arguments. Filter out sub-agent sessions
using a global exclusion set so only root sessions are shown. Auto-set
session title from the first user message to make the picker useful.

Refactor picker key handling: extract generic Esc/Up/Down/Char/Backspace
into handle_generic_picker_key to eliminate duplication between model and
session pickers. Split rewind picker and session query methods into
separate files to stay within the 200-line limit.

* fix: correct rustfmt import ordering in session_query and sub_page
yishuiliunian added a commit that referenced this pull request Apr 5, 2026
…tial response (#79)

When an API proxy silently drops SSE connections mid-stream (no
`message_stop` event), the agent loop previously treated the truncated
response as a normal EndTurn — causing sub-agents to exit with
incomplete output (e.g. "Let me create the file." without the Write
tool call).

Detection: track `received_done` in the stream loop; EOF without Done
and without cancel sets `stream_error = true`.

Recovery: stream_error with partial content triggers auto-continue
(record text, discard incomplete tool_uses, re-call LLM) — same
pattern as MaxTokens/PauseTurn. Bounded by `max_auto_continuations`.

Cancel safety: cancel paths are excluded from both truncation detection
(llm.rs) and auto-continue (turn_exec.rs) to preserve existing cancel
behavior.

Also extracts stop-hook and observer dispatch into
`turn_observer_dispatch.rs` as an independent lifecycle extension point.
yishuiliunian added a commit that referenced this pull request Apr 5, 2026
…tial response (#82)

* fix(runtime): detect SSE stream truncation and auto-continue from partial response (#79)

When an API proxy silently drops SSE connections mid-stream (no
`message_stop` event), the agent loop previously treated the truncated
response as a normal EndTurn — causing sub-agents to exit with
incomplete output (e.g. "Let me create the file." without the Write
tool call).

Detection: track `received_done` in the stream loop; EOF without Done
and without cancel sets `stream_error = true`.

Recovery: stream_error with partial content triggers auto-continue
(record text, discard incomplete tool_uses, re-call LLM) — same
pattern as MaxTokens/PauseTurn. Bounded by `max_auto_continuations`.

Cancel safety: cancel paths are excluded from both truncation detection
(llm.rs) and auto-continue (turn_exec.rs) to preserve existing cancel
behavior.

Also extracts stop-hook and observer dispatch into
`turn_observer_dispatch.rs` as an independent lifecycle extension point.

* fix: rustfmt formatting and reorganize tests into ≤200-line files
yishuiliunian added a commit that referenced this pull request Apr 5, 2026
…ebounce

xterm alternate scroll (\x1b[?1007h) converts mouse wheel events into
Up/Down arrow keys, creating an unresolvable conflict between content
scrolling and history navigation. Previous attempts (#6, #13, #30, #42,
#79) toggled priority chains without a lasting fix because the two
event sources share the same key codes.

Resolve by introducing a 30 ms debounce state machine that exploits
the timing difference between mouse wheel bursts (<5 ms between events)
and keyboard presses (>20 ms):

  Idle → Pending (defer 30 ms) → Scrolling (burst confirmed)
                                → History   (timer expired / other key)

- Mouse wheel: rapid-fire Up/Down detected as burst → scroll content
- Keyboard Up/Down: single isolated event → history navigation
- Multiline cursor: bypasses debounce entirely (immediate response)
- Ctrl+P/N: dedicated history bindings, always immediate
- Global/modal/autocomplete keys: discard pending debounce (prevents
  stale timer from polluting state after Ctrl+C, paste, etc.)

Stale timer and dropped-tick resilience:
- Pending state checks elapsed time on second arrow to handle delayed
  timers; stale pending is flushed as history before starting new
- Scrolling state has lazy 150 ms expiry check in addition to tick-
  based cleanup, so dropped ticks don't leave stale scroll mode
yishuiliunian added a commit that referenced this pull request Apr 5, 2026
…ebounce (#85)

* fix(tui): decouple mouse scroll from input history via timing-based debounce

xterm alternate scroll (\x1b[?1007h) converts mouse wheel events into
Up/Down arrow keys, creating an unresolvable conflict between content
scrolling and history navigation. Previous attempts (#6, #13, #30, #42,
#79) toggled priority chains without a lasting fix because the two
event sources share the same key codes.

Resolve by introducing a 30 ms debounce state machine that exploits
the timing difference between mouse wheel bursts (<5 ms between events)
and keyboard presses (>20 ms):

  Idle → Pending (defer 30 ms) → Scrolling (burst confirmed)
                                → History   (timer expired / other key)

- Mouse wheel: rapid-fire Up/Down detected as burst → scroll content
- Keyboard Up/Down: single isolated event → history navigation
- Multiline cursor: bypasses debounce entirely (immediate response)
- Ctrl+P/N: dedicated history bindings, always immediate
- Global/modal/autocomplete keys: discard pending debounce (prevents
  stale timer from polluting state after Ctrl+C, paste, etc.)

Stale timer and dropped-tick resilience:
- Pending state checks elapsed time on second arrow to handle delayed
  timers; stale pending is flushed as history before starting new
- Scrolling state has lazy 150 ms expiry check in addition to tick-
  based cleanup, so dropped ticks don't leave stale scroll mode

* fix: rustfmt — alphabetical module order in suite.rs + line-length formatting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant