Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 36 additions & 44 deletions codex-rs/tui2/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::ApprovalRequest;
use crate::chatwidget::ChatWidget;
use crate::clipboard_copy;
use crate::custom_terminal::Frame;
use crate::diff_render::DiffSummary;
use crate::exec_command::strip_bash_lc_and_escape;
Expand All @@ -17,6 +16,8 @@ use crate::pager_overlay::Overlay;
use crate::render::highlight::highlight_bash_to_lines;
use crate::render::renderable::Renderable;
use crate::resume_picker::ResumeSelection;
use crate::transcript_copy_action::TranscriptCopyAction;
use crate::transcript_copy_action::TranscriptCopyFeedback;
use crate::transcript_copy_ui::TranscriptCopyUi;
use crate::transcript_multi_click::TranscriptMultiClick;
use crate::transcript_selection::TRANSCRIPT_GUTTER_COLS;
Expand Down Expand Up @@ -335,6 +336,7 @@ pub(crate) struct App {
transcript_view_top: usize,
transcript_total_lines: usize,
transcript_copy_ui: TranscriptCopyUi,
transcript_copy_action: TranscriptCopyAction,

// Pager overlay state (Transcript or Static like Diff)
pub(crate) overlay: Option<Overlay>,
Expand Down Expand Up @@ -500,6 +502,7 @@ impl App {
transcript_view_top: 0,
transcript_total_lines: 0,
transcript_copy_ui: TranscriptCopyUi::new_with_shortcut(copy_selection_shortcut),
transcript_copy_action: TranscriptCopyAction::default(),
overlay: None,
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
Expand Down Expand Up @@ -667,11 +670,14 @@ impl App {
self.transcript_total_lines,
))
};
let copy_selection_key = self.copy_selection_key();
let copy_feedback = self.transcript_copy_feedback_for_footer();
self.chat_widget.set_transcript_ui_state(
transcript_scrolled,
selection_active,
scroll_position,
self.copy_selection_key(),
copy_selection_key,
copy_feedback,
);
}
}
Expand Down Expand Up @@ -893,7 +899,14 @@ impl App {
.transcript_copy_ui
.hit_test(mouse_event.column, mouse_event.row)
{
self.copy_transcript_selection(tui);
if self.transcript_copy_action.copy_and_handle(
tui,
chat_height,
&self.transcript_cells,
self.transcript_selection,
) {
self.transcript_selection = TranscriptSelection::default();
}
return;
}

Expand Down Expand Up @@ -1239,46 +1252,8 @@ impl App {
}
}

/// Copy the currently selected transcript region to the system clipboard.
///
/// The selection is defined in terms of flattened wrapped transcript line
/// indices and columns, and this method reconstructs the same wrapped
/// transcript used for on-screen rendering so the copied text closely
/// matches the highlighted region.
///
/// Important: copy operates on the selection's full content-relative range,
/// not just the current viewport. A selection can extend outside the visible
/// region (for example, by scrolling after selecting, or by selecting while
/// autoscrolling), and we still want the clipboard payload to reflect the
/// entire selected transcript.
fn copy_transcript_selection(&mut self, tui: &tui::Tui) {
let size = tui.terminal.last_known_screen_size;
let width = size.width;
let height = size.height;
if width == 0 || height == 0 {
return;
}

let chat_height = self.chat_widget.desired_height(width);
if chat_height >= height {
return;
}

let transcript_height = height.saturating_sub(chat_height);
if transcript_height == 0 {
return;
}

let Some(text) = crate::transcript_copy::selection_to_copy_text_for_cells(
&self.transcript_cells,
self.transcript_selection,
width,
) else {
return;
};
if let Err(err) = clipboard_copy::copy_text(text) {
tracing::error!(error = %err, "failed to copy selection to clipboard");
}
fn transcript_copy_feedback_for_footer(&mut self) -> Option<TranscriptCopyFeedback> {
self.transcript_copy_action.footer_feedback()
}

fn copy_selection_key(&self) -> crate::key_hint::KeyBinding {
Expand Down Expand Up @@ -1902,7 +1877,22 @@ impl App {
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
} if self.transcript_copy_ui.is_copy_key(ch, modifiers) => {
self.copy_transcript_selection(tui);
let size = tui.terminal.last_known_screen_size;
let width = size.width;
let height = size.height;
if width == 0 || height == 0 {
return;
}

let chat_height = self.chat_widget.desired_height(width);
if self.transcript_copy_action.copy_and_handle(
tui,
chat_height,
&self.transcript_cells,
self.transcript_selection,
) {
self.transcript_selection = TranscriptSelection::default();
}
}
KeyEvent {
code: KeyCode::PageUp,
Expand Down Expand Up @@ -2093,6 +2083,7 @@ mod tests {
transcript_copy_ui: TranscriptCopyUi::new_with_shortcut(
CopySelectionShortcut::CtrlShiftC,
),
transcript_copy_action: TranscriptCopyAction::default(),
overlay: None,
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
Expand Down Expand Up @@ -2144,6 +2135,7 @@ mod tests {
transcript_copy_ui: TranscriptCopyUi::new_with_shortcut(
CopySelectionShortcut::CtrlShiftC,
),
transcript_copy_action: TranscriptCopyAction::default(),
overlay: None,
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/tui2/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::key_hint::has_ctrl_or_alt;
use crate::transcript_copy_action::TranscriptCopyFeedback;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
Expand Down Expand Up @@ -124,6 +125,7 @@ pub(crate) struct ChatComposer {
transcript_selection_active: bool,
transcript_scroll_position: Option<(usize, usize)>,
transcript_copy_selection_key: KeyBinding,
transcript_copy_feedback: Option<TranscriptCopyFeedback>,
skills: Option<Vec<SkillMetadata>>,
dismissed_skill_popup_token: Option<String>,
}
Expand Down Expand Up @@ -176,6 +178,7 @@ impl ChatComposer {
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
skills: None,
dismissed_skill_popup_token: None,
};
Expand Down Expand Up @@ -1545,6 +1548,7 @@ impl ChatComposer {
transcript_selection_active: self.transcript_selection_active,
transcript_scroll_position: self.transcript_scroll_position,
transcript_copy_selection_key: self.transcript_copy_selection_key,
transcript_copy_feedback: self.transcript_copy_feedback,
}
}

Expand Down Expand Up @@ -1577,11 +1581,13 @@ impl ChatComposer {
selection_active: bool,
scroll_position: Option<(usize, usize)>,
copy_selection_key: KeyBinding,
copy_feedback: Option<TranscriptCopyFeedback>,
) -> bool {
if self.transcript_scrolled == scrolled
&& self.transcript_selection_active == selection_active
&& self.transcript_scroll_position == scroll_position
&& self.transcript_copy_selection_key == copy_selection_key
&& self.transcript_copy_feedback == copy_feedback
{
return false;
}
Expand All @@ -1590,6 +1596,7 @@ impl ChatComposer {
self.transcript_selection_active = selection_active;
self.transcript_scroll_position = scroll_position;
self.transcript_copy_selection_key = copy_selection_key;
self.transcript_copy_feedback = copy_feedback;
true
}

Expand Down
49 changes: 47 additions & 2 deletions codex-rs/tui2/src/bottom_pane/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::render::line_utils::prefix_lines;
use crate::status::format_tokens_compact;
use crate::transcript_copy_action::TranscriptCopyFeedback;
use crate::ui_consts::FOOTER_INDENT_COLS;
use crossterm::event::KeyCode;
use ratatui::buffer::Buffer;
Expand All @@ -26,6 +27,7 @@ pub(crate) struct FooterProps {
pub(crate) transcript_selection_active: bool,
pub(crate) transcript_scroll_position: Option<(usize, usize)>,
pub(crate) transcript_copy_selection_key: KeyBinding,
pub(crate) transcript_copy_feedback: Option<TranscriptCopyFeedback>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -80,11 +82,26 @@ pub(crate) fn render_footer(area: Rect, buf: &mut Buffer, props: FooterProps) {
}

fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
fn apply_copy_feedback(lines: &mut [Line<'static>], feedback: Option<TranscriptCopyFeedback>) {
let Some(line) = lines.first_mut() else {
return;
};
let Some(feedback) = feedback else {
return;
};

line.push_span(" · ".dim());
match feedback {
TranscriptCopyFeedback::Copied => line.push_span("Copied".green().bold()),
TranscriptCopyFeedback::Failed => line.push_span("Copy failed".red().bold()),
}
}

// Show the context indicator on the left, appended after the primary hint
// (e.g., "? for shortcuts"). Keep it visible even when typing (i.e., when
// the shortcut hint is hidden). Hide it only for the multi-line
// ShortcutOverlay.
match props.mode {
let mut lines = match props.mode {
FooterMode::CtrlCReminder => vec![ctrl_c_reminder_line(CtrlCReminderState {
is_task_running: props.is_task_running,
})],
Expand Down Expand Up @@ -139,7 +156,9 @@ fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
props.context_window_percent,
props.context_window_used_tokens,
)],
}
};
apply_copy_feedback(&mut lines, props.transcript_copy_feedback);
lines
}

#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -469,6 +488,7 @@ mod tests {
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
},
);

Expand All @@ -485,6 +505,7 @@ mod tests {
transcript_selection_active: true,
transcript_scroll_position: Some((3, 42)),
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
},
);

Expand All @@ -501,6 +522,7 @@ mod tests {
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
},
);

Expand All @@ -517,6 +539,7 @@ mod tests {
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
},
);

Expand All @@ -533,6 +556,7 @@ mod tests {
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
},
);

Expand All @@ -549,6 +573,7 @@ mod tests {
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
},
);

Expand All @@ -565,6 +590,7 @@ mod tests {
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
},
);

Expand All @@ -581,6 +607,7 @@ mod tests {
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
},
);

Expand All @@ -597,6 +624,24 @@ mod tests {
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: None,
},
);

snapshot_footer(
"footer_copy_feedback_copied",
FooterProps {
mode: FooterMode::ShortcutSummary,
esc_backtrack_hint: false,
use_shift_enter_hint: false,
is_task_running: false,
context_window_percent: None,
context_window_used_tokens: None,
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
transcript_copy_feedback: Some(TranscriptCopyFeedback::Copied),
},
);
}
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/tui2/src/bottom_pane/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,12 +388,14 @@ impl BottomPane {
selection_active: bool,
scroll_position: Option<(usize, usize)>,
copy_selection_key: crate::key_hint::KeyBinding,
copy_feedback: Option<crate::transcript_copy_action::TranscriptCopyFeedback>,
) {
let updated = self.composer.set_transcript_ui_state(
scrolled,
selection_active,
scroll_position,
copy_selection_key,
copy_feedback,
);
if updated {
self.request_redraw();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
source: tui2/src/bottom_pane/footer.rs
assertion_line: 473
expression: terminal.backend()
---
" 100% context left · ? for shortcuts · Copied "
2 changes: 2 additions & 0 deletions codex-rs/tui2/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3096,12 +3096,14 @@ impl ChatWidget {
selection_active: bool,
scroll_position: Option<(usize, usize)>,
copy_selection_key: crate::key_hint::KeyBinding,
copy_feedback: Option<crate::transcript_copy_action::TranscriptCopyFeedback>,
) {
self.bottom_pane.set_transcript_ui_state(
scrolled,
selection_active,
scroll_position,
copy_selection_key,
copy_feedback,
);
}

Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ mod terminal_palette;
mod text_formatting;
mod tooltips;
mod transcript_copy;
mod transcript_copy_action;
mod transcript_copy_ui;
mod transcript_multi_click;
mod transcript_render;
Expand Down
Loading
Loading