mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
## Why Several configuration toggles required restarting Codex, and settings search only filtered the current group. Narrow terminals also truncated setting values and descriptions. ## What changed - Apply animations, composer stars, paste detection, automatic recaps, and tips without restarting Codex. Update active motion effects and paste detection when their preferences change. - Group animations and composer stars in an Effects picker that refreshes after saving and requires animations for composer stars. - Search across all configuration groups, show each result's group, and preserve the query when switching groups. - Keep setting values visible in narrow terminals and show the selected description below the list. Reserve space to keep the panel height stable and tailor footer hints to the selected action. - Simplify configuration notices and omit routine save confirmations. ## Testing Add and update tests and snapshots for live preference saves while offline, paste detection updates, Effects picker refreshes, global search, and stable panel height at narrow widths. GitOrigin-RevId: 83773da2f6457ce461ef16f0cddde0df640a36c3
232 lines
8.4 KiB
Rust
232 lines
8.4 KiB
Rust
//! Clipboard text paste from Windows shortcuts, right-click, or X11 middle-click PRIMARY.
|
|
//! The policy also reports why a configured mode is unavailable in this terminal.
|
|
//! Existing selection handlers run first. Only a draw may deliver a read, so real input
|
|
//! invalidates pending paste before completion and is never replaced by clipboard text.
|
|
//! Releasing or pressing another paste shortcut does not cancel its pending paste.
|
|
|
|
use super::*;
|
|
use crate::clipboard_copy::worker::PasteSource;
|
|
use crate::transcript_mode::TranscriptMode;
|
|
use crate::tui::VscodeDetection;
|
|
use codex_config::types::RightClickPaste;
|
|
use crossterm::event::MouseButton;
|
|
use crossterm::event::MouseEvent;
|
|
use crossterm::event::MouseEventKind;
|
|
|
|
pub(super) struct PendingPaste {
|
|
target: (Option<ThreadId>, crate::bottom_pane::ClipboardPasteTarget),
|
|
source: PasteSource,
|
|
shortcut: Option<KeyCode>,
|
|
_request: Arc<()>,
|
|
}
|
|
|
|
pub(crate) struct PasteEnvironment {
|
|
pub(super) primary: bool,
|
|
pub(super) platform_default: bool,
|
|
pub(super) ssh: bool,
|
|
pub(super) wsl: bool,
|
|
pub(super) vscode: VscodeDetection,
|
|
}
|
|
|
|
impl PasteEnvironment {
|
|
pub(crate) fn detect() -> Self {
|
|
Self {
|
|
primary: crate::clipboard_copy::primary::available(),
|
|
platform_default: cfg!(any(target_os = "windows", target_os = "linux")),
|
|
ssh: crate::clipboard_copy::is_ssh_session(),
|
|
wsl: crate::clipboard_copy::is_wsl_session(),
|
|
vscode: tui::detect_vscode_terminal(),
|
|
}
|
|
}
|
|
|
|
pub(super) fn allows(&self, mode: RightClickPaste, transcript_mode: TranscriptMode) -> bool {
|
|
self.status(mode, transcript_mode)
|
|
.unwrap_or(/*default*/ false)
|
|
}
|
|
|
|
/// Return the effective state or why this terminal must handle paste itself.
|
|
pub(crate) fn status(
|
|
&self,
|
|
mode: RightClickPaste,
|
|
transcript_mode: TranscriptMode,
|
|
) -> Result<bool, &'static str> {
|
|
if mode == RightClickPaste::Off {
|
|
return Ok(false);
|
|
}
|
|
if !transcript_mode.is_owned() {
|
|
return Err("Unavailable in inline mode; use your terminal's paste command");
|
|
}
|
|
if self.ssh {
|
|
return Err("Unavailable over SSH; use your terminal's paste command");
|
|
}
|
|
if self.vscode == VscodeDetection::VsCode {
|
|
return Err("VS Code handles pasting; use its terminal paste command");
|
|
}
|
|
if mode == RightClickPaste::Auto && self.wsl && self.vscode == VscodeDetection::Unknown {
|
|
return Err(
|
|
"Auto cannot identify this WSL terminal; use On or your terminal's paste command",
|
|
);
|
|
}
|
|
match mode {
|
|
RightClickPaste::Off => Ok(false),
|
|
RightClickPaste::On if cfg!(target_os = "android") => Err("Unavailable on Android"),
|
|
RightClickPaste::On => Ok(true),
|
|
RightClickPaste::Auto => Ok(self.platform_default),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
pub(super) fn right_click_paste_target(
|
|
&self,
|
|
tui: &tui::Tui,
|
|
source: PasteSource,
|
|
shortcut: Option<KeyCode>,
|
|
) -> Option<(Option<ThreadId>, crate::bottom_pane::ClipboardPasteTarget)> {
|
|
let allowed = shortcut.is_some()
|
|
|| match source {
|
|
PasteSource::Clipboard => {
|
|
!self.transcript_view.has_selection_range()
|
|
&& !self
|
|
.chat_widget
|
|
.rendered_selection
|
|
.borrow()
|
|
.view
|
|
.has_selection_range()
|
|
&& self.right_click_paste_environment.allows(
|
|
self.local_settings.tui.right_click_paste,
|
|
self.local_settings.transcript_mode,
|
|
)
|
|
}
|
|
PasteSource::Primary => self.right_click_paste_environment.primary,
|
|
};
|
|
if (shortcut.is_none() && !tui.is_owned_screen())
|
|
|| self.overlay.is_some()
|
|
|| self.transcript_view.is_search_editing()
|
|
|| !allowed
|
|
{
|
|
return None;
|
|
}
|
|
Some((
|
|
self.current_displayed_thread_id(),
|
|
self.chat_widget.clipboard_paste_target(shortcut)?,
|
|
))
|
|
}
|
|
|
|
pub(super) fn start_right_click_paste(&mut self, tui: &mut tui::Tui, mouse: MouseEvent) {
|
|
let source = match mouse.kind {
|
|
MouseEventKind::Down(MouseButton::Right) => PasteSource::Clipboard,
|
|
MouseEventKind::Down(MouseButton::Middle) => PasteSource::Primary,
|
|
_ => return,
|
|
};
|
|
if !mouse.modifiers.is_empty() || self.pending_right_click_paste.is_some() {
|
|
return;
|
|
}
|
|
self.start_clipboard_text_paste(tui, source, /*shortcut*/ None);
|
|
}
|
|
|
|
pub(crate) fn start_clipboard_text_paste(
|
|
&mut self,
|
|
tui: &mut tui::Tui,
|
|
source: PasteSource,
|
|
shortcut: Option<KeyCode>,
|
|
) {
|
|
let Some(target) = self.right_click_paste_target(tui, source, shortcut) else {
|
|
return;
|
|
};
|
|
match tui.clipboard.read_text(source, tui.frame_requester()) {
|
|
Ok(Some(request)) => {
|
|
self.pending_right_click_paste = Some(PendingPaste {
|
|
target,
|
|
source,
|
|
shortcut,
|
|
_request: request,
|
|
});
|
|
tui.frame_requester().schedule_frame();
|
|
}
|
|
Ok(None) => {}
|
|
Err(error) => self.chat_widget.add_error_message(error),
|
|
}
|
|
}
|
|
|
|
/// Invalidate before polling, even when the worker has already completed.
|
|
pub(super) fn invalidate_right_click_paste(&mut self, event: &TuiEvent) {
|
|
let Some(pending) = &self.pending_right_click_paste else {
|
|
return;
|
|
};
|
|
let button = match pending.source {
|
|
PasteSource::Clipboard => MouseButton::Right,
|
|
PasteSource::Primary => MouseButton::Middle,
|
|
};
|
|
let keep = match event {
|
|
TuiEvent::Draw | TuiEvent::Resize(_) | TuiEvent::FocusGained => true,
|
|
TuiEvent::Mouse(mouse) => {
|
|
mouse.kind == MouseEventKind::Moved
|
|
|| (pending.shortcut.is_none()
|
|
&& (mouse.kind == MouseEventKind::Up(button)
|
|
|| (mouse.kind == MouseEventKind::Down(button)
|
|
&& mouse.modifiers.is_empty())))
|
|
}
|
|
TuiEvent::Key(key) => {
|
|
pending.shortcut.is_some()
|
|
&& match key.code {
|
|
KeyCode::Insert => {
|
|
key.kind == KeyEventKind::Release
|
|
|| key.modifiers == KeyModifiers::SHIFT
|
|
}
|
|
KeyCode::Char(c) if c.eq_ignore_ascii_case(&'v') => {
|
|
key.kind == KeyEventKind::Release
|
|
|| key.modifiers == KeyModifiers::CONTROL
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
TuiEvent::Paste(_) | TuiEvent::FocusLost | TuiEvent::Resume => false,
|
|
};
|
|
if !keep {
|
|
self.pending_right_click_paste = None;
|
|
}
|
|
}
|
|
|
|
pub(super) fn finish_right_click_paste(
|
|
&mut self,
|
|
tui: &mut tui::Tui,
|
|
event: TuiEvent,
|
|
) -> TuiEvent {
|
|
if !matches!(event, TuiEvent::Draw) {
|
|
return event;
|
|
}
|
|
let Some(pending) = self.pending_right_click_paste.as_ref() else {
|
|
tui.clipboard.take_text_result();
|
|
return event;
|
|
};
|
|
if self
|
|
.right_click_paste_target(tui, pending.source, pending.shortcut)
|
|
.as_ref()
|
|
!= Some(&pending.target)
|
|
{
|
|
self.pending_right_click_paste = None;
|
|
return event;
|
|
}
|
|
let Some(result) = tui.clipboard.take_text_result() else {
|
|
return event;
|
|
};
|
|
self.pending_right_click_paste = None;
|
|
match result {
|
|
Ok(text) if !text.is_empty() => {
|
|
tui.frame_requester().schedule_frame();
|
|
TuiEvent::Paste(text)
|
|
}
|
|
Ok(_) => event,
|
|
Err(error) => {
|
|
self.chat_widget.add_error_message(error);
|
|
event
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "right_click_paste_tests.rs"]
|
|
mod tests;
|