mirror of
https://github.com/openai/codex.git
synced 2026-10-17 17:08:55 +00:00
Forward clipboard copies to the terminal in Herdr sessions (#52557)
## Why Herdr sessions can gain attached clients after Codex starts. Successful native clipboard copying should still forward the text to the attached terminal, even without inherited SSH variables. ## What changed Detect Herdr sessions with `HERDR_ENV=1` and enable terminal forwarding for `/copy` and `Ctrl+O` even when native copying succeeds. Use OSC 52 directly when outside tmux, while preserving the native copy result. ## Testing Add a routing test verifying that a Herdr session copies natively before sending OSC 52 and retains the successful native clipboard lease. GitOrigin-RevId: c43c08ca037ff51e902267dc8636889493f5c30c
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
//! Clipboard copy backend for the TUI's `/copy` command and `Ctrl+O` hotkey.
|
||||
//!
|
||||
//! Local copying uses the native clipboard, with WSL PowerShell as a fallback.
|
||||
//! In tmux, also forward to the attached terminal so clients attached after Codex
|
||||
//! started receive the copy. Over SSH without tmux, send OSC 52 directly.
|
||||
//! In tmux and Herdr, also forward to the attached terminal so clients attached after Codex
|
||||
//! started receive the copy. Over SSH without a multiplexer, send OSC 52 directly.
|
||||
//!
|
||||
//! Terminal writes have no delivery acknowledgement: a successful send must not
|
||||
//! suppress native copying. Both the host and attached client's clipboards may
|
||||
//! change. Outside tmux and SSH, use OSC 52 only if native copying fails.
|
||||
//! change. Outside tmux, Herdr, and SSH, use OSC 52 only if native copying fails.
|
||||
//!
|
||||
//! On Linux, X11 and some Wayland compositors require the process that wrote the
|
||||
//! clipboard to keep its handle open. `ClipboardLease` wraps the `arboard::Clipboard`
|
||||
@@ -83,8 +83,8 @@ impl CopyStatus {
|
||||
|
||||
/// Copy text to the system clipboard.
|
||||
///
|
||||
/// Try native copying, then independently attempt terminal forwarding in tmux or
|
||||
/// SSH. A terminal send is best effort and does not confirm clipboard delivery.
|
||||
/// Try native copying, then independently attempt terminal forwarding in tmux,
|
||||
/// Herdr, or SSH. A terminal send is best effort and does not confirm clipboard delivery.
|
||||
/// Terminal forwarding may replace native HTML with plain text.
|
||||
///
|
||||
/// Native or WSL success returns `Copied`, even if terminal forwarding fails.
|
||||
@@ -114,6 +114,7 @@ fn copy_to_clipboard(
|
||||
text,
|
||||
format,
|
||||
CopyEnvironment {
|
||||
herdr_session: std::env::var("HERDR_ENV").is_ok_and(|value| value == "1"),
|
||||
ssh_session: is_ssh_session(),
|
||||
wsl_session: is_wsl_session(),
|
||||
tmux_session: is_tmux_session(),
|
||||
@@ -159,6 +160,7 @@ impl ClipboardLease {
|
||||
/// without touching real clipboards or terminal I/O.
|
||||
#[derive(Clone, Copy)]
|
||||
struct CopyEnvironment {
|
||||
herdr_session: bool,
|
||||
ssh_session: bool,
|
||||
wsl_session: bool,
|
||||
tmux_session: bool,
|
||||
@@ -214,9 +216,11 @@ fn copy_to_clipboard_with(
|
||||
});
|
||||
// Copy natively first: an X11 SelectionClear from a terminal write can otherwise
|
||||
// race with arboard reusing its ownership window and clear the new native data.
|
||||
// Persistent tmux sessions may gain remote clients after Codex starts, so still
|
||||
// Persistent multiplexer sessions may gain remote clients after Codex starts, so still
|
||||
// forward even when no SSH variables were inherited or native copying succeeded.
|
||||
let terminal_result = (environment.tmux_session || environment.ssh_session).then(terminal_copy);
|
||||
let terminal_result =
|
||||
(environment.tmux_session || environment.herdr_session || environment.ssh_session)
|
||||
.then(terminal_copy);
|
||||
match native_result {
|
||||
Ok(lease) => Ok(CopyOutcome::Copied(lease)),
|
||||
Err(native_error) => terminal_result
|
||||
@@ -391,6 +395,7 @@ mod tests {
|
||||
|
||||
fn remote_environment() -> CopyEnvironment {
|
||||
CopyEnvironment {
|
||||
herdr_session: false,
|
||||
ssh_session: true,
|
||||
wsl_session: true,
|
||||
tmux_session: false,
|
||||
@@ -406,6 +411,7 @@ mod tests {
|
||||
|
||||
fn local_environment() -> CopyEnvironment {
|
||||
CopyEnvironment {
|
||||
herdr_session: false,
|
||||
ssh_session: false,
|
||||
wsl_session: false,
|
||||
tmux_session: false,
|
||||
|
||||
@@ -17,6 +17,7 @@ fn local_tmux_preserves_native_html_when_terminal_silently_rejects_copy() {
|
||||
"**hello**",
|
||||
CopyFormat::Markdown,
|
||||
CopyEnvironment {
|
||||
herdr_session: false,
|
||||
ssh_session: false,
|
||||
tmux_session: true,
|
||||
wsl_session: false,
|
||||
@@ -48,6 +49,7 @@ fn copy_uses_osc52_when_tmux_fails_even_if_native_copy_succeeds() {
|
||||
"hello",
|
||||
CopyFormat::PlainText,
|
||||
CopyEnvironment {
|
||||
herdr_session: false,
|
||||
ssh_session: false,
|
||||
tmux_session: true,
|
||||
wsl_session: false,
|
||||
@@ -73,12 +75,42 @@ fn copy_uses_osc52_when_tmux_fails_even_if_native_copy_succeeds() {
|
||||
|
||||
fn local_tmux_environment() -> CopyEnvironment {
|
||||
CopyEnvironment {
|
||||
herdr_session: false,
|
||||
tmux_session: true,
|
||||
ssh_session: false,
|
||||
wsl_session: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn herdr_forwards_to_osc52_even_when_native_copy_succeeds() {
|
||||
let calls = RefCell::new(Vec::new());
|
||||
let result = copy_to_clipboard_with(
|
||||
"hello",
|
||||
CopyFormat::PlainText,
|
||||
CopyEnvironment {
|
||||
herdr_session: true,
|
||||
ssh_session: false,
|
||||
tmux_session: false,
|
||||
wsl_session: false,
|
||||
},
|
||||
|_| panic!("Herdr should use OSC 52 directly"),
|
||||
|_| {
|
||||
calls.borrow_mut().push("osc52");
|
||||
Ok(())
|
||||
},
|
||||
|text, html| {
|
||||
assert_eq!((text, html), ("hello", None));
|
||||
calls.borrow_mut().push("native");
|
||||
Ok(Some(ClipboardLease::test()))
|
||||
},
|
||||
|_| panic!("native copy succeeded"),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Ok(CopyOutcome::Copied(Some(_)))));
|
||||
assert_eq!(calls.into_inner(), vec!["native", "osc52"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_tmux_copy_preserves_native_copy() {
|
||||
for (size, expected_calls) in [
|
||||
|
||||
Reference in New Issue
Block a user