mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
85065ea1b8
commit
5e3793def2
@@ -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();
|
||||
|
||||
+21
@@ -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,
|
||||
]
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
+21
@@ -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,
|
||||
]
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user