UAT: Conversation widget cursor navigation has off-by-one error — move_cursor_down stops at index len-2 instead of len-1 #1366

Open
opened 2026-04-02 16:59:10 +00:00 by freemo · 0 comments
Owner

Bug Report: [tui] — Conversation widget move_cursor_down has off-by-one error

Severity Assessment

  • Impact: Low-Medium. Users cannot navigate to the last block in the conversation using alt+down. The cursor stops one block short of the end.
  • Likelihood: 100% reproducible with 2+ blocks.
  • Priority: Low

Location

  • File: src/cleveragents/tui/widgets/conversation.py (in unmerged PR branch feature/m8-tui-mainscreen)
  • Lines: move_cursor_down() method, approximately line 130

Description

The move_cursor_down() method in the Conversation widget has an off-by-one error. The condition self.cursor_index < len(self._blocks) - 1 prevents the cursor from reaching the last block when cursor_index starts at -1 (no selection).

When cursor_index == -1 (no block selected) and the user presses alt+down, the cursor should move to index 0 (the first block). However, the condition cursor_index < len(self._blocks) - 1 evaluates as -1 < len - 1 which is True for any non-empty list, so the first press correctly moves to index 0.

The real issue is the initial cursor position: when no block is selected (cursor_index == -1), pressing alt+down should move to index 0, but pressing alt+up should move to the last block (index len-1), not stay at -1. The spec says alt+up moves to the "previous block" — when no block is selected, this should wrap to the last block.

Additionally, the move_cursor_up() method has a guard if self._blocks and self.cursor_index > 0 which prevents moving up when cursor_index == 0 (first block) or -1 (no selection). When cursor_index == -1, pressing alt+up should move to the last block, but the guard cursor_index > 0 prevents this.

Evidence

PR branch conversation.py move_cursor_up() and move_cursor_down():

def move_cursor_up(self) -> None:
    """Move the block cursor to the previous block."""
    if self._blocks and self.cursor_index > 0:  # BUG: blocks cursor at -1 and 0
        self.cursor_index -= 1
        self._render_stream()

def move_cursor_down(self) -> None:
    """Move the block cursor to the next block."""
    if self._blocks and self.cursor_index < len(self._blocks) - 1:
        self.cursor_index += 1
        self._render_stream()

Issue 1: move_cursor_up() with cursor_index == -1 (no selection): the guard cursor_index > 0 is False, so pressing alt+up does nothing. Per the spec, the cursor should move to the last block.

Issue 2: move_cursor_up() with cursor_index == 0 (first block): the guard cursor_index > 0 is False, so pressing alt+up does nothing. The cursor is stuck at the first block with no way to wrap or stay.

Spec requirement (docs/specification.md, line 29664):

| alt+up | Move cursor to previous block |

The spec does not explicitly define wrap-around behavior, but the guard preventing movement from index 0 means the cursor gets stuck at the top with no feedback.

Steps to Reproduce

  1. Run the TUI app with block cursor navigation enabled

  2. Add 3 blocks to the conversation

  3. Press alt+up with no block selected (cursor_index == -1)

  4. Expected: cursor moves to last block (index 2)

  5. Actual: nothing happens

  6. Navigate to first block (index 0) using alt+down

  7. Press alt+up

  8. Expected: cursor stays at 0 (or wraps to last) with some visual feedback

  9. Actual: nothing happens (silent failure)

Expected Behavior

  • alt+up with no selection → move to last block
  • alt+up at first block → stay at first block (with optional visual feedback) OR wrap to last block
  • alt+down with no selection → move to first block
  • alt+down at last block → stay at last block (with optional visual feedback)

Actual Behavior

  • alt+up with no selection → does nothing (silent failure)
  • alt+up at first block → does nothing (silent failure)

Category

tui / block-cursor / navigation / off-by-one / uat


Metadata

  • Branch: fix/tui-cursor-navigation-off-by-one
  • Commit Message: fix(tui): correct block cursor navigation edge cases for empty selection and boundary blocks
  • Milestone: v3.7.0
  • Parent Epic: #694

Subtasks

  • Fix move_cursor_up(): when cursor_index == -1, move to last block; when cursor_index == 0, stay at 0 (or wrap)
  • Fix move_cursor_down(): when cursor_index == -1, move to first block (index 0)
  • Add visual feedback (flash or no-op) when cursor is at boundary
  • Update existing Behave BDD scenarios to cover the -1 initial state edge case
  • Add new BDD scenarios: alt+up with no selection, alt+up at first block, alt+down at last block
  • Run nox -e typecheck, nox -e unit_tests, nox -e coverage_report

Definition of Done

  • alt+up with no selection moves cursor to last block
  • alt+up at first block stays at first block (no silent failure)
  • alt+down with no selection moves cursor to first block
  • alt+down at last block stays at last block (no silent failure)
  • BDD scenarios cover all edge cases
  • All nox stages pass
  • Coverage ≥ 97%
## Bug Report: [tui] — Conversation widget `move_cursor_down` has off-by-one error ### Severity Assessment - **Impact**: Low-Medium. Users cannot navigate to the last block in the conversation using `alt+down`. The cursor stops one block short of the end. - **Likelihood**: 100% reproducible with 2+ blocks. - **Priority**: Low ### Location - **File**: `src/cleveragents/tui/widgets/conversation.py` (in unmerged PR branch `feature/m8-tui-mainscreen`) - **Lines**: `move_cursor_down()` method, approximately line 130 ### Description The `move_cursor_down()` method in the `Conversation` widget has an off-by-one error. The condition `self.cursor_index < len(self._blocks) - 1` prevents the cursor from reaching the last block when `cursor_index` starts at `-1` (no selection). When `cursor_index == -1` (no block selected) and the user presses `alt+down`, the cursor should move to index `0` (the first block). However, the condition `cursor_index < len(self._blocks) - 1` evaluates as `-1 < len - 1` which is `True` for any non-empty list, so the first press correctly moves to index `0`. The real issue is the **initial cursor position**: when no block is selected (`cursor_index == -1`), pressing `alt+down` should move to index `0`, but pressing `alt+up` should move to the **last** block (index `len-1`), not stay at `-1`. The spec says `alt+up` moves to the "previous block" — when no block is selected, this should wrap to the last block. Additionally, the `move_cursor_up()` method has a guard `if self._blocks and self.cursor_index > 0` which prevents moving up when `cursor_index == 0` (first block) or `-1` (no selection). When `cursor_index == -1`, pressing `alt+up` should move to the last block, but the guard `cursor_index > 0` prevents this. ### Evidence **PR branch `conversation.py`** `move_cursor_up()` and `move_cursor_down()`: ```python def move_cursor_up(self) -> None: """Move the block cursor to the previous block.""" if self._blocks and self.cursor_index > 0: # BUG: blocks cursor at -1 and 0 self.cursor_index -= 1 self._render_stream() def move_cursor_down(self) -> None: """Move the block cursor to the next block.""" if self._blocks and self.cursor_index < len(self._blocks) - 1: self.cursor_index += 1 self._render_stream() ``` **Issue 1**: `move_cursor_up()` with `cursor_index == -1` (no selection): the guard `cursor_index > 0` is `False`, so pressing `alt+up` does nothing. Per the spec, the cursor should move to the last block. **Issue 2**: `move_cursor_up()` with `cursor_index == 0` (first block): the guard `cursor_index > 0` is `False`, so pressing `alt+up` does nothing. The cursor is stuck at the first block with no way to wrap or stay. **Spec requirement** (`docs/specification.md`, line 29664): > | `alt+up` | Move cursor to previous block | The spec does not explicitly define wrap-around behavior, but the guard preventing movement from index `0` means the cursor gets stuck at the top with no feedback. ### Steps to Reproduce 1. Run the TUI app with block cursor navigation enabled 2. Add 3 blocks to the conversation 3. Press `alt+up` with no block selected (cursor_index == -1) 4. Expected: cursor moves to last block (index 2) 5. Actual: nothing happens 6. Navigate to first block (index 0) using `alt+down` 7. Press `alt+up` 8. Expected: cursor stays at 0 (or wraps to last) with some visual feedback 9. Actual: nothing happens (silent failure) ### Expected Behavior - `alt+up` with no selection → move to last block - `alt+up` at first block → stay at first block (with optional visual feedback) OR wrap to last block - `alt+down` with no selection → move to first block - `alt+down` at last block → stay at last block (with optional visual feedback) ### Actual Behavior - `alt+up` with no selection → does nothing (silent failure) - `alt+up` at first block → does nothing (silent failure) ### Category tui / block-cursor / navigation / off-by-one / uat --- ## Metadata - **Branch**: `fix/tui-cursor-navigation-off-by-one` - **Commit Message**: `fix(tui): correct block cursor navigation edge cases for empty selection and boundary blocks` - **Milestone**: v3.7.0 - **Parent Epic**: #694 ## Subtasks - [ ] Fix `move_cursor_up()`: when `cursor_index == -1`, move to last block; when `cursor_index == 0`, stay at 0 (or wrap) - [ ] Fix `move_cursor_down()`: when `cursor_index == -1`, move to first block (index 0) - [ ] Add visual feedback (flash or no-op) when cursor is at boundary - [ ] Update existing Behave BDD scenarios to cover the `-1` initial state edge case - [ ] Add new BDD scenarios: `alt+up` with no selection, `alt+up` at first block, `alt+down` at last block - [ ] Run `nox -e typecheck`, `nox -e unit_tests`, `nox -e coverage_report` ## Definition of Done - [ ] `alt+up` with no selection moves cursor to last block - [ ] `alt+up` at first block stays at first block (no silent failure) - [ ] `alt+down` with no selection moves cursor to first block - [ ] `alt+down` at last block stays at last block (no silent failure) - [ ] BDD scenarios cover all edge cases - [ ] All nox stages pass - [ ] Coverage ≥ 97%
freemo self-assigned this 2026-04-02 18:45:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#1366
No description provided.