From d64360f6281df5c062b86715c5c49a9dce03edfb Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 2 Oct 2026 07:09:47 +0000 Subject: [PATCH] Support Ctrl+Insert for copying TUI selections (#50215) ## Why Windows Terminal forwards `Ctrl+Insert` when its Copy action has no native terminal selection. Recognize this shortcut so it can copy text selected within Codex. ## What changed Add `Ctrl+Insert` to the shared copy-key detection used by transcript and composer selections. Use the same detection for transcript input ownership so the selection consumes the shortcut before other input handlers. ## Testing Extend transcript tests to verify `Ctrl+Insert` input ownership and copying. Extend composer tests and snapshots to verify that failed or unconfirmed copies preserve the selection, while confirmed copies clear it without changing the draft or cursor. GitOrigin-RevId: 5a33c72cff4c4395511779a7051fc92f8bd5e5d7 --- codex-rs/tui/src/app/owned_transcript_tests.rs | 11 +++++++---- ..._tests__fullscreen_composer_right_click_copy.snap | 12 ++++++++++++ codex-rs/tui/src/text_selection.rs | 10 ++++++---- codex-rs/tui/src/transcript_view/input.rs | 5 +---- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/codex-rs/tui/src/app/owned_transcript_tests.rs b/codex-rs/tui/src/app/owned_transcript_tests.rs index 1548028051..009b47fdc7 100644 --- a/codex-rs/tui/src/app/owned_transcript_tests.rs +++ b/codex-rs/tui/src/app/owned_transcript_tests.rs @@ -801,10 +801,11 @@ async fn owned_search_and_selection_consume_input_before_composer_and_backtrack( &TuiEvent::Key(key) )?); } - let copy = app.transcript_view.handle_key( - KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), - &app.transcript_cells, - ); + let copy_key = KeyEvent::new(KeyCode::Insert, KeyModifiers::CONTROL); + assert!(app.transcript_view.owns_interaction_key(copy_key)); + let copy = app + .transcript_view + .handle_key(copy_key, &app.transcript_cells); assert!(matches!(copy, Some(ViewAction::Copy(text)) if !text.is_empty())); assert!(app.handle_owned_transcript_event(&mut tui, &mut app_server, &escape)?); assert!(!app.backtrack.overlay_preview_active); @@ -1414,6 +1415,7 @@ async fn fullscreen_composer_mouse_copy_and_input_ownership() -> Result<()> { let copy_events = [ TuiEvent::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::SUPER)), TuiEvent::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)), + TuiEvent::Key(KeyEvent::new(KeyCode::Insert, KeyModifiers::CONTROL)), mouse(Down(Right), x + 2, y), ]; let mut selection_frames = Vec::new(); @@ -1473,6 +1475,7 @@ async fn fullscreen_composer_mouse_copy_and_input_ownership() -> Result<()> { ); let gesture = match event { TuiEvent::Mouse(_) => "right-click", + TuiEvent::Key(key) if key.code == KeyCode::Insert => "ctrl-insert", TuiEvent::Key(key) if key.modifiers == KeyModifiers::SUPER => "cmd-c", _ => "ctrl-c", }; diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__tests__fullscreen_composer_right_click_copy.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__tests__fullscreen_composer_right_click_copy.snap index ebc424a3b0..ef78c11d19 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__tests__fullscreen_composer_right_click_copy.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__tests__fullscreen_composer_right_click_copy.snap @@ -26,6 +26,18 @@ ctrl-c Ok(Confirmed) hello world ··········· +ctrl-insert Err("clipboard unavailable") +hello world +^^^^^······ + +ctrl-insert Ok(Unconfirmed) +hello world +^^^^^······ + +ctrl-insert Ok(Confirmed) +hello world +··········· + right-click Err("clipboard unavailable") hello world ^^^^^······ diff --git a/codex-rs/tui/src/text_selection.rs b/codex-rs/tui/src/text_selection.rs index cdd3491109..b2fbfb1073 100644 --- a/codex-rs/tui/src/text_selection.rs +++ b/codex-rs/tui/src/text_selection.rs @@ -67,10 +67,12 @@ pub(crate) fn click_count( pub(crate) fn is_copy_key(key: KeyEvent) -> bool { // Kitty reports Cmd+C as Super+C, including over SSH. Crossterm may report - // Ctrl+Shift+C as uppercase C with only Control set. + // Ctrl+Shift+C as uppercase C with only Control set. Windows Terminal forwards + // Ctrl+Insert when its Copy action has no native terminal selection. let (code, modifiers) = crate::key_hint::normalize_key_parts(key.code, key.modifiers); key.kind != KeyEventKind::Release - && code == KeyCode::Char('c') - && (matches!(modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER) - || modifiers == (KeyModifiers::CONTROL | KeyModifiers::SHIFT)) + && ((code == KeyCode::Char('c') + && (matches!(modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER) + || modifiers == (KeyModifiers::CONTROL | KeyModifiers::SHIFT))) + || (code == KeyCode::Insert && modifiers == KeyModifiers::CONTROL)) } diff --git a/codex-rs/tui/src/transcript_view/input.rs b/codex-rs/tui/src/transcript_view/input.rs index d3d5be9f9c..e2923e55e2 100644 --- a/codex-rs/tui/src/transcript_view/input.rs +++ b/codex-rs/tui/src/transcript_view/input.rs @@ -136,10 +136,7 @@ impl TranscriptView { return true; } if self.selection.is_some() { - return (code == KeyCode::Char('c') - && matches!(modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER)) - || (code == KeyCode::Char('c') - && modifiers == (KeyModifiers::CONTROL | KeyModifiers::SHIFT)) + return crate::text_selection::is_copy_key(key) || code == KeyCode::Esc || (code == KeyCode::Enter && modifiers == KeyModifiers::NONE) || (matches!(modifiers, KeyModifiers::NONE | KeyModifiers::SHIFT)