feat: Allow pasting newlines (#866)

Noticed that when pasting multi-line blocks, each newline was treated
like a new submission.
Update tui to handle Paste directly and map newlines to shift+enter.

# Test

Copied this into clipboard:
```
Do nothing.
Explain this repo to me.
```

Pasted in and saw multi-line input. Hitting Enter then submitted the
full block.
This commit is contained in:
jcoens-openai
2025-05-09 11:33:46 -07:00
committed by GitHub
parent 93817643ee
commit 78843c3940
3 changed files with 37 additions and 10 deletions

View File

@@ -21,7 +21,7 @@ codex-ansi-escape = { path = "../ansi-escape" }
codex-core = { path = "../core" } codex-core = { path = "../core" }
codex-common = { path = "../common", features = ["cli", "elapsed"] } codex-common = { path = "../common", features = ["cli", "elapsed"] }
color-eyre = "0.6.3" color-eyre = "0.6.3"
crossterm = "0.28.1" crossterm = { version = "0.28.1", features = ["bracketed-paste"] }
mcp-types = { path = "../mcp-types" } mcp-types = { path = "../mcp-types" }
ratatui = { version = "0.29.0", features = [ ratatui = { version = "0.29.0", features = [
"unstable-widget-ref", "unstable-widget-ref",

View File

@@ -47,29 +47,52 @@ impl App<'_> {
let app_event_tx = app_event_tx.clone(); let app_event_tx = app_event_tx.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
while let Ok(event) = crossterm::event::read() { while let Ok(event) = crossterm::event::read() {
let app_event = match event { match event {
crossterm::event::Event::Key(key_event) => AppEvent::KeyEvent(key_event), crossterm::event::Event::Key(key_event) => {
crossterm::event::Event::Resize(_, _) => AppEvent::Redraw, if let Err(e) = app_event_tx.send(AppEvent::KeyEvent(key_event)) {
tracing::error!("failed to send key event: {e}");
}
}
crossterm::event::Event::Resize(_, _) => {
if let Err(e) = app_event_tx.send(AppEvent::Redraw) {
tracing::error!("failed to send resize event: {e}");
}
}
crossterm::event::Event::Mouse(MouseEvent { crossterm::event::Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp, kind: MouseEventKind::ScrollUp,
.. ..
}) => { }) => {
scroll_event_helper.scroll_up(); scroll_event_helper.scroll_up();
continue;
} }
crossterm::event::Event::Mouse(MouseEvent { crossterm::event::Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown, kind: MouseEventKind::ScrollDown,
.. ..
}) => { }) => {
scroll_event_helper.scroll_down(); scroll_event_helper.scroll_down();
continue; }
crossterm::event::Event::Paste(pasted) => {
use crossterm::event::KeyModifiers;
for ch in pasted.chars() {
let key_event = match ch {
'\n' | '\r' => {
// Represent newline as <Shift+Enter> so that the bottom
// pane treats it as a literal newline instead of a submit
// action (submission is only triggered on Enter *without*
// any modifiers).
KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT)
}
_ => KeyEvent::new(KeyCode::Char(ch), KeyModifiers::empty()),
};
if let Err(e) = app_event_tx.send(AppEvent::KeyEvent(key_event)) {
tracing::error!("failed to send pasted key event: {e}");
break;
}
}
} }
_ => { _ => {
continue; // Ignore any other events.
} }
};
if let Err(e) = app_event_tx.send(app_event) {
tracing::error!("failed to send event: {e}");
} }
} }
}); });

View File

@@ -2,7 +2,9 @@ use std::io::Stdout;
use std::io::stdout; use std::io::stdout;
use std::io::{self}; use std::io::{self};
use crossterm::event::DisableBracketedPaste;
use crossterm::event::DisableMouseCapture; use crossterm::event::DisableMouseCapture;
use crossterm::event::EnableBracketedPaste;
use crossterm::event::EnableMouseCapture; use crossterm::event::EnableMouseCapture;
use ratatui::Terminal; use ratatui::Terminal;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
@@ -19,6 +21,7 @@ pub type Tui = Terminal<CrosstermBackend<Stdout>>;
pub fn init() -> io::Result<Tui> { pub fn init() -> io::Result<Tui> {
execute!(stdout(), EnterAlternateScreen)?; execute!(stdout(), EnterAlternateScreen)?;
execute!(stdout(), EnableMouseCapture)?; execute!(stdout(), EnableMouseCapture)?;
execute!(stdout(), EnableBracketedPaste)?;
enable_raw_mode()?; enable_raw_mode()?;
set_panic_hook(); set_panic_hook();
Terminal::new(CrosstermBackend::new(stdout())) Terminal::new(CrosstermBackend::new(stdout()))
@@ -35,6 +38,7 @@ fn set_panic_hook() {
/// Restore the terminal to its original state /// Restore the terminal to its original state
pub fn restore() -> io::Result<()> { pub fn restore() -> io::Result<()> {
execute!(stdout(), DisableMouseCapture)?; execute!(stdout(), DisableMouseCapture)?;
execute!(stdout(), DisableBracketedPaste)?;
execute!(stdout(), LeaveAlternateScreen)?; execute!(stdout(), LeaveAlternateScreen)?;
disable_raw_mode()?; disable_raw_mode()?;
Ok(()) Ok(())