Compare commits

...

3 Commits

Author SHA1 Message Date
Charles Cunningham
b526b0b1e9 fix(tui): accept ctrl word-delete aliases in textarea 2026-02-19 14:52:09 -08:00
Charles Cunningham
ec1a4ceb63 docs(tui): explain Option+Up terminal encoding fallback 2026-02-19 13:36:13 -08:00
Charles Cunningham
0ecc0eca02 fix(tui): accept ctrl-up for queued message edit 2026-02-19 13:33:07 -08:00
3 changed files with 52 additions and 6 deletions

View File

@@ -306,9 +306,11 @@ impl TextArea {
} if is_altgr(modifiers) => self.insert_str(&c.to_string()),
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::ALT,
modifiers,
..
} => self.delete_backward_word(),
} if modifiers.intersects(KeyModifiers::ALT | KeyModifiers::CONTROL) => {
self.delete_backward_word()
}
KeyEvent {
code: KeyCode::Backspace,
..
@@ -320,9 +322,11 @@ impl TextArea {
} => self.delete_backward(1),
KeyEvent {
code: KeyCode::Delete,
modifiers: KeyModifiers::ALT,
modifiers,
..
} => self.delete_forward_word(),
} if modifiers.intersects(KeyModifiers::ALT | KeyModifiers::CONTROL) => {
self.delete_forward_word()
}
KeyEvent {
code: KeyCode::Delete,
..
@@ -1737,6 +1741,13 @@ mod tests {
t.input(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT));
assert_eq!(t.text(), "hello ");
assert_eq!(t.cursor(), 6);
// Some terminals encode Option+Backspace as Ctrl+Backspace.
let mut t = ta_with("hello world");
t.set_cursor(t.text().len());
t.input(KeyEvent::new(KeyCode::Backspace, KeyModifiers::CONTROL));
assert_eq!(t.text(), "hello ");
assert_eq!(t.cursor(), 6);
}
#[test]
@@ -1756,6 +1767,13 @@ mod tests {
assert_eq!(t.text(), " world");
assert_eq!(t.cursor(), 0);
// Some terminals encode Option+Delete as Ctrl+Delete.
let mut t = ta_with("hello world");
t.set_cursor(0);
t.input(KeyEvent::new(KeyCode::Delete, KeyModifiers::CONTROL));
assert_eq!(t.text(), " world");
assert_eq!(t.cursor(), 0);
let mut t = ta_with("hello");
t.set_cursor(0);
t.input(KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE));

View File

@@ -3098,12 +3098,16 @@ impl ChatWidget {
{
self.cycle_collaboration_mode();
}
// Terminals can encode Option+Up as Alt+Up or Ctrl+Up (e.g. CSI 1;5A),
// so accept either modifier for queued-message restore.
KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::ALT,
modifiers,
kind: KeyEventKind::Press,
..
} if !self.queued_user_messages.is_empty() => {
} if modifiers.intersects(KeyModifiers::ALT | KeyModifiers::CONTROL)
&& !self.queued_user_messages.is_empty() =>
{
// Prefer the most recently queued item.
if let Some(user_message) = self.queued_user_messages.pop_back() {
self.restore_user_message_to_composer(user_message);

View File

@@ -2807,6 +2807,30 @@ async fn alt_up_edits_most_recent_queued_message() {
);
}
#[tokio::test]
async fn control_up_edits_most_recent_queued_message() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.bottom_pane.set_task_running(true);
chat.queued_user_messages
.push_back(UserMessage::from("first queued".to_string()));
chat.queued_user_messages
.push_back(UserMessage::from("second queued".to_string()));
chat.refresh_queued_user_messages();
chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL));
assert_eq!(
chat.bottom_pane.composer_text(),
"second queued".to_string()
);
assert_eq!(chat.queued_user_messages.len(), 1);
assert_eq!(
chat.queued_user_messages.front().unwrap().text,
"first queued"
);
}
/// Pressing Up to recall the most recent history entry and immediately queuing
/// it while a task is running should always enqueue the same text, even when it
/// is queued repeatedly.