Default to copying transcript selections in more terminals (#48469)

## What changed

Make `tui.copy_on_select = "auto"` copy on mouse release unless a direct terminal is known to forward its native copy shortcut: Ghostty with a parsed version at least `1.2.0`, Kitty on macOS, Windows Terminal, or VS Code on Windows.

Unknown terminals and Ghostty with older, missing, or unrecognized versions now default to copying. Keep copying enabled under tmux/Zellij and preserve explicit `always` and `never` overrides. Update the configuration documentation and schema to describe these defaults.

## Testing

Expand the existing configuration test matrix to cover more terminals, Ghostty versions (including `1.2.0-dev`), platform-specific defaults, and multiplexer behavior alongside configuration and launch overrides.

GitOrigin-RevId: 410abfa580156454b0f88e0bf4ed977fb8c81b1e
(cherry picked from commit b334d5b3f2)
This commit is contained in:
Felipe Coury
2026-09-26 15:49:13 -07:00
committed by Ian MacLeod
parent 3e1ab64168
commit 5a7bb4dac9
4 changed files with 75 additions and 23 deletions
+3 -2
View File
@@ -865,8 +865,9 @@ pub struct Tui {
pub fullscreen_transcript: bool,
/// Copy selected transcript text when the mouse button is released.
/// Defaults to `auto`: enabled in tmux/Zellij and in direct macOS terminals except Ghostty/Kitty.
/// On other platforms, direct terminals default off except iTerm2/Terminal.app.
/// Defaults to `auto`: enabled except in direct terminals known to forward their native
/// copy shortcut (Ghostty 1.2+, Kitty on macOS, Windows Terminal, and VS Code on Windows).
/// Unknown terminals, Ghostty without a recognized version, and tmux/Zellij default to copying.
#[serde(default)]
pub copy_on_select: CopyOnSelect,
+1 -1
View File
@@ -4359,7 +4359,7 @@
}
],
"default": "auto",
"description": "Copy selected transcript text when the mouse button is released. Defaults to `auto`: enabled in tmux/Zellij and in direct macOS terminals except Ghostty/Kitty. On other platforms, direct terminals default off except iTerm2/Terminal.app."
"description": "Copy selected transcript text when the mouse button is released. Defaults to `auto`: enabled except in direct terminals known to forward their native copy shortcut (Ghostty 1.2+, Kitty on macOS, Windows Terminal, and VS Code on Windows). Unknown terminals, Ghostty without a recognized version, and tmux/Zellij default to copying."
},
"disable_paste_burst": {
"description": "When true, disables burst-paste detection for typed input entirely. All characters are inserted as they are received, and no buffering or placeholder replacement will occur for fast keypress bursts. Overrides the legacy top-level `disable_paste_burst` setting. Defaults to `false`.",
+37 -13
View File
@@ -14,7 +14,6 @@ use codex_config::types::CopyOnSelect;
use codex_config::types::History;
use codex_config::types::Notice;
use codex_config::types::Tui;
use codex_terminal_detection::Multiplexer;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -129,22 +128,47 @@ impl LocalSettings {
settings
}
/// Prefer explicit overrides; tmux/Zellij always default to copying on release.
/// Direct macOS terminals default on except Ghostty/Kitty, which forward Cmd-C.
/// Copy on release unless a direct terminal is known to forward its default copy shortcut.
/// Multiplexers, unknown terminals and Ghostty without a recognized version default to copying.
pub(crate) fn copy_on_select(&self, terminal: &TerminalInfo) -> bool {
match self.tui.copy_on_select {
CopyOnSelect::Always => true,
CopyOnSelect::Never => false,
CopyOnSelect::Auto => match terminal.multiplexer {
Some(Multiplexer::Tmux { .. } | Multiplexer::Zellij { .. }) => true,
None if cfg!(target_os = "macos") => {
!matches!(terminal.name, TerminalName::Ghostty | TerminalName::Kitty)
}
None => matches!(
terminal.name,
TerminalName::Iterm2 | TerminalName::AppleTerminal
),
},
CopyOnSelect::Auto => {
terminal.multiplexer.is_some()
|| match terminal.name {
// Since 1.2, both Ghostty's Cmd-C and Ctrl-Shift-C bindings are
// "performable": they forward the key if Ghostty has no selection.
TerminalName::Ghostty => !terminal
.version
.as_deref()
.and_then(|version| semver::Version::parse(version).ok())
.is_some_and(|version| {
version
>= semver::Version::new(
/*major*/ 1, /*minor*/ 2, /*patch*/ 0,
)
}),
// Kitty's Cmd-C uses copy_or_noop. Its Ctrl-Shift-C consumes the key.
TerminalName::Kitty => !cfg!(target_os = "macos"),
// Windows Terminal's Copy action forwards its key when unselected,
// including when the CLI runs in WSL.
TerminalName::WindowsTerminal => false,
// VS Code gates Copy on native selection. Only Windows' default
// (plain Ctrl-C) is also forwarded by the legacy xterm.js encoder.
TerminalName::VsCode => !cfg!(target_os = "windows"),
TerminalName::AppleTerminal
| TerminalName::Iterm2
| TerminalName::WarpTerminal
| TerminalName::WezTerm
| TerminalName::Alacritty
| TerminalName::Konsole
| TerminalName::GnomeTerminal
| TerminalName::Vte
| TerminalName::Dumb
| TerminalName::Unknown => true,
}
}
}
}
+34 -7
View File
@@ -231,28 +231,55 @@ async fn copy_on_select_respects_terminal_defaults_and_config_overrides() -> any
(config.tui_copy_on_select, local.tui.copy_on_select),
(expected, expected),
);
for (name, multiplexer, default_enabled) in [
(TerminalName::Iterm2, None, true),
(TerminalName::AppleTerminal, None, true),
(TerminalName::Ghostty, None, false),
(TerminalName::Kitty, None, false),
(TerminalName::Unknown, None, cfg!(target_os = "macos")),
for (name, version, multiplexer, default_enabled) in [
(TerminalName::Iterm2, None, None, true),
(TerminalName::AppleTerminal, None, None, true),
(TerminalName::Ghostty, Some("1.2.0"), None, false),
(TerminalName::Ghostty, Some("1.3.0"), None, false),
(TerminalName::Ghostty, Some("1.1.3"), None, true),
(TerminalName::Ghostty, Some("1.2.0-dev"), None, true),
(TerminalName::Ghostty, Some("invalid"), None, true),
(TerminalName::Ghostty, None, None, true),
(TerminalName::Kitty, None, None, !cfg!(target_os = "macos")),
(TerminalName::WindowsTerminal, None, None, false),
(
TerminalName::VsCode,
None,
None,
!cfg!(target_os = "windows"),
),
(TerminalName::Alacritty, None, None, true),
(TerminalName::GnomeTerminal, None, None, true),
(TerminalName::Konsole, None, None, true),
(TerminalName::Vte, None, None, true),
(TerminalName::WarpTerminal, None, None, true),
(TerminalName::WezTerm, None, None, true),
(TerminalName::Dumb, None, None, true),
(TerminalName::Unknown, None, None, true),
(
TerminalName::Ghostty,
Some("1.3.0"),
Some(Multiplexer::Tmux { version: None }),
true,
),
(
TerminalName::Kitty,
None,
Some(Multiplexer::Zellij { version: None }),
true,
),
(
TerminalName::WindowsTerminal,
None,
Some(Multiplexer::Tmux { version: None }),
true,
),
] {
let terminal = TerminalInfo {
name,
multiplexer,
term_program: None,
version: None,
version: version.map(str::to_owned),
term: None,
};
assert_eq!(