mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
handle paste
This commit is contained in:
@@ -104,6 +104,8 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
|
||||
|
||||
// Terminal setup
|
||||
use crossterm::ExecutableCommand;
|
||||
use crossterm::event::DisableBracketedPaste;
|
||||
use crossterm::event::EnableBracketedPaste;
|
||||
use crossterm::event::KeyboardEnhancementFlags;
|
||||
use crossterm::event::PopKeyboardEnhancementFlags;
|
||||
use crossterm::event::PushKeyboardEnhancementFlags;
|
||||
@@ -116,6 +118,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
|
||||
let mut stdout = std::io::stdout();
|
||||
enable_raw_mode()?;
|
||||
stdout.execute(EnterAlternateScreen)?;
|
||||
stdout.execute(EnableBracketedPaste)?;
|
||||
// Enable enhanced key reporting so Shift+Enter is distinguishable from Enter.
|
||||
// Some terminals may not support these flags; ignore errors if enabling fails.
|
||||
let _ = crossterm::execute!(
|
||||
@@ -648,6 +651,27 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
|
||||
}
|
||||
maybe_event = events.next() => {
|
||||
match maybe_event {
|
||||
Some(Ok(Event::Paste(pasted))) => {
|
||||
if app.env_modal.is_some() {
|
||||
if let Some(m) = app.env_modal.as_mut() {
|
||||
for ch in pasted.chars() {
|
||||
match ch {
|
||||
'\r' | '\n' => continue,
|
||||
'\t' => m.query.push(' '),
|
||||
_ => m.query.push(ch),
|
||||
}
|
||||
}
|
||||
}
|
||||
needs_redraw = true;
|
||||
} else if let Some(page) = app.new_task.as_mut() {
|
||||
if !page.submitting {
|
||||
if page.composer.handle_paste(pasted) {
|
||||
needs_redraw = true;
|
||||
}
|
||||
let _ = frame_tx.send(Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(Event::Key(key))) if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) => {
|
||||
// Treat Ctrl-C like pressing 'q' in the current context.
|
||||
if key.modifiers.contains(KeyModifiers::CONTROL)
|
||||
@@ -1313,6 +1337,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
|
||||
// Restore terminal
|
||||
disable_raw_mode().ok();
|
||||
terminal.show_cursor().ok();
|
||||
let _ = crossterm::execute!(std::io::stdout(), DisableBracketedPaste);
|
||||
// Best-effort restore of keyboard enhancement flags before leaving alt screen.
|
||||
let _ = crossterm::execute!(std::io::stdout(), PopKeyboardEnhancementFlags);
|
||||
let _ = crossterm::execute!(std::io::stdout(), LeaveAlternateScreen);
|
||||
|
||||
@@ -2,6 +2,7 @@ use ratatui::layout::Constraint;
|
||||
use ratatui::layout::Direction;
|
||||
use ratatui::layout::Layout;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
@@ -20,6 +21,7 @@ use crate::app::App;
|
||||
use crate::app::AttemptView;
|
||||
use chrono::Local;
|
||||
use chrono::Utc;
|
||||
use codex_cloud_tasks_client::AttemptStatus;
|
||||
use codex_cloud_tasks_client::TaskStatus;
|
||||
|
||||
pub fn draw(frame: &mut Frame, app: &mut App) {
|
||||
@@ -385,7 +387,9 @@ fn draw_diff_overlay(frame: &mut Frame, area: Rect, app: &mut App) {
|
||||
{
|
||||
spans.extend(vec![
|
||||
" ".into(),
|
||||
format!("Attempt {}/{}", ov.selected_attempt + 1, total).dim(),
|
||||
format!("Attempt {}/{}", ov.selected_attempt + 1, total)
|
||||
.bold()
|
||||
.dim(),
|
||||
" ".into(),
|
||||
"(Tab/Shift-Tab or [ ] to cycle attempts)".dim(),
|
||||
]);
|
||||
@@ -415,29 +419,10 @@ fn draw_diff_overlay(frame: &mut Frame, area: Rect, app: &mut App) {
|
||||
.map(|l| style_diff_line(l))
|
||||
.collect()
|
||||
} else {
|
||||
let mut in_code = false;
|
||||
let raw = app.diff_overlay.as_ref().map(|o| o.sd.wrapped_lines());
|
||||
raw.unwrap_or(&[])
|
||||
.iter()
|
||||
.map(|raw| {
|
||||
if raw.trim_start().starts_with("```") {
|
||||
in_code = !in_code;
|
||||
return Line::from(raw.to_string().cyan());
|
||||
}
|
||||
if in_code {
|
||||
return Line::from(raw.to_string().cyan());
|
||||
}
|
||||
let s = raw.trim_start();
|
||||
if s.starts_with("### ") || s.starts_with("## ") || s.starts_with("# ") {
|
||||
return Line::from(raw.to_string().magenta().bold());
|
||||
}
|
||||
if s.starts_with("- ") || s.starts_with("* ") {
|
||||
let rest = &s[2..];
|
||||
return Line::from(vec!["• ".into(), rest.to_string().into()]);
|
||||
}
|
||||
Line::from(raw.to_string())
|
||||
})
|
||||
.collect()
|
||||
app.diff_overlay
|
||||
.as_ref()
|
||||
.map(|o| style_conversation_lines(&o.sd, o.current_attempt()))
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let raw_empty = app
|
||||
.diff_overlay
|
||||
@@ -540,6 +525,197 @@ pub fn draw_apply_modal(frame: &mut Frame, area: Rect, app: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ConversationSpeaker {
|
||||
User,
|
||||
Assistant,
|
||||
}
|
||||
|
||||
fn style_conversation_lines(
|
||||
sd: &crate::scrollable_diff::ScrollableDiff,
|
||||
attempt: Option<&AttemptView>,
|
||||
) -> Vec<Line<'static>> {
|
||||
use ratatui::text::Span;
|
||||
|
||||
let wrapped = sd.wrapped_lines();
|
||||
if wrapped.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let indices = sd.wrapped_src_indices();
|
||||
let mut styled: Vec<Line<'static>> = Vec::new();
|
||||
let mut speaker: Option<ConversationSpeaker> = None;
|
||||
let mut in_code = false;
|
||||
let mut last_src: Option<usize> = None;
|
||||
let mut bullet_indent: Option<usize> = None;
|
||||
|
||||
for (display, &src_idx) in wrapped.iter().zip(indices.iter()) {
|
||||
let raw = sd.raw_line_at(src_idx);
|
||||
let trimmed = raw.trim();
|
||||
let is_new_raw = last_src.map(|prev| prev != src_idx).unwrap_or(true);
|
||||
|
||||
if trimmed.eq_ignore_ascii_case("user:") {
|
||||
speaker = Some(ConversationSpeaker::User);
|
||||
in_code = false;
|
||||
bullet_indent = None;
|
||||
styled.push(conversation_header_line(ConversationSpeaker::User, None));
|
||||
last_src = Some(src_idx);
|
||||
continue;
|
||||
}
|
||||
if trimmed.eq_ignore_ascii_case("assistant:") {
|
||||
speaker = Some(ConversationSpeaker::Assistant);
|
||||
in_code = false;
|
||||
bullet_indent = None;
|
||||
styled.push(conversation_header_line(
|
||||
ConversationSpeaker::Assistant,
|
||||
attempt,
|
||||
));
|
||||
last_src = Some(src_idx);
|
||||
continue;
|
||||
}
|
||||
if raw.is_empty() {
|
||||
let mut spans: Vec<Span> = Vec::new();
|
||||
if let Some(role) = speaker {
|
||||
spans.push(conversation_gutter_span(role));
|
||||
} else {
|
||||
spans.push(Span::raw(String::new()));
|
||||
}
|
||||
styled.push(Line::from(spans));
|
||||
last_src = Some(src_idx);
|
||||
bullet_indent = None;
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_new_raw {
|
||||
let trimmed_start = raw.trim_start();
|
||||
if trimmed_start.starts_with("```") {
|
||||
in_code = !in_code;
|
||||
bullet_indent = None;
|
||||
} else if !in_code
|
||||
&& (trimmed_start.starts_with("- ") || trimmed_start.starts_with("* "))
|
||||
{
|
||||
let indent = raw.chars().take_while(|c| c.is_whitespace()).count();
|
||||
bullet_indent = Some(indent);
|
||||
} else if !in_code {
|
||||
bullet_indent = None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut spans: Vec<Span> = Vec::new();
|
||||
if let Some(role) = speaker {
|
||||
spans.push(conversation_gutter_span(role));
|
||||
}
|
||||
|
||||
spans.extend(conversation_text_spans(
|
||||
display,
|
||||
in_code,
|
||||
is_new_raw,
|
||||
bullet_indent,
|
||||
));
|
||||
|
||||
styled.push(Line::from(spans));
|
||||
last_src = Some(src_idx);
|
||||
}
|
||||
|
||||
if styled.is_empty() {
|
||||
wrapped.iter().map(|l| Line::from(l.to_string())).collect()
|
||||
} else {
|
||||
styled
|
||||
}
|
||||
}
|
||||
|
||||
fn conversation_header_line(
|
||||
speaker: ConversationSpeaker,
|
||||
attempt: Option<&AttemptView>,
|
||||
) -> Line<'static> {
|
||||
use ratatui::text::Span;
|
||||
|
||||
let mut spans: Vec<Span> = vec!["╭ ".dim()];
|
||||
match speaker {
|
||||
ConversationSpeaker::User => {
|
||||
spans.push("User".cyan().bold());
|
||||
spans.push(" prompt".dim());
|
||||
}
|
||||
ConversationSpeaker::Assistant => {
|
||||
spans.push("Assistant".magenta().bold());
|
||||
spans.push(" response".dim());
|
||||
if let Some(attempt) = attempt {
|
||||
if let Some(status_span) = attempt_status_span(attempt.status) {
|
||||
spans.push(" • ".dim());
|
||||
spans.push(status_span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
fn conversation_gutter_span(speaker: ConversationSpeaker) -> ratatui::text::Span<'static> {
|
||||
match speaker {
|
||||
ConversationSpeaker::User => "│ ".cyan().dim(),
|
||||
ConversationSpeaker::Assistant => "│ ".magenta().dim(),
|
||||
}
|
||||
}
|
||||
|
||||
fn conversation_text_spans(
|
||||
display: &str,
|
||||
in_code: bool,
|
||||
is_new_raw: bool,
|
||||
bullet_indent: Option<usize>,
|
||||
) -> Vec<ratatui::text::Span<'static>> {
|
||||
use ratatui::text::Span;
|
||||
|
||||
if in_code {
|
||||
return vec![Span::styled(
|
||||
display.to_string(),
|
||||
Style::default().fg(Color::Cyan),
|
||||
)];
|
||||
}
|
||||
|
||||
let trimmed = display.trim_start();
|
||||
|
||||
if let Some(indent) = bullet_indent {
|
||||
if is_new_raw {
|
||||
let rest = trimmed.get(2..).unwrap_or("").trim_start();
|
||||
let mut spans: Vec<Span> = Vec::new();
|
||||
if indent > 0 {
|
||||
spans.push(Span::raw(" ".repeat(indent)));
|
||||
}
|
||||
spans.push("• ".into());
|
||||
spans.push(Span::raw(rest.to_string()));
|
||||
return spans;
|
||||
}
|
||||
let mut continuation = String::new();
|
||||
continuation.push_str(&" ".repeat(indent + 2));
|
||||
continuation.push_str(trimmed);
|
||||
return vec![Span::raw(continuation)];
|
||||
}
|
||||
|
||||
if is_new_raw
|
||||
&& (trimmed.starts_with("### ") || trimmed.starts_with("## ") || trimmed.starts_with("# "))
|
||||
{
|
||||
return vec![Span::styled(
|
||||
display.to_string(),
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)];
|
||||
}
|
||||
|
||||
vec![Span::raw(display.to_string())]
|
||||
}
|
||||
|
||||
fn attempt_status_span(status: AttemptStatus) -> Option<ratatui::text::Span<'static>> {
|
||||
match status {
|
||||
AttemptStatus::Completed => Some("Completed".green()),
|
||||
AttemptStatus::Failed => Some("Failed".red().bold()),
|
||||
AttemptStatus::InProgress => Some("In progress".magenta()),
|
||||
AttemptStatus::Pending => Some("Pending".yellow()),
|
||||
AttemptStatus::Cancelled => Some("Cancelled".yellow().dim()),
|
||||
AttemptStatus::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn style_diff_line(raw: &str) -> Line<'static> {
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
|
||||
@@ -58,6 +58,10 @@ impl ComposerInput {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_paste(&mut self, pasted: String) -> bool {
|
||||
self.inner.handle_paste(pasted)
|
||||
}
|
||||
|
||||
/// Override the footer hint items displayed under the composer.
|
||||
/// Each tuple is rendered as "<key> <label>", with keys styled.
|
||||
pub fn set_hint_items(&mut self, items: Vec<(impl Into<String>, impl Into<String>)>) {
|
||||
|
||||
Reference in New Issue
Block a user