Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions crates/loopal-tui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@ use loopal_session::SessionController;
use loopal_tool_background::BackgroundTaskStore;

use crate::command::CommandRegistry;
use crate::input::scroll_debounce::ArrowDebounce;
use crate::views::progress::LineCache;
use crate::views::progress::ContentScroll;

/// Main application state — UI-only fields + session controller handle.
pub struct App {
// === UI-only state ===
pub exiting: bool,
pub input: String,
pub input_cursor: usize,
pub scroll_offset: u16,
pub input_history: Vec<String>,
pub history_index: Option<usize>,
/// Images attached to the current input (pending submit).
Expand All @@ -50,8 +48,6 @@ pub struct App {
pub focused_bg_task: Option<String>,
/// Which UI region owns keyboard focus.
pub focus_mode: FocusMode,
/// Arrow-key debounce state for mouse-wheel vs keyboard detection.
pub(crate) arrow_debounce: ArrowDebounce,
/// Scroll offset for the agent panel (index of first visible agent).
pub agent_panel_offset: usize,

Expand All @@ -63,8 +59,8 @@ pub struct App {
// === Session Controller (observable + interactive) ===
pub session: SessionController,

// === Render optimization ===
pub line_cache: LineCache,
// === Content area scroll + render state ===
pub content_scroll: ContentScroll,
}

impl App {
Expand All @@ -82,7 +78,6 @@ impl App {
exiting: false,
input: String::new(),
input_cursor: 0,
scroll_offset: 0,
input_history: Vec::new(),
history_index: None,
pending_images: Vec::new(),
Expand All @@ -97,12 +92,11 @@ impl App {
focused_agent: None,
focused_bg_task: None,
focus_mode: FocusMode::default(),
arrow_debounce: ArrowDebounce::default(),
agent_panel_offset: 0,
bg_store: BackgroundTaskStore::new(),
bg_snapshots: Vec::new(),
session,
line_cache: LineCache::new(),
content_scroll: ContentScroll::new(),
}
}

Expand All @@ -123,7 +117,7 @@ impl App {
}
self.input_cursor = 0;
self.input_scroll = 0;
self.scroll_offset = 0;
self.content_scroll.to_bottom();
Some(UserContent {
text,
images,
Expand Down
2 changes: 0 additions & 2 deletions crates/loopal-tui/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ pub enum AppEvent {
Paste(PasteResult),
/// Tick for periodic UI refresh
Tick,
/// Arrow-key debounce timer expired — flush pending arrow as history
ArrowDebounceTimeout,
}

/// Merges crossterm terminal events with agent events into a single stream.
Expand Down
2 changes: 0 additions & 2 deletions crates/loopal-tui/src/input/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,4 @@ pub enum InputAction {
QuestionCancel,
/// User pressed Ctrl+V — caller should spawn async clipboard read
PasteRequested,
/// Arrow key deferred — start 30 ms debounce timer for scroll detection
StartArrowDebounce,
}
32 changes: 6 additions & 26 deletions crates/loopal-tui/src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ mod modal;
pub(crate) mod multiline;
mod navigation;
pub(crate) mod paste;
pub(crate) mod scroll_debounce;
mod status_page_keys;
mod sub_page;
mod sub_page_rewind;
Expand All @@ -21,22 +20,18 @@ use editing::{handle_backspace, handle_ctrl_c, handle_enter};
use navigation::{
DEFAULT_WRAP_WIDTH, handle_down, handle_esc, handle_up, move_cursor_left, move_cursor_right,
};
use scroll_debounce::{ScrollDirection, handle_arrow_with_debounce, resolve_pending_arrow};

/// Process a key event and update the app's input state.
pub fn handle_key(app: &mut App, key: KeyEvent) -> InputAction {
if let Some(action) = modal::handle_modal_keys(app, &key) {
scroll_debounce::discard_pending(app);
return action;
}
if let Some(action) = handle_global_keys(app, &key) {
scroll_debounce::discard_pending(app);
return action;
}
if app.autocomplete.is_some()
&& let Some(action) = handle_autocomplete_key(app, &key)
{
scroll_debounce::discard_pending(app);
return action;
}

Expand All @@ -45,14 +40,6 @@ pub fn handle_key(app: &mut App, key: KeyEvent) -> InputAction {
action
}

/// Flush any pending arrow-key debounce as history navigation.
///
/// Called by the event loop when the 30 ms debounce timer expires and by
/// tests to simulate the timeout deterministically.
pub fn resolve_arrow_debounce(app: &mut App) {
scroll_debounce::resolve_pending_arrow(app);
}

/// Handle global shortcuts: Ctrl combos, Shift+Tab.
fn handle_global_keys(app: &mut App, key: &KeyEvent) -> Option<InputAction> {
if key.modifiers.contains(KeyModifiers::CONTROL) {
Expand Down Expand Up @@ -123,12 +110,7 @@ fn handle_panel_key(app: &mut App, key: &KeyEvent) -> InputAction {

/// Keys in Input mode: typing, navigation, submit.
fn handle_input_mode_key(app: &mut App, key: &KeyEvent) -> InputAction {
// Flush any pending arrow debounce on non-arrow key input.
if !matches!(key.code, KeyCode::Up | KeyCode::Down) {
resolve_pending_arrow(app);
}
// Auto-scroll to bottom on input interaction (except scroll/panel/escape/arrow keys).
// Arrow keys are exempt because they may become scroll via debounce.
// Auto-scroll to bottom on input interaction (except scroll/panel/escape keys).
if !matches!(
key.code,
KeyCode::PageUp
Expand All @@ -138,7 +120,7 @@ fn handle_input_mode_key(app: &mut App, key: &KeyEvent) -> InputAction {
| KeyCode::Up
| KeyCode::Down
) {
app.scroll_offset = 0;
app.content_scroll.to_bottom();
}
match key.code {
KeyCode::Enter if key.modifiers.contains(KeyModifiers::SHIFT) => {
Expand Down Expand Up @@ -177,18 +159,16 @@ fn handle_input_mode_key(app: &mut App, key: &KeyEvent) -> InputAction {
multiline::line_end(&app.input, app.input_cursor, DEFAULT_WRAP_WIDTH);
InputAction::None
}
KeyCode::Up | KeyCode::Down => {
let dir = ScrollDirection::from_key(key.code).unwrap();
handle_arrow_with_debounce(app, dir)
}
KeyCode::Up => handle_up(app),
KeyCode::Down => handle_down(app),
KeyCode::Tab => InputAction::EnterPanel,
KeyCode::Esc => handle_esc(app),
KeyCode::PageUp => {
app.scroll_offset = app.scroll_offset.saturating_add(10);
app.content_scroll.scroll_up(10);
InputAction::None
}
KeyCode::PageDown => {
app.scroll_offset = app.scroll_offset.saturating_sub(10);
app.content_scroll.scroll_down(10);
InputAction::None
}
_ => InputAction::None,
Expand Down
2 changes: 0 additions & 2 deletions crates/loopal-tui/src/input/navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ pub(super) fn move_cursor_right(app: &mut App) {

/// Up: multiline navigation first, then history browse.
pub(super) fn handle_up(app: &mut App) -> InputAction {
app.scroll_offset = 0;
if multiline::is_multiline(&app.input, DEFAULT_WRAP_WIDTH)
&& let Some(new_cursor) =
multiline::cursor_up(&app.input, app.input_cursor, DEFAULT_WRAP_WIDTH)
Expand All @@ -55,7 +54,6 @@ pub(super) fn handle_up(app: &mut App) -> InputAction {

/// Down: multiline navigation first, then history browse.
pub(super) fn handle_down(app: &mut App) -> InputAction {
app.scroll_offset = 0;
if multiline::is_multiline(&app.input, DEFAULT_WRAP_WIDTH)
&& let Some(new_cursor) =
multiline::cursor_down(&app.input, app.input_cursor, DEFAULT_WRAP_WIDTH)
Expand Down
197 changes: 0 additions & 197 deletions crates/loopal-tui/src/input/scroll_debounce.rs

This file was deleted.

Loading
Loading