Use Shift+Left to edit queued messages in tmux (#15480)

## Summary
- use Shift+Left to edit the most recent queued message when running
under tmux
- mirror the same binding change in the app-server TUI
- add tmux-specific tests and snapshot coverage for the rendered
queued-message hint

## Testing
- just fmt
- cargo test -p codex-tui
- cargo test -p codex-tui-app-server
- just argument-comment-lint -p codex-tui -p codex-tui-app-server

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charley Cunningham
2026-03-22 21:19:31 -07:00
committed by GitHub
Unverified
parent 85065ea1b8
commit 5e3793def2
8 changed files with 262 additions and 42 deletions
@@ -176,6 +176,21 @@ mod tests {
assert_snapshot!("render_one_message", format!("{buf:?}"));
}
#[test]
fn render_one_message_with_shift_left_binding() {
let mut queue = PendingInputPreview::new();
queue.queued_messages.push("Hello, world!".to_string());
queue.set_edit_binding(key_hint::shift(KeyCode::Left));
let width = 40;
let height = queue.desired_height(width);
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
queue.render(Rect::new(0, 0, width, height), &mut buf);
assert_snapshot!(
"render_one_message_with_shift_left_binding",
format!("{buf:?}")
);
}
#[test]
fn render_two_messages() {
let mut queue = PendingInputPreview::new();
@@ -0,0 +1,21 @@
---
source: tui/src/bottom_pane/pending_input_preview.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 40, height: 3 },
content: [
"• Queued follow-up messages ",
" ↳ Hello, world! ",
" shift + ← edit last queued message ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 38, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}
+17 -11
View File
@@ -155,6 +155,8 @@ use codex_protocol::request_permissions::RequestPermissionsEvent;
use codex_protocol::request_user_input::RequestUserInputEvent;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_terminal_detection::terminal_info;
use codex_utils_sleep_inhibitor::SleepInhibitor;
@@ -195,14 +197,21 @@ const CONNECTORS_SELECTION_VIEW_ID: &str = "connectors-selection";
/// Choose the keybinding used to edit the most-recently queued message.
///
/// Apple Terminal, Warp, and VSCode integrated terminals intercept or silently
/// swallow Alt+Up, so users in those environments would never be able to trigger
/// the edit action. We fall back to Shift+Left for those terminals while
/// keeping the more discoverable Alt+Up everywhere else.
/// swallow Alt+Up, and tmux does not reliably pass that chord through. We fall
/// back to Shift+Left for those environments while keeping the more discoverable
/// Alt+Up everywhere else.
///
/// The match is exhaustive so that adding a new `TerminalName` variant forces
/// an explicit decision about which binding that terminal should use.
fn queued_message_edit_binding_for_terminal(terminal_name: TerminalName) -> KeyBinding {
match terminal_name {
fn queued_message_edit_binding_for_terminal(terminal_info: TerminalInfo) -> KeyBinding {
if matches!(
terminal_info.multiplexer.as_ref(),
Some(Multiplexer::Tmux { .. })
) {
return key_hint::shift(KeyCode::Left);
}
match terminal_info.name {
TerminalName::AppleTerminal | TerminalName::WarpTerminal | TerminalName::VsCode => {
key_hint::shift(KeyCode::Left)
}
@@ -3617,8 +3626,7 @@ impl ChatWidget {
let active_cell = Some(Self::placeholder_session_header_cell(&config));
let current_cwd = Some(config.cwd.clone());
let queued_message_edit_binding =
queued_message_edit_binding_for_terminal(terminal_info().name);
let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info());
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),
@@ -3819,8 +3827,7 @@ impl ChatWidget {
let active_cell = Some(Self::placeholder_session_header_cell(&config));
let current_cwd = Some(config.cwd.clone());
let queued_message_edit_binding =
queued_message_edit_binding_for_terminal(terminal_info().name);
let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info());
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),
@@ -4013,8 +4020,7 @@ impl ChatWidget {
settings: fallback_default,
};
let queued_message_edit_binding =
queued_message_edit_binding_for_terminal(terminal_info().name);
let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info());
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),
+79 -12
View File
@@ -120,6 +120,8 @@ use codex_protocol::request_user_input::RequestUserInputQuestion;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_approval_presets::builtin_approval_presets;
@@ -3756,10 +3758,10 @@ async fn alt_up_edits_most_recent_queued_message() {
}
async fn assert_shift_left_edits_most_recent_queued_message_for_terminal(
terminal_name: TerminalName,
terminal_info: TerminalInfo,
) {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_name);
chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info);
chat.bottom_pane
.set_queued_message_edit_binding(chat.queued_message_edit_binding);
@@ -3791,37 +3793,102 @@ async fn assert_shift_left_edits_most_recent_queued_message_for_terminal(
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_apple_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::AppleTerminal)
.await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::AppleTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_warp_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::WarpTerminal)
.await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::WarpTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_vscode_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::VsCode).await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::VsCode,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_tmux() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: Some(Multiplexer::Tmux { version: None }),
})
.await;
}
#[test]
fn queued_message_edit_binding_mapping_covers_special_terminals() {
fn queued_message_edit_binding_mapping_covers_special_terminals_and_tmux() {
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::AppleTerminal),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::AppleTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::WarpTerminal),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::WarpTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::VsCode),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::VsCode,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::Iterm2),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: Some(Multiplexer::Tmux { version: None }),
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::alt(KeyCode::Up)
);
}
@@ -176,6 +176,21 @@ mod tests {
assert_snapshot!("render_one_message", format!("{buf:?}"));
}
#[test]
fn render_one_message_with_shift_left_binding() {
let mut queue = PendingInputPreview::new();
queue.queued_messages.push("Hello, world!".to_string());
queue.set_edit_binding(key_hint::shift(KeyCode::Left));
let width = 40;
let height = queue.desired_height(width);
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
queue.render(Rect::new(0, 0, width, height), &mut buf);
assert_snapshot!(
"render_one_message_with_shift_left_binding",
format!("{buf:?}")
);
}
#[test]
fn render_two_messages() {
let mut queue = PendingInputPreview::new();
@@ -0,0 +1,21 @@
---
source: tui_app_server/src/bottom_pane/pending_input_preview.rs
expression: "format!(\"{buf:?}\")"
---
Buffer {
area: Rect { x: 0, y: 0, width: 40, height: 3 },
content: [
"• Queued follow-up messages ",
" ↳ Hello, world! ",
" shift + ← edit last queued message ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
x: 38, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}
+15 -7
View File
@@ -199,6 +199,8 @@ use codex_protocol::request_user_input::RequestUserInputEvent;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_terminal_detection::terminal_info;
use codex_utils_sleep_inhibitor::SleepInhibitor;
@@ -238,14 +240,21 @@ const APP_SERVER_TUI_STUB_MESSAGE: &str = "Not available in app-server TUI yet."
/// Choose the keybinding used to edit the most-recently queued message.
///
/// Apple Terminal, Warp, and VSCode integrated terminals intercept or silently
/// swallow Alt+Up, so users in those environments would never be able to trigger
/// the edit action. We fall back to Shift+Left for those terminals while
/// keeping the more discoverable Alt+Up everywhere else.
/// swallow Alt+Up, and tmux does not reliably pass that chord through. We fall
/// back to Shift+Left for those environments while keeping the more discoverable
/// Alt+Up everywhere else.
///
/// The match is exhaustive so that adding a new `TerminalName` variant forces
/// an explicit decision about which binding that terminal should use.
fn queued_message_edit_binding_for_terminal(terminal_name: TerminalName) -> KeyBinding {
match terminal_name {
fn queued_message_edit_binding_for_terminal(terminal_info: TerminalInfo) -> KeyBinding {
if matches!(
terminal_info.multiplexer.as_ref(),
Some(Multiplexer::Tmux { .. })
) {
return key_hint::shift(KeyCode::Left);
}
match terminal_info.name {
TerminalName::AppleTerminal | TerminalName::WarpTerminal | TerminalName::VsCode => {
key_hint::shift(KeyCode::Left)
}
@@ -4170,8 +4179,7 @@ impl ChatWidget {
let active_cell = Some(Self::placeholder_session_header_cell(&config));
let current_cwd = Some(config.cwd.clone());
let queued_message_edit_binding =
queued_message_edit_binding_for_terminal(terminal_info().name);
let queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info());
let mut widget = Self {
app_event_tx: app_event_tx.clone(),
frame_requester: frame_requester.clone(),
+79 -12
View File
@@ -143,6 +143,8 @@ use codex_protocol::request_user_input::RequestUserInputQuestion;
use codex_protocol::request_user_input::RequestUserInputQuestionOption;
use codex_protocol::user_input::TextElement;
use codex_protocol::user_input::UserInput;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_approval_presets::builtin_approval_presets;
@@ -3765,10 +3767,10 @@ async fn alt_up_edits_most_recent_queued_message() {
}
async fn assert_shift_left_edits_most_recent_queued_message_for_terminal(
terminal_name: TerminalName,
terminal_info: TerminalInfo,
) {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_name);
chat.queued_message_edit_binding = queued_message_edit_binding_for_terminal(terminal_info);
chat.bottom_pane
.set_queued_message_edit_binding(chat.queued_message_edit_binding);
@@ -3800,37 +3802,102 @@ async fn assert_shift_left_edits_most_recent_queued_message_for_terminal(
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_apple_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::AppleTerminal)
.await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::AppleTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_warp_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::WarpTerminal)
.await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::WarpTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_vscode_terminal() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalName::VsCode).await;
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::VsCode,
term_program: None,
version: None,
term: None,
multiplexer: None,
})
.await;
}
#[tokio::test]
async fn shift_left_edits_most_recent_queued_message_in_tmux() {
assert_shift_left_edits_most_recent_queued_message_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: Some(Multiplexer::Tmux { version: None }),
})
.await;
}
#[test]
fn queued_message_edit_binding_mapping_covers_special_terminals() {
fn queued_message_edit_binding_mapping_covers_special_terminals_and_tmux() {
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::AppleTerminal),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::AppleTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::WarpTerminal),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::WarpTerminal,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::VsCode),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::VsCode,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalName::Iterm2),
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: Some(Multiplexer::Tmux { version: None }),
}),
crate::key_hint::shift(KeyCode::Left)
);
assert_eq!(
queued_message_edit_binding_for_terminal(TerminalInfo {
name: TerminalName::Iterm2,
term_program: None,
version: None,
term: None,
multiplexer: None,
}),
crate::key_hint::alt(KeyCode::Up)
);
}