Prefer the Windows clipboard and bound copy attempts on WSL (#53453)

## Why

WSLg clipboard initialization can block indefinitely, and PowerShell clipboard writes can stall while receiving input or exiting. These stalls can prevent subsequent TUI copy requests from completing.

## What changed

- Try the Windows clipboard first for `/copy` and `Ctrl+O` on WSL, falling back to the native clipboard and terminal delivery when needed.
- Give Windows executable discovery and writing a shared five-second deadline, with a separate one-second deadline for native clipboard setup. Reuse pending setup tasks after timeouts to avoid accumulating blocked threads.
- Launch PowerShell through a native shell and stop its process group on write failures or timeouts. Reap the writer before returning so an older copy cannot overwrite a newer one, and suppress stderr that could expose clipboard contents.

## Testing

Add tests for command compatibility, large Unicode input, expired requests, and cleanup of stalled writers and subprocesses. Update routing tests to cover Windows-first copying and native fallback with HTML and clipboard leases.

GitOrigin-RevId: 218c8541a169a16536db0370455e076a31a42b1f
This commit is contained in:
Eric Traut
2026-10-12 19:02:36 +00:00
committed by copyberry
parent 7a0a8ac9c4
commit 0c329ad2c9
4 changed files with 347 additions and 117 deletions
+69 -115
View File
@@ -1,6 +1,6 @@
//! 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.
//! Local copying uses the native clipboard, or the Windows clipboard directly on WSL.
//! 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.
//!
@@ -22,6 +22,7 @@
pub(crate) mod primary;
mod tmux;
pub(crate) mod worker;
mod wsl;
use base64::Engine;
use std::io::Write;
@@ -98,10 +99,24 @@ fn copy_to_clipboard(
begin_delivery: impl Fn() -> Result<(), String>,
osc52: impl Fn(&str) -> Result<(), String>,
) -> Result<CopyOutcome, String> {
let wsl_deadline = std::time::Instant::now() + std::time::Duration::from_secs(/*secs*/ 5);
let environment = 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(),
};
#[cfg(not(target_os = "android"))]
let native_copy = {
let clipboard = arboard::Clipboard::new();
move |text: &str, html: Option<&str>| arboard_copy(clipboard, text, html)
// Never contact WSLg before trying Windows: even native setup can block forever.
let clipboard = (!environment.wsl_session).then(arboard::Clipboard::new);
move |text: &str, html: Option<&str>| {
let clipboard = match clipboard {
Some(clipboard) => clipboard.map_err(|e| format!("clipboard unavailable: {e}"))?,
None => wsl::native_clipboard()?,
};
arboard_copy(clipboard, text, html)
}
};
#[cfg(target_os = "android")]
let native_copy = |_text: &str, _html: Option<&str>| {
@@ -113,16 +128,11 @@ fn copy_to_clipboard(
copy_to_clipboard_with(
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(),
},
environment,
tmux_clipboard_copy,
osc52,
native_copy,
wsl_clipboard_copy,
|text| wsl::copy(text, wsl_deadline),
)
}
@@ -198,22 +208,25 @@ fn copy_to_clipboard_with(
osc52_copy_fn(text)
}
};
let html = match format {
CopyFormat::PlainText => None,
CopyFormat::Markdown => Some(crate::clipboard_html::render_markdown(text)),
CopyFormat::MarkdownSelection(source) => {
Some(crate::clipboard_html::render_markdown(&source))
}
let native_copy = || {
let html = match format {
CopyFormat::PlainText => None,
CopyFormat::Markdown => Some(crate::clipboard_html::render_markdown(text)),
CopyFormat::MarkdownSelection(source) => {
Some(crate::clipboard_html::render_markdown(&source))
}
};
arboard_copy_fn(text, html.as_deref()).map_err(|error| format!("native clipboard: {error}"))
};
let native_result = arboard_copy_fn(text, html.as_deref()).or_else(|native_error| {
if environment.wsl_session {
wsl_copy_fn(text).map(|()| None).map_err(|wsl_error| {
format!("native clipboard: {native_error}; WSL fallback: {wsl_error}")
let native_result = if environment.wsl_session {
wsl_copy_fn(text).map(|()| None).or_else(|windows_error| {
native_copy().map_err(|native_error| {
format!("Windows clipboard: {windows_error}; {native_error}")
})
} else {
Err(format!("native clipboard: {native_error}"))
}
});
})
} else {
native_copy()
};
// 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 multiplexer sessions may gain remote clients after Codex starts, so still
@@ -255,11 +268,10 @@ pub(crate) fn is_wsl_session() -> bool {
/// Write to the native clipboard. The TUI owns any process-wide stderr redirection.
#[cfg(not(target_os = "android"))]
fn arboard_copy(
clipboard: Result<arboard::Clipboard, arboard::Error>,
mut clipboard: arboard::Clipboard,
text: &str,
html: Option<&str>,
) -> Result<Option<ClipboardLease>, String> {
let mut clipboard = clipboard.map_err(|e| format!("clipboard unavailable: {e}"))?;
match html {
Some(html) => clipboard
.set_html(html, Some(text))
@@ -278,62 +290,6 @@ fn arboard_copy(
}
}
/// Copy text into the Windows clipboard from a WSL process.
#[cfg(target_os = "linux")]
fn wsl_clipboard_copy(text: &str) -> Result<(), String> {
let executable = codex_utils_path::system_executable("powershell.exe")
.ok_or_else(|| "PowerShell is unavailable in the system PATH".to_string())?;
let path = codex_utils_path::system_path()
.map_err(|error| format!("failed to resolve system PATH: {error}"))?;
let mut child = std::process::Command::new(executable)
.env("PATH", path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.args([
"-NoProfile",
"-Command",
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; $ErrorActionPreference = 'Stop'; $text = [Console]::In.ReadToEnd(); Set-Clipboard -Value $text",
])
.spawn()
.map_err(|e| format!("failed to spawn powershell.exe: {e}"))?;
let Some(mut stdin) = child.stdin.take() else {
let _ = child.kill();
let _ = child.wait();
return Err("failed to open powershell.exe stdin".to_string());
};
if let Err(err) = stdin.write_all(text.as_bytes()) {
let _ = child.kill();
let _ = child.wait();
return Err(format!("failed to write to powershell.exe: {err}"));
}
drop(stdin);
let output = child
.wait_with_output()
.map_err(|e| format!("failed to wait for powershell.exe: {e}"))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if stderr.is_empty() {
let status = output.status;
Err(format!("powershell.exe exited with status {status}"))
} else {
Err(format!("powershell.exe failed: {stderr}"))
}
}
}
#[cfg(not(target_os = "linux"))]
fn wsl_clipboard_copy(_text: &str) -> Result<(), String> {
Err("WSL clipboard fallback unavailable on this platform".to_string())
}
/// Write text to the clipboard via the OSC 52 terminal escape sequence.
fn osc52_copy(text: &str) -> Result<(), String> {
let sequence = osc52_sequence(text, std::env::var_os("TMUX").is_some())?;
@@ -468,7 +424,7 @@ mod tests {
}
#[test]
fn ssh_attempts_both_native_and_osc52() {
fn ssh_on_wsl_attempts_windows_and_osc52() {
let tmux_calls = Cell::new(/*value*/ 0_u8);
let osc_calls = Cell::new(/*value*/ 0_u8);
let native_calls = Cell::new(/*value*/ 0_u8);
@@ -499,8 +455,8 @@ mod tests {
assert!(matches!(result, Ok(CopyOutcome::Copied(None))));
assert_eq!(tmux_calls.get(), 0);
assert_eq!(osc_calls.get(), 1);
assert_eq!(native_calls.get(), 1);
assert_eq!(wsl_calls.get(), 0);
assert_eq!(native_calls.get(), 0);
assert_eq!(wsl_calls.get(), 1);
}
#[test]
@@ -536,7 +492,7 @@ mod tests {
};
assert_eq!(
error,
"native clipboard: native unavailable; WSL fallback: powershell unavailable; terminal clipboard: blocked"
"Windows clipboard: powershell unavailable; native clipboard: native unavailable; terminal clipboard: blocked"
);
assert_eq!(tmux_calls.get(), 0);
assert_eq!(osc_calls.get(), 1);
@@ -545,7 +501,7 @@ mod tests {
}
#[test]
fn ssh_inside_tmux_attempts_both_native_and_tmux() {
fn ssh_on_wsl_inside_tmux_attempts_windows_and_tmux() {
let tmux_calls = Cell::new(/*value*/ 0_u8);
let osc_calls = Cell::new(/*value*/ 0_u8);
let native_calls = Cell::new(/*value*/ 0_u8);
@@ -575,8 +531,8 @@ mod tests {
assert!(matches!(result, Ok(CopyOutcome::Copied(None))));
assert_eq!(tmux_calls.get(), 1);
assert_eq!(osc_calls.get(), 0);
assert_eq!(native_calls.get(), 1);
assert_eq!(wsl_calls.get(), 0);
assert_eq!(native_calls.get(), 0);
assert_eq!(wsl_calls.get(), 1);
}
#[test]
@@ -610,8 +566,8 @@ mod tests {
assert!(matches!(result, Ok(CopyOutcome::Copied(None))));
assert_eq!(tmux_calls.get(), 1);
assert_eq!(osc_calls.get(), 1);
assert_eq!(native_calls.get(), 1);
assert_eq!(wsl_calls.get(), 0);
assert_eq!(native_calls.get(), 0);
assert_eq!(wsl_calls.get(), 1);
}
#[test]
@@ -629,10 +585,6 @@ mod tests {
let Err(error) = result else {
panic!("expected tmux and OSC 52 errors");
};
assert_eq!(
error,
"native clipboard: native unavailable; WSL fallback: powershell unavailable; terminal clipboard: tmux clipboard: tmux unavailable; OSC 52 fallback: osc blocked"
);
insta::assert_snapshot!("all_copy_backends_fail", error);
}
@@ -644,7 +596,7 @@ mod tests {
let result = copy_to_clipboard_with(
"hello",
CopyFormat::PlainText,
local_wsl_environment(),
local_environment(),
|_| Ok(()),
|_| {
osc_calls.set(osc_calls.get() + 1);
@@ -681,19 +633,21 @@ mod tests {
Some("<p><strong>hello</strong> /repo/my_notes!.txt</p>\n"),
),
] {
let result = copy_to_clipboard_with(
text,
format,
local_environment(),
|_| panic!("native copy should succeed"),
|_| panic!("native copy should succeed"),
|actual_text, actual_html| {
assert_eq!((actual_text, actual_html), (text, html));
Ok(None)
},
|_| panic!("native copy should succeed"),
);
assert!(matches!(result, Ok(CopyOutcome::Copied(None))));
for environment in [local_environment(), local_wsl_environment()] {
let result = copy_to_clipboard_with(
text,
format.clone(),
environment,
|_| panic!("native copy should succeed"),
|_| panic!("native copy should succeed"),
|actual_text, actual_html| {
assert_eq!((actual_text, actual_html), (text, html));
Ok(Some(super::ClipboardLease::test()))
},
|_| Err("Windows clipboard unavailable".into()),
);
assert!(matches!(result, Ok(CopyOutcome::Copied(Some(_)))));
}
}
}
@@ -767,7 +721,7 @@ mod tests {
}
#[test]
fn local_wsl_native_failure_uses_powershell_and_skips_osc52_on_success() {
fn local_wsl_uses_windows_and_skips_osc52_on_success() {
let osc_calls = Cell::new(/*value*/ 0_u8);
let native_calls = Cell::new(/*value*/ 0_u8);
let wsl_calls = Cell::new(/*value*/ 0_u8);
@@ -792,12 +746,12 @@ mod tests {
assert!(matches!(result, Ok(CopyOutcome::Copied(None))));
assert_eq!(osc_calls.get(), 0);
assert_eq!(native_calls.get(), 1);
assert_eq!(native_calls.get(), 0);
assert_eq!(wsl_calls.get(), 1);
}
#[test]
fn local_wsl_falls_back_to_osc52_when_native_and_powershell_fail() {
fn local_wsl_falls_back_to_osc52_when_powershell_fails() {
let osc_calls = Cell::new(/*value*/ 0_u8);
let native_calls = Cell::new(/*value*/ 0_u8);
let wsl_calls = Cell::new(/*value*/ 0_u8);
@@ -863,7 +817,7 @@ mod tests {
}
#[test]
fn local_wsl_reports_native_powershell_and_osc52_errors_when_all_fail() {
fn local_wsl_reports_windows_and_osc52_errors_when_all_fail() {
let osc_calls = Cell::new(/*value*/ 0_u8);
let native_calls = Cell::new(/*value*/ 0_u8);
let wsl_calls = Cell::new(/*value*/ 0_u8);
@@ -887,11 +841,11 @@ mod tests {
);
let Err(error) = result else {
panic!("expected native, WSL, and OSC 52 errors");
panic!("expected Windows and OSC 52 errors");
};
assert_eq!(
error,
"native clipboard: native unavailable; WSL fallback: powershell unavailable; terminal clipboard: osc blocked"
"Windows clipboard: powershell unavailable; native clipboard: native unavailable; terminal clipboard: osc blocked"
);
assert_eq!(osc_calls.get(), 1);
assert_eq!(native_calls.get(), 1);
+177
View File
@@ -0,0 +1,177 @@
//! WSL text copies with Windows interop use the Windows clipboard without contacting the display.
//! Bound discovery, input, and exit; give fallback native setup its own short deadline.
//! Never release the worker while a writer can still run.
#[cfg(target_os = "linux")]
use std::ffi::OsString;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use std::sync::Mutex;
#[cfg(target_os = "linux")]
use std::sync::mpsc;
#[cfg(any(target_os = "linux", all(test, unix)))]
use std::time::Instant;
#[cfg(target_os = "linux")]
type Pending<T> = Mutex<Option<mpsc::Receiver<Result<T, String>>>>;
#[cfg(target_os = "linux")]
pub(super) fn copy(text: &str, deadline: Instant) -> Result<(), String> {
static DISCOVERY: Pending<(PathBuf, OsString)> = Mutex::new(None);
let (executable, path) = read_only_setup(&DISCOVERY, deadline, || {
let executable = codex_utils_path::system_executable("powershell.exe")
.ok_or("PowerShell is unavailable in the system PATH")?;
let path = codex_utils_path::system_path()
.map_err(|error| format!("failed to resolve system PATH: {error}"))?;
Ok((executable, path))
})?;
let status = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| format!("could not start Windows clipboard writer: {error}"))?
.block_on(write_command(command(&executable, &path), text, deadline))?;
if status.success() {
Ok(())
} else {
Err(format!("powershell.exe exited with status {status}"))
}
}
#[cfg(any(target_os = "linux", all(test, unix)))]
fn command(executable: &std::path::Path, path: &std::ffi::OsStr) -> tokio::process::Command {
// Spawn only the native shell here: directly spawning a Windows exe can block on WSL.
let mut command = tokio::process::Command::new("/bin/sh");
command
.env("PATH", path)
.args(["-c", r#"exec "$@""#, "codex-wsl-copy"])
.arg(executable)
// Preserve the released command signature: trusted adapters may match it exactly.
.args([
"-NoProfile",
"-Command",
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; $ErrorActionPreference = 'Stop'; $text = [Console]::In.ReadToEnd(); Set-Clipboard -Value $text",
]);
command
}
#[cfg(not(target_os = "linux"))]
pub(super) fn copy(_text: &str, _deadline: std::time::Instant) -> Result<(), String> {
Err("Windows clipboard unavailable on this platform".into())
}
#[cfg(target_os = "linux")]
pub(super) fn native_clipboard() -> Result<arboard::Clipboard, String> {
static NATIVE: Pending<arboard::Clipboard> = Mutex::new(None);
// Windows discovery may have exhausted its budget. Do not starve a healthy WSLg.
let deadline = Instant::now() + std::time::Duration::from_secs(/*secs*/ 1);
read_only_setup(&NATIVE, deadline, || {
arboard::Clipboard::new().map_err(|error| format!("clipboard unavailable: {error}"))
})
}
// Discovery and WSLg setup may block forever. Keep at most one outstanding task of each
// kind and reuse its result after a timeout. Neither task may capture text or write a clipboard.
#[cfg(target_os = "linux")]
fn read_only_setup<T: Send + 'static>(
pending: &Pending<T>,
deadline: Instant,
setup: impl FnOnce() -> Result<T, String> + Send + 'static,
) -> Result<T, String> {
let remaining = deadline
.checked_duration_since(Instant::now())
.ok_or("clipboard setup timed out")?;
let mut pending = pending.lock().map_err(|error| error.to_string())?;
if pending.is_none() {
let (tx, rx) = mpsc::channel();
std::thread::Builder::new()
.name("wsl-clipboard-setup".into())
.spawn(move || {
let _ = tx.send(setup());
})
.map_err(|error| format!("could not start clipboard setup: {error}"))?;
*pending = Some(rx);
}
let result = pending
.as_ref()
.ok_or("clipboard setup unavailable")?
.recv_timeout(remaining);
if !matches!(result, Err(mpsc::RecvTimeoutError::Timeout)) {
*pending = None;
}
result.map_err(|error| format!("clipboard setup: {error}"))?
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
pub(super) fn native_clipboard() -> Result<arboard::Clipboard, String> {
Err("WSL native clipboard unavailable on this platform".into())
}
#[cfg(any(target_os = "linux", all(test, unix)))]
async fn write_command(
mut command: tokio::process::Command,
text: &str,
deadline: Instant,
) -> Result<std::process::ExitStatus, String> {
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
if Instant::now() >= deadline {
return Err("Windows clipboard writer timed out".into());
}
let mut child = command
.process_group(/*pgroup*/ 0)
.stdin(Stdio::piped())
.stdout(Stdio::null())
// Do not expose clipboard contents from PowerShell errors or let its stderr block input.
.stderr(Stdio::null())
.kill_on_drop(/*kill_on_drop*/ true)
.spawn()
.map_err(|error| format!("failed to spawn powershell.exe: {error}"))?;
// WSL process spawn itself may stall. Do not write after a late spawn.
let result = tokio::time::timeout_at(deadline.into(), async {
if Instant::now() >= deadline {
return Err("Windows clipboard writer timed out".into());
}
child
.stdin
.as_mut()
.ok_or("failed to open powershell.exe stdin")?
.write_all(text.as_bytes())
.await
.map_err(|error| format!("failed to write to powershell.exe: {error}"))?;
drop(child.stdin.take());
child
.wait()
.await
.map_err(|error| format!("failed to wait for powershell.exe: {error}"))
})
.await
.unwrap_or_else(|_| Err("Windows clipboard writer timed out".into()));
match result {
Ok(status) => Ok(status),
Err(error) => {
// Keep partially written stdin open until kill: PowerShell copies on EOF.
// If kill fails, still wait. Releasing the worker earlier could allow an old copy
// to overwrite a newer one. kill_on_drop alone does not guarantee reaping.
if let Some(pid) = child.id() {
// SAFETY: process_group(0) assigned the owned, unreaped child its own group.
// An installed adapter may have spawned its own writer. Stop the whole group.
unsafe { libc::killpg(pid as libc::pid_t, libc::SIGKILL) };
}
let _ = child.start_kill();
// wait() closes child.stdin itself, even if kill failed. Never give the
// writer EOF with partial input while waiting for it to stop.
let _stdin = child.stdin.take();
child
.wait()
.await
.map_err(|error| format!("failed to reap powershell.exe: {error}"))?;
Err(error)
}
}
}
#[cfg(all(test, unix))]
#[path = "wsl_tests.rs"]
mod tests;
@@ -0,0 +1,99 @@
use super::write_command;
use pretty_assertions::assert_eq;
use std::time::Duration;
use std::time::Instant;
#[tokio::test]
async fn installed_adapter_receives_compatible_command_and_never_copies_expired_work() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let root = dir.path().canonicalize().unwrap();
let executable = root.join("powershell.exe");
std::fs::write(
&executable,
r#"#!/bin/sh
[ "$#" -eq 3 ] && [ "$1" = '-NoProfile' ] && [ "$2" = '-Command' ] || exit 46
[ "$3" = "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; \$ErrorActionPreference = 'Stop'; \$text = [Console]::In.ReadToEnd(); Set-Clipboard -Value \$text" ] || exit 47
/bin/cat > "$COPY_OUTPUT"
"#,
).unwrap();
std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o755)).unwrap();
let output = root.join("clipboard");
let mut command = super::command(&executable, root.as_os_str());
command.env("COPY_OUTPUT", &output);
let text = " café 🦀\r\n\t".repeat(/*n*/ 32_768);
let status = write_command(
command,
&text,
Instant::now() + Duration::from_secs(/*secs*/ 5),
)
.await
.unwrap();
assert!(status.success(), "{status}");
assert_eq!(std::fs::read_to_string(&output).unwrap(), text);
let mut command = super::command(&executable, root.as_os_str());
command.env("COPY_OUTPUT", &output);
assert_eq!(
write_command(command, "late", Instant::now()).await,
Err("Windows clipboard writer timed out".into())
);
assert_eq!(std::fs::read_to_string(output).unwrap(), text);
}
#[tokio::test]
async fn stalled_input_and_stalled_exit_are_reaped_before_returning() {
let dir = tempfile::tempdir().unwrap();
// Large input blocks on the pipe; small input fits, so the worker reaches the exit wait.
for (stage, text, script) in [
(
"input",
"x".repeat(/*n*/ 1_048_576),
"echo $$ > \"$1\"; exec sleep 30",
),
("exit", "small".into(), "echo $$ > \"$1\"; exec sleep 30"),
// An adapter waiting on a subprocess must stop that subprocess too.
(
"subprocess",
String::new(),
"sh -c 'echo $$ > \"$1\"; exec sleep 30' sh \"$1\"; wait",
),
] {
let pid_path = dir.path().join(format!("{stage}.pid"));
let mut command = tokio::process::Command::new("sh");
command.args(["-c", script, "sh"]).arg(&pid_path);
let started = Instant::now();
assert_eq!(
write_command(command, &text, started + Duration::from_secs(/*secs*/ 1)).await,
Err("Windows clipboard writer timed out".into())
);
assert!(
started.elapsed() < Duration::from_secs(/*secs*/ 10),
"{stage}: cleanup exceeded its budget: {:?}",
started.elapsed()
);
let pid: libc::pid_t = std::fs::read_to_string(&pid_path)
.unwrap()
.trim()
.parse()
.unwrap();
if stage == "subprocess" {
// The grandchild must be stopped; only init can reap it. A zombie is
// acceptable here and still responds to kill(pid, 0) until reaped.
let output = std::process::Command::new("ps")
.args(["-o", "stat=", "-p", &pid.to_string()])
.output()
.unwrap();
let state = String::from_utf8(output.stdout).unwrap();
let state = state.trim();
assert!(state.is_empty() || state.starts_with('Z'), "state: {state}");
} else {
// SAFETY: Signal 0 observes this disposable child without modifying it.
assert_eq!(unsafe { libc::kill(pid, 0) }, -1);
assert_eq!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::ESRCH)
);
}
}
}
@@ -1,5 +1,5 @@
---
source: codex-rs/tui/src/clipboard_copy.rs
source: tui/src/clipboard_copy.rs
expression: error
---
native clipboard: native unavailable; WSL fallback: powershell unavailable; terminal clipboard: tmux clipboard: tmux unavailable; OSC 52 fallback: osc blocked
Windows clipboard: powershell unavailable; native clipboard: native unavailable; terminal clipboard: tmux clipboard: tmux unavailable; OSC 52 fallback: osc blocked