feat: improve input field functionality with double-Esc clear and exact history

- Add double-Esc to clear input field (press Esc twice within 500ms)
- Fix command history to store exact input including slash commands
- Maintain input focus when new chat content is added above
- Prevent cursor from disappearing during content updates

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
This commit is contained in:
James Peter
2025-08-12 11:04:16 +10:00
co-authored by Claude
parent 3778243b76
commit 1f68fb0fb6
4 changed files with 62 additions and 2 deletions
+35 -2
View File
@@ -60,6 +60,7 @@ pub(crate) struct ChatComposer {
token_usage_info: Option<TokenUsageInfo>,
has_focus: bool,
has_chat_history: bool,
last_esc_time: Option<std::time::Instant>,
}
/// Popup state – at most one can be visible at any time.
@@ -91,6 +92,7 @@ impl ChatComposer {
token_usage_info: None,
has_focus: has_input_focus,
has_chat_history: false,
last_esc_time: None,
}
}
@@ -298,8 +300,11 @@ impl ChatComposer {
// Get the full command text before clearing
let command_text = self.textarea.text().to_string();
// Record the exact slash command that was typed
self.history.record_local_submission(&command_text);
// Send command to the app layer with full text.
self.app_event_tx.send(AppEvent::DispatchCommand(*cmd, command_text));
self.app_event_tx.send(AppEvent::DispatchCommand(*cmd, command_text.clone()));
// Clear textarea so no residual text remains.
self.textarea.set_text("");
@@ -510,6 +515,31 @@ impl ChatComposer {
/// Handle key event when no popup is visible.
fn handle_key_event_without_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
match key_event {
// -------------------------------------------------------------
// Handle Esc key for double-press clearing
// -------------------------------------------------------------
KeyEvent {
code: KeyCode::Esc, ..
} => {
// Check if this is a double-Esc press
const DOUBLE_ESC_THRESHOLD: std::time::Duration = std::time::Duration::from_millis(500);
let now = std::time::Instant::now();
if let Some(last_time) = self.last_esc_time {
if now.duration_since(last_time) < DOUBLE_ESC_THRESHOLD && !self.textarea.is_empty() {
// Double-Esc detected, clear the input field
self.textarea.set_text("");
self.pending_pastes.clear();
self.last_esc_time = None;
self.history.reset_navigation();
return (InputResult::None, true);
}
}
// Single Esc - just record the time
self.last_esc_time = Some(now);
(InputResult::None, false)
}
// -------------------------------------------------------------
// History navigation (Up / Down) – only when the composer is not
// empty or when the cursor is at the correct position, to avoid
@@ -541,6 +571,9 @@ impl ChatComposer {
modifiers: KeyModifiers::NONE,
..
} => {
// Record the exact text that was typed (before replacement)
let original_text = self.textarea.text().to_string();
let mut text = self.textarea.text().to_string();
self.textarea.set_text("");
@@ -555,7 +588,7 @@ impl ChatComposer {
if text.is_empty() {
(InputResult::None, true)
} else {
self.history.record_local_submission(&text);
self.history.record_local_submission(&original_text);
(InputResult::Submitted(text), true)
}
}
@@ -61,6 +61,12 @@ impl ChatComposerHistory {
self.last_history_text = None;
}
}
/// Reset navigation state (used when clearing input with double-Esc)
pub fn reset_navigation(&mut self) {
self.history_cursor = None;
self.last_history_text = None;
}
/// Should Up/Down key presses be interpreted as history navigation given
/// the current content and cursor position of `textarea`?
+15
View File
@@ -391,6 +391,21 @@ impl BottomPane<'_> {
self.live_ring = None;
}
/// Ensure input focus is maintained, especially after redraws or content updates
pub(crate) fn ensure_input_focus(&mut self) {
// Only ensure focus if there's no active modal view
if self.active_view.is_none() {
self.has_input_focus = true;
// Reset any transient state that might affect focus
// Clear any temporary status overlays that might interfere
if !self.is_task_running {
self.live_status = None;
}
// Ensure composer knows it has focus
self.composer.set_ctrl_c_quit_hint(self.ctrl_c_quit_hint, self.has_input_focus);
}
}
// Removed restart_live_status_with_text – no longer used by the current streaming UI.
}
+6
View File
@@ -509,6 +509,8 @@ impl ChatWidget<'_> {
matches!(cell, HistoryCell::UserPrompt { .. })
});
self.bottom_pane.set_has_chat_history(has_conversation);
// Ensure input focus is maintained when new content arrives
self.bottom_pane.ensure_input_focus();
// Request redraw to show new history
self.app_event_tx.send(AppEvent::RequestRedraw);
}
@@ -1765,6 +1767,8 @@ impl ChatWidget<'_> {
for line in lines {
self.history_cells.push(HistoryCell::new_text_line(line));
}
// Ensure input focus is maintained when adding streamed content
self.bottom_pane.ensure_input_focus();
self.app_event_tx.send(AppEvent::RequestRedraw);
}
@@ -1812,6 +1816,8 @@ impl ChatWidget<'_> {
for line in lines {
self.history_cells.push(HistoryCell::new_text_line(line));
}
// Ensure input focus is maintained when adding streamed content
self.bottom_pane.ensure_input_focus();
self.app_event_tx.send(AppEvent::RequestRedraw);
}