From 8cfae426baae20a1100bb19e214f57a7ffe04f4b Mon Sep 17 00:00:00 2001 From: Rohith Date: Sun, 21 Jun 2026 14:08:42 +0530 Subject: [PATCH] Fix task input cursor escaping into the Activity panel on overflow The input box is a fixed-height widget, but the terminal cursor position wasn't clamped to it. Once typed content (via Shift+Enter newlines or a long wrapped line) exceeded the box's visible rows, the cursor row kept incrementing past the box's bottom border. Ratatui clips rendered widget content to its own Rect, but the terminal cursor is a separate global position it does not clip - so the cursor visually landed inside the Activity panel below instead of staying inside the input box. Add auto-scroll (Paragraph::scroll) so the cursor's line always stays within the box's visible rows, and clamp the cursor's column defensively for the edge case of one long unbroken word that word-wrap can't break. Co-Authored-By: Claude Sonnet 4.6 --- src/cli/ui.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/cli/ui.rs b/src/cli/ui.rs index c342d57..ef6f65b 100644 --- a/src/cli/ui.rs +++ b/src/cli/ui.rs @@ -75,6 +75,21 @@ fn render_input(f: &mut Frame, area: Rect, app: &AppState) { (MUTED, " Press 'i' to describe a code task ") }; + // Content rows available inside the border + horizontal padding. + let visible_rows = area.height.saturating_sub(2); + + let before_cursor = &app.input_text[..app.cursor_position.min(app.input_text.len())]; + let cursor_row = before_cursor.matches('\n').count() as u16; + let cursor_col = before_cursor.rsplit('\n').next().unwrap_or("").chars().count() as u16; + + // Auto-scroll so the cursor's line is always inside the box, instead of + // letting it run off the bottom — past which the terminal cursor would + // land outside this widget's area entirely (ratatui clips rendered + // content to a widget's Rect, but not the separate global terminal + // cursor position, so an unclamped row visually overlaps whatever is + // drawn below, e.g. the Activity panel). + let scroll_row = cursor_row.saturating_sub(visible_rows.saturating_sub(1)); + let input = Paragraph::new(app.input_text.clone()) .style(Style::default().fg(FG)) .block( @@ -85,15 +100,15 @@ fn render_input(f: &mut Frame, area: Rect, app: &AppState) { .title(Span::styled(label, Style::default().fg(border_color).add_modifier(Modifier::BOLD))) .border_style(Style::default().fg(border_color)), ) - .wrap(Wrap { trim: true }); + .wrap(Wrap { trim: true }) + .scroll((scroll_row, 0)); f.render_widget(input, area); if app.is_input_mode { - let before_cursor = &app.input_text[..app.cursor_position.min(app.input_text.len())]; - let row = before_cursor.matches('\n').count() as u16; - let col = before_cursor.rsplit('\n').next().unwrap_or("").chars().count() as u16; - f.set_cursor_position((area.x + col + 2, area.y + 1 + row)); + let cursor_row_in_box = (cursor_row - scroll_row).min(visible_rows.saturating_sub(1)); + let cursor_col = cursor_col.min(area.width.saturating_sub(4)); + f.set_cursor_position((area.x + cursor_col + 2, area.y + 1 + cursor_row_in_box)); } }