mirror of
https://github.com/openai/codex.git
synced 2026-10-21 07:42:01 +00:00
Use native scrollback for Terminal.app over SSH in auto mode (#47417)
## What changed - Detect Terminal.app from primary and secondary device attributes during startup on Unix SSH sessions without a detected multiplexer. Ignore signatures inside bracketed paste and leave incomplete replies unknown. - Use inline mode when Terminal.app is detected and `tui.alternate_screen` is `auto`. Apply the decision before the first startup frame and retain it for later configuration loads. Preserve explicit `always`, `never`, and `--no-alt-screen` overrides. - Update the `crossterm` dependency revision and lockfiles. ## Testing Add coverage for terminal signatures, fragmented and pasted replies, probe completion and timeout behavior, screen-mode overrides, and an inline startup snapshot. GitOrigin-RevId: 9b937824b080f9bd6de29df1d9e84f42a698af46
This commit is contained in:
Generated
+1
-1
@@ -1085,7 +1085,7 @@
|
||||
"git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4_process_security_environment_spec": "{\"dependencies\":[{\"name\":\"flatbuffers\",\"req\":\"25\"}],\"features\":{},\"strip_prefix\":\"core/generated/process_security_environment_specification\"}",
|
||||
"git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4_sandbox_spec": "{\"dependencies\":[{\"name\":\"flatbuffers\",\"req\":\"25\"}],\"features\":{},\"strip_prefix\":\"core/generated/base_container_specification\"}",
|
||||
"git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4_wxc_common": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"0.22\"},{\"name\":\"cidr\",\"req\":\"0.3.2\"},{\"name\":\"getrandom\",\"req\":\"0.2\"},{\"default_features\":true,\"features\":[],\"name\":\"mxc_config_contract\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"mxc_schema_support\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"mxc_telemetry\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"nanvix_common\",\"optional\":true},{\"name\":\"schemars\",\"optional\":true,\"req\":\"0.8\"},{\"name\":\"semver\",\"req\":\"1\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":false,\"req\":\"1\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"req\":\"1\"},{\"name\":\"serde_path_to_error\",\"req\":\"0.1\"},{\"name\":\"thiserror\",\"req\":\"2\"},{\"name\":\"unicode-general-category\",\"req\":\"1\"},{\"name\":\"url\",\"req\":\"2\"},{\"default_features\":true,\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"1\"},{\"name\":\"widestring\",\"req\":\"1\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"default_features\":true,\"features\":[\"Win32_Foundation\",\"Win32_Globalization\",\"Win32_NetworkManagement_WindowsFirewall\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Security_Authorization\",\"Win32_Security_Credentials\",\"Win32_Security_Isolation\",\"Win32_Storage_FileSystem\",\"Win32_System_Com\",\"Win32_System_Console\",\"Win32_System_Diagnostics_Debug\",\"Win32_System_Diagnostics_Etw\",\"Win32_System_Diagnostics_ToolHelp\",\"Win32_System_Environment\",\"Win32_System_IO\",\"Win32_System_JobObjects\",\"Win32_System_LibraryLoader\",\"Win32_System_Memory\",\"Win32_System_Ole\",\"Win32_System_Pipes\",\"Win32_System_ProcessStatus\",\"Win32_System_Registry\",\"Win32_System_SystemInformation\",\"Win32_System_SystemServices\",\"Win32_System_Threading\",\"Win32_System_Time\",\"Win32_System_Variant\",\"Win32_System_WindowsProgramming\",\"Win32_UI_Shell\"],\"name\":\"windows\",\"optional\":false,\"req\":\"0.62\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"windows-core\",\"req\":\"0.62\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"winreg\",\"req\":\"0.55\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"libc\",\"req\":\"0.2\",\"target\":\"cfg(unix)\"}],\"features\":{\"microvm\":[\"dep:nanvix_common\",\"dep:uuid\"],\"schema-gen\":[\"dep:mxc_schema_support\",\"dep:schemars\"]},\"strip_prefix\":\"core/wxc_common\"}",
|
||||
"git+https://github.com/openai-oss-forks/crossterm?rev=45fecb9508105988f42fe6ff0441783ed3717f92#45fecb9508105988f42fe6ff0441783ed3717f92_crossterm": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"base64\",\"optional\":true,\"req\":\"0.22\"},{\"default_features\":true,\"features\":[],\"name\":\"bitflags\",\"optional\":false,\"req\":\"2.9\"},{\"default_features\":true,\"features\":[\"is_variant\"],\"name\":\"derive_more\",\"optional\":true,\"req\":\"2.0.0\"},{\"name\":\"document-features\",\"req\":\"0.2.11\"},{\"default_features\":false,\"features\":[],\"name\":\"futures-core\",\"optional\":true,\"req\":\"0.3\"},{\"name\":\"parking_lot\",\"req\":\"0.12\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"1.0\"},{\"default_features\":true,\"features\":[],\"name\":\"filedescriptor\",\"optional\":true,\"req\":\"0.8\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[],\"name\":\"libc\",\"optional\":true,\"req\":\"0.2\",\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"os-poll\"],\"name\":\"mio\",\"optional\":true,\"req\":\"1.0\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[\"std\",\"stdio\",\"termios\"],\"name\":\"rustix\",\"optional\":false,\"req\":\"1\",\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"signal-hook\",\"optional\":true,\"req\":\"0.3.17\",\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"support-v1_0\"],\"name\":\"signal-hook-mio\",\"optional\":true,\"req\":\"0.2.4\",\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"crossterm_winapi\",\"optional\":true,\"req\":\"0.9.1\",\"target\":\"cfg(windows)\"},{\"default_features\":true,\"features\":[\"winuser\",\"winerror\"],\"name\":\"winapi\",\"optional\":true,\"req\":\"0.3.9\",\"target\":\"cfg(windows)\"}],\"features\":{\"bracketed-paste\":[],\"default\":[\"bracketed-paste\",\"events\",\"windows\",\"derive-more\"],\"derive-more\":[\"dep:derive_more\"],\"event-stream\":[\"dep:futures-core\",\"events\"],\"events\":[\"dep:mio\",\"dep:signal-hook\",\"dep:signal-hook-mio\"],\"osc52\":[\"dep:base64\"],\"serde\":[\"dep:serde\",\"bitflags/serde\"],\"use-dev-tty\":[\"filedescriptor\",\"rustix/process\"],\"windows\":[\"dep:winapi\",\"dep:crossterm_winapi\"]},\"strip_prefix\":\"\"}",
|
||||
"git+https://github.com/openai-oss-forks/crossterm?rev=efa177859fd9623d57b9fe7ae9bf491ae1ac6ec4#efa177859fd9623d57b9fe7ae9bf491ae1ac6ec4_crossterm": "{\"dependencies\":[{\"default_features\":true,\"features\":[],\"name\":\"base64\",\"optional\":true,\"req\":\"0.22\"},{\"default_features\":true,\"features\":[],\"name\":\"bitflags\",\"optional\":false,\"req\":\"2.9\"},{\"default_features\":true,\"features\":[\"is_variant\"],\"name\":\"derive_more\",\"optional\":true,\"req\":\"2.0.0\"},{\"name\":\"document-features\",\"req\":\"0.2.11\"},{\"default_features\":false,\"features\":[],\"name\":\"futures-core\",\"optional\":true,\"req\":\"0.3\"},{\"name\":\"parking_lot\",\"req\":\"0.12\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"1.0\"},{\"default_features\":true,\"features\":[],\"name\":\"filedescriptor\",\"optional\":true,\"req\":\"0.8\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[],\"name\":\"libc\",\"optional\":true,\"req\":\"0.2\",\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"os-poll\"],\"name\":\"mio\",\"optional\":true,\"req\":\"1.0\",\"target\":\"cfg(unix)\"},{\"default_features\":false,\"features\":[\"std\",\"stdio\",\"termios\"],\"name\":\"rustix\",\"optional\":false,\"req\":\"1\",\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"signal-hook\",\"optional\":true,\"req\":\"0.3.17\",\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[\"support-v1_0\"],\"name\":\"signal-hook-mio\",\"optional\":true,\"req\":\"0.2.4\",\"target\":\"cfg(unix)\"},{\"default_features\":true,\"features\":[],\"name\":\"crossterm_winapi\",\"optional\":true,\"req\":\"0.9.1\",\"target\":\"cfg(windows)\"},{\"default_features\":true,\"features\":[\"winuser\",\"winerror\"],\"name\":\"winapi\",\"optional\":true,\"req\":\"0.3.9\",\"target\":\"cfg(windows)\"}],\"features\":{\"bracketed-paste\":[],\"default\":[\"bracketed-paste\",\"events\",\"windows\",\"derive-more\"],\"derive-more\":[\"dep:derive_more\"],\"event-stream\":[\"dep:futures-core\",\"events\"],\"events\":[\"dep:mio\",\"dep:signal-hook\",\"dep:signal-hook-mio\"],\"osc52\":[\"dep:base64\"],\"serde\":[\"dep:serde\",\"bitflags/serde\"],\"use-dev-tty\":[\"filedescriptor\",\"rustix/process\"],\"windows\":[\"dep:winapi\",\"dep:crossterm_winapi\"]},\"strip_prefix\":\"\"}",
|
||||
"git+https://github.com/openai-oss-forks/tokio-tungstenite?rev=0e5b2d73aa18dd9f0a50ee9ff199d5aef7594186#0e5b2d73aa18dd9f0a50ee9ff199d5aef7594186_tokio-tungstenite": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"sink\",\"std\"],\"name\":\"futures-util\",\"optional\":false},{\"name\":\"log\"},{\"default_features\":true,\"features\":[],\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\"},{\"default_features\":false,\"features\":[],\"name\":\"rustls\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-native-certs\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-pki-types\",\"optional\":true},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"tokio-native-tls\",\"optional\":true},{\"default_features\":false,\"features\":[],\"name\":\"tokio-rustls\",\"optional\":true},{\"default_features\":false,\"features\":[],\"name\":\"tungstenite\",\"optional\":false},{\"default_features\":true,\"features\":[],\"name\":\"webpki-roots\",\"optional\":true}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\",\"tokio-rustls\",\"stream\",\"tungstenite/__rustls-tls\",\"handshake\"],\"connect\":[\"stream\",\"tokio/net\",\"tokio/time\",\"handshake\"],\"default\":[\"connect\",\"handshake\"],\"handshake\":[\"tungstenite/handshake\"],\"native-tls\":[\"native-tls-crate\",\"tokio-native-tls\",\"stream\",\"tungstenite/native-tls\",\"handshake\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\",\"tungstenite/native-tls-vendored\"],\"proxy\":[\"tungstenite/proxy\",\"tokio/net\",\"handshake\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"stream\":[],\"url\":[\"tungstenite/url\"]},\"strip_prefix\":\"\"}",
|
||||
"git+https://github.com/openai-oss-forks/tungstenite-rs?rev=4fffad30fe373adbdcffab9545e9e9bf4f2fc19f#4fffad30fe373adbdcffab9545e9e9bf4f2fc19f_tungstenite": "{\"dependencies\":[{\"name\":\"bytes\"},{\"default_features\":true,\"features\":[],\"name\":\"data-encoding\",\"optional\":true},{\"default_features\":false,\"features\":[\"zlib-rs\"],\"name\":\"flate2\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"headers\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"http\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"httparse\",\"optional\":true},{\"name\":\"log\"},{\"default_features\":true,\"features\":[],\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\"},{\"name\":\"rand\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-native-certs\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"rustls-pki-types\",\"optional\":true},{\"default_features\":true,\"features\":[],\"name\":\"sha1\",\"optional\":true},{\"name\":\"thiserror\"},{\"default_features\":true,\"features\":[],\"name\":\"url\",\"optional\":true},{\"name\":\"utf-8\"},{\"default_features\":true,\"features\":[],\"name\":\"webpki-roots\",\"optional\":true}],\"features\":{\"__rustls-tls\":[\"rustls\",\"rustls-pki-types\"],\"default\":[\"handshake\"],\"deflate\":[\"headers\",\"flate2\"],\"handshake\":[\"data-encoding\",\"headers\",\"httparse\",\"sha1\"],\"headers\":[\"http\",\"dep:headers\"],\"native-tls\":[\"native-tls-crate\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate/vendored\"],\"proxy\":[\"handshake\"],\"rustls-tls-native-roots\":[\"__rustls-tls\",\"rustls-native-certs\"],\"rustls-tls-webpki-roots\":[\"__rustls-tls\",\"webpki-roots\"],\"url\":[\"dep:url\"]},\"strip_prefix\":\"\"}",
|
||||
"git+https://github.com/rust-lang/rust-clippy?rev=20ce69b9a63bcd2756cd906fe0964d1e901e042a#20ce69b9a63bcd2756cd906fe0964d1e901e042a_clippy_utils": "{\"dependencies\":[{\"default_features\":false,\"features\":[],\"name\":\"arrayvec\",\"optional\":false},{\"name\":\"itertools\"},{\"name\":\"rustc_apfloat\"},{\"default_features\":true,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":false}],\"features\":{},\"strip_prefix\":\"clippy_utils\"}",
|
||||
|
||||
Generated
+1
-1
@@ -5658,7 +5658,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
source = "git+https://github.com/openai-oss-forks/crossterm?rev=45fecb9508105988f42fe6ff0441783ed3717f92#45fecb9508105988f42fe6ff0441783ed3717f92"
|
||||
source = "git+https://github.com/openai-oss-forks/crossterm?rev=efa177859fd9623d57b9fe7ae9bf491ae1ac6ec4#efa177859fd9623d57b9fe7ae9bf491ae1ac6ec4"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"crossterm_winapi",
|
||||
|
||||
+1
-1
@@ -630,7 +630,7 @@ opt-level = 0
|
||||
[patch.crates-io]
|
||||
# Uncomment to debug local changes.
|
||||
# ratatui = { path = "../../ratatui" }
|
||||
crossterm = { git = "https://github.com/openai-oss-forks/crossterm", rev = "45fecb9508105988f42fe6ff0441783ed3717f92" }
|
||||
crossterm = { git = "https://github.com/openai-oss-forks/crossterm", rev = "efa177859fd9623d57b9fe7ae9bf491ae1ac6ec4" }
|
||||
tokio-tungstenite = { git = "https://github.com/openai-oss-forks/tokio-tungstenite", rev = "0e5b2d73aa18dd9f0a50ee9ff199d5aef7594186" }
|
||||
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "4fffad30fe373adbdcffab9545e9e9bf4f2fc19f" }
|
||||
|
||||
|
||||
+38
-21
@@ -1918,7 +1918,11 @@ async fn run_ratatui_app(
|
||||
}
|
||||
|
||||
// Cloud configuration and session selection can change screen policy after first paint.
|
||||
let use_alt_screen = determine_alt_screen_mode(cli.no_alt_screen, config.tui_alternate_screen);
|
||||
let use_alt_screen = determine_alt_screen_mode(
|
||||
cli.no_alt_screen,
|
||||
config.tui_alternate_screen,
|
||||
tui.terminal_app_over_ssh,
|
||||
);
|
||||
let mode = crate::transcript_mode::TranscriptMode::resolve(
|
||||
config.tui_fullscreen_transcript,
|
||||
use_alt_screen,
|
||||
@@ -2105,13 +2109,21 @@ impl Drop for TerminalRestoreGuard {
|
||||
/// - Otherwise, respect the `tui.alternate_screen` config setting:
|
||||
/// - `always`: Use alternate screen
|
||||
/// - `never`: Inline mode only, preserves scrollback
|
||||
/// - `auto` (default): Use alternate screen
|
||||
fn determine_alt_screen_mode(no_alt_screen: bool, tui_alternate_screen: AltScreenMode) -> bool {
|
||||
/// - `auto` (default): Use native scrollback for Terminal.app over SSH, otherwise alternate screen
|
||||
fn determine_alt_screen_mode(
|
||||
no_alt_screen: bool,
|
||||
tui_alternate_screen: AltScreenMode,
|
||||
terminal_app_over_ssh: bool,
|
||||
) -> bool {
|
||||
if no_alt_screen {
|
||||
return false;
|
||||
}
|
||||
|
||||
tui_alternate_screen != AltScreenMode::Never
|
||||
match tui_alternate_screen {
|
||||
AltScreenMode::Always => true,
|
||||
AltScreenMode::Never => false,
|
||||
AltScreenMode::Auto => !terminal_app_over_ssh,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -2887,23 +2899,28 @@ requires_openai_auth = {requires_openai_auth}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternate_screen_auto_uses_alt_screen() {
|
||||
assert!(determine_alt_screen_mode(
|
||||
/*no_alt_screen*/ false,
|
||||
AltScreenMode::Auto,
|
||||
));
|
||||
assert!(determine_alt_screen_mode(
|
||||
/*no_alt_screen*/ false,
|
||||
AltScreenMode::Always,
|
||||
));
|
||||
assert!(!determine_alt_screen_mode(
|
||||
/*no_alt_screen*/ false,
|
||||
AltScreenMode::Never,
|
||||
));
|
||||
assert!(!determine_alt_screen_mode(
|
||||
/*no_alt_screen*/ true,
|
||||
AltScreenMode::Auto,
|
||||
));
|
||||
fn alternate_screen_respects_terminal_compatibility_and_overrides() {
|
||||
for terminal_app_over_ssh in [false, true] {
|
||||
for (mode, expected) in [
|
||||
(AltScreenMode::Auto, !terminal_app_over_ssh),
|
||||
(AltScreenMode::Always, true),
|
||||
(AltScreenMode::Never, false),
|
||||
] {
|
||||
assert_eq!(
|
||||
determine_alt_screen_mode(
|
||||
/*no_alt_screen*/ false,
|
||||
mode,
|
||||
terminal_app_over_ssh
|
||||
),
|
||||
expected,
|
||||
);
|
||||
assert!(!determine_alt_screen_mode(
|
||||
/*no_alt_screen*/ true,
|
||||
mode,
|
||||
terminal_app_over_ssh
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: tui/src/startup_draft_tests.rs
|
||||
assertion_line: 162
|
||||
expression: "frames.join(\"\\n---\\n\").replace(crate::version::CODEX_CLI_VERSION, \"<VERSION>\")"
|
||||
---
|
||||
terminal_app_over_ssh=false, owned=true
|
||||
╭───────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v<VERSION>) │
|
||||
│ │
|
||||
│ model: loading /model to change │
|
||||
│ directory: loading │
|
||||
╰───────────────────────────────────────╯
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
? for shortcuts
|
||||
---
|
||||
terminal_app_over_ssh=true, owned=false
|
||||
╭───────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v<VERSION>) │
|
||||
│ │
|
||||
│ model: loading /model to change │
|
||||
│ directory: loading │
|
||||
╰───────────────────────────────────────╯
|
||||
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
? for shortcuts
|
||||
@@ -132,6 +132,7 @@ impl StartupDraft {
|
||||
initialized_terminal.enhanced_keys_supported,
|
||||
initialized_terminal.stderr_guard,
|
||||
);
|
||||
tui.terminal_app_over_ssh = initialized_terminal.terminal_app_over_ssh;
|
||||
tui.set_alt_screen_enabled(screen.use_alt_screen);
|
||||
let mut pump = StartupDraftPump::new(&tui, initial_screen, session_action);
|
||||
pump.bottom_pane
|
||||
|
||||
@@ -128,6 +128,61 @@ fn startup_draft_renders_full_empty_and_multiline_composer_frames() {
|
||||
insta::assert_snapshot!("startup_draft_full_frames", snapshots.join("\n---\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_app_ssh_fallback_renders_inline_startup() {
|
||||
let pump = startup_test_pump(std::iter::empty());
|
||||
let owned_layout = super::layout::OwnedStartupLayout::new(
|
||||
&pump.header,
|
||||
&pump.bottom_pane,
|
||||
StartupDraftSessionAction::New,
|
||||
);
|
||||
let mut frames = Vec::new();
|
||||
for terminal_app_over_ssh in [false, true] {
|
||||
let owned = crate::determine_alt_screen_mode(
|
||||
/*no_alt_screen*/ false,
|
||||
codex_config::types::AltScreenMode::Auto,
|
||||
terminal_app_over_ssh,
|
||||
);
|
||||
let renderable = if owned {
|
||||
crate::render::renderable::RenderableItem::Borrowed(&owned_layout)
|
||||
} else {
|
||||
startup_draft_renderable(
|
||||
&pump.header,
|
||||
&pump.bottom_pane,
|
||||
StartupDraftSessionAction::New,
|
||||
)
|
||||
};
|
||||
let width = 48;
|
||||
let height = if owned {
|
||||
16
|
||||
} else {
|
||||
renderable.desired_height(width)
|
||||
};
|
||||
let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
renderable.render(area, &mut buffer);
|
||||
let frame = (0..height)
|
||||
.map(|y| {
|
||||
(0..width)
|
||||
.map(|x| buffer[(x, y)].symbol())
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
frames.push(format!(
|
||||
"terminal_app_over_ssh={terminal_app_over_ssh}, owned={owned}\n{frame}"
|
||||
));
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
"terminal_app_ssh_startup",
|
||||
frames
|
||||
.join("\n---\n")
|
||||
.replace(crate::version::CODEX_CLI_VERSION, "<VERSION>")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn startup_draft_clears_loading_status_when_starting_fresh() {
|
||||
let mut snapshots = Vec::new();
|
||||
|
||||
@@ -275,8 +275,22 @@ pub(super) async fn run_main_inner(
|
||||
let startup_presentation::StartupPresentation {
|
||||
bootstrap_config,
|
||||
config_cwd,
|
||||
screen,
|
||||
mut screen,
|
||||
} = presentation;
|
||||
screen.use_alt_screen = determine_alt_screen_mode(
|
||||
cli.no_alt_screen,
|
||||
bootstrap_config
|
||||
.config_toml
|
||||
.tui
|
||||
.as_ref()
|
||||
.map(|tui| tui.alternate_screen)
|
||||
.unwrap_or_default(),
|
||||
initialized_terminal.terminal_app_over_ssh,
|
||||
);
|
||||
screen.transcript_mode = crate::transcript_mode::TranscriptMode::resolve(
|
||||
screen.transcript_mode.is_owned(),
|
||||
screen.use_alt_screen,
|
||||
);
|
||||
let mut startup_draft = startup_draft::StartupDraft::new(
|
||||
initialized_terminal,
|
||||
terminal_restore_guard,
|
||||
|
||||
@@ -56,7 +56,12 @@ pub(super) async fn load(
|
||||
.as_ref()
|
||||
.map(|tui| tui.alternate_screen)
|
||||
.unwrap_or_default();
|
||||
let use_alt_screen = crate::determine_alt_screen_mode(cli.no_alt_screen, alternate_screen);
|
||||
// Terminal probing happens after configuration loads; startup finalizes this before painting.
|
||||
let use_alt_screen = crate::determine_alt_screen_mode(
|
||||
cli.no_alt_screen,
|
||||
alternate_screen,
|
||||
/*terminal_app_over_ssh*/ false,
|
||||
);
|
||||
let transcript_mode = crate::transcript_mode::TranscriptMode::resolve(
|
||||
config_toml
|
||||
.tui
|
||||
|
||||
@@ -16,6 +16,9 @@ use std::time::Duration;
|
||||
#[cfg(unix)]
|
||||
mod startup_replay;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod terminal_identity;
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
#[path = "terminal_probe/windows_replay.rs"]
|
||||
mod windows_replay;
|
||||
@@ -38,6 +41,7 @@ mod imp {
|
||||
use super::DefaultColors;
|
||||
use super::parse_default_colors;
|
||||
use super::startup_replay::startup_replay_input;
|
||||
use super::terminal_identity;
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io;
|
||||
@@ -61,6 +65,7 @@ mod imp {
|
||||
pub(crate) cursor_position: Option<Position>,
|
||||
pub(crate) default_colors: Option<DefaultColors>,
|
||||
pub(crate) keyboard_enhancement_supported: Option<bool>,
|
||||
pub(crate) terminal_app_over_ssh: Option<bool>,
|
||||
}
|
||||
|
||||
/// Whether the startup probe should query keyboard enhancement support.
|
||||
@@ -274,17 +279,32 @@ mod imp {
|
||||
keyboard_probe: StartupKeyboardEnhancementProbe,
|
||||
) -> io::Result<StartupProbe> {
|
||||
let mut tty = Tty::open()?;
|
||||
match keyboard_probe {
|
||||
StartupKeyboardEnhancementProbe::Query => {
|
||||
tty.write_all(b"\x1B[6n\x1B]10;?\x1B\\\x1B]11;?\x1B\\\x1B[?u\x1B[c")?;
|
||||
}
|
||||
StartupKeyboardEnhancementProbe::Skip => {
|
||||
tty.write_all(b"\x1B[6n\x1B]10;?\x1B\\\x1B]11;?\x1B\\")?;
|
||||
}
|
||||
let query_identity = codex_terminal_detection::terminal_info()
|
||||
.multiplexer
|
||||
.is_none()
|
||||
&& (std::env::var_os("SSH_TTY").is_some()
|
||||
|| std::env::var_os("SSH_CONNECTION").is_some());
|
||||
let mut query = b"\x1B[6n\x1B]10;?\x1B\\\x1B]11;?\x1B\\".to_vec();
|
||||
if keyboard_probe == StartupKeyboardEnhancementProbe::Query {
|
||||
query.extend_from_slice(b"\x1B[?u");
|
||||
}
|
||||
// DA1 must follow the keyboard query: its reply also finishes keyboard detection.
|
||||
if keyboard_probe == StartupKeyboardEnhancementProbe::Query || query_identity {
|
||||
query.extend_from_slice(b"\x1B[c");
|
||||
}
|
||||
if query_identity {
|
||||
query.extend_from_slice(b"\x1B[>c");
|
||||
}
|
||||
tty.write_all(&query)?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let result = read_startup_probe(&mut tty, timeout, keyboard_probe, &mut buffer);
|
||||
let result = read_startup_probe(
|
||||
&mut tty,
|
||||
timeout,
|
||||
keyboard_probe,
|
||||
query_identity,
|
||||
&mut buffer,
|
||||
);
|
||||
crossterm::event::buffer_input(&startup_replay_input(&buffer))?;
|
||||
result
|
||||
}
|
||||
@@ -320,6 +340,7 @@ mod imp {
|
||||
tty: &mut Tty,
|
||||
timeout: Duration,
|
||||
keyboard_probe: StartupKeyboardEnhancementProbe,
|
||||
query_identity: bool,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> io::Result<StartupProbe> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
@@ -327,6 +348,7 @@ mod imp {
|
||||
cursor_position: None,
|
||||
default_colors: None,
|
||||
keyboard_enhancement_supported: None,
|
||||
terminal_app_over_ssh: (!query_identity).then_some(/*t*/ false),
|
||||
};
|
||||
let mut saw_supported_keyboard = false;
|
||||
loop {
|
||||
@@ -364,6 +386,9 @@ mod imp {
|
||||
if probe.default_colors.is_none() {
|
||||
probe.default_colors = parse_default_colors(buffer);
|
||||
}
|
||||
if probe.terminal_app_over_ssh.is_none() {
|
||||
probe.terminal_app_over_ssh = terminal_identity::is_apple_terminal(buffer);
|
||||
}
|
||||
if keyboard_probe == StartupKeyboardEnhancementProbe::Skip
|
||||
|| probe.keyboard_enhancement_supported.is_some()
|
||||
{
|
||||
@@ -389,6 +414,7 @@ mod imp {
|
||||
) -> bool {
|
||||
probe.cursor_position.is_some()
|
||||
&& probe.default_colors.is_some()
|
||||
&& probe.terminal_app_over_ssh.is_some()
|
||||
&& (keyboard_probe == StartupKeyboardEnhancementProbe::Skip
|
||||
|| probe.keyboard_enhancement_supported.is_some())
|
||||
}
|
||||
@@ -633,6 +659,7 @@ mod imp {
|
||||
cursor_position: None,
|
||||
default_colors: None,
|
||||
keyboard_enhancement_supported: None,
|
||||
terminal_app_over_ssh: Some(false),
|
||||
};
|
||||
let mut saw_supported_keyboard = false;
|
||||
update_startup_probe(
|
||||
@@ -651,6 +678,7 @@ mod imp {
|
||||
bg: (17, 17, 17),
|
||||
}),
|
||||
keyboard_enhancement_supported: Some(true),
|
||||
terminal_app_over_ssh: Some(false),
|
||||
}
|
||||
);
|
||||
assert!(startup_probe_complete(
|
||||
@@ -659,6 +687,57 @@ mod imp {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_probe_waits_for_identity_even_when_keyboard_probe_is_skipped() {
|
||||
let mut probe = StartupProbe {
|
||||
cursor_position: None,
|
||||
default_colors: None,
|
||||
keyboard_enhancement_supported: None,
|
||||
terminal_app_over_ssh: None,
|
||||
};
|
||||
let mut supported_keyboard = false;
|
||||
let input =
|
||||
b"\x1b[1;1R\x1b]10;rgb:eeee/eeee/eeee\x07\x1b]11;rgb:1111/1111/1111\x07\x1b[?1;2c";
|
||||
update_startup_probe(
|
||||
&mut probe,
|
||||
&mut supported_keyboard,
|
||||
input,
|
||||
StartupKeyboardEnhancementProbe::Skip,
|
||||
);
|
||||
assert!(!startup_probe_complete(
|
||||
&probe,
|
||||
StartupKeyboardEnhancementProbe::Skip
|
||||
));
|
||||
let mut input = input.to_vec();
|
||||
input.extend_from_slice(b"\x1b[>1;95;0c");
|
||||
update_startup_probe(
|
||||
&mut probe,
|
||||
&mut supported_keyboard,
|
||||
&input,
|
||||
StartupKeyboardEnhancementProbe::Skip,
|
||||
);
|
||||
assert_eq!(probe.terminal_app_over_ssh, Some(true));
|
||||
assert!(startup_probe_complete(
|
||||
&probe,
|
||||
StartupKeyboardEnhancementProbe::Skip
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_identity_remains_unknown_at_the_deadline() {
|
||||
let mut tty = tty_with_buffered_input(b"\x1b[?1;2c");
|
||||
let mut buffer = Vec::new();
|
||||
let probe = read_startup_probe(
|
||||
&mut tty,
|
||||
Duration::from_millis(/*millis*/ 5),
|
||||
StartupKeyboardEnhancementProbe::Skip,
|
||||
/*query_identity*/ true,
|
||||
&mut buffer,
|
||||
)
|
||||
.expect("finish within probe deadline");
|
||||
assert_eq!(probe.terminal_app_over_ssh, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_probe_finds_responses_after_large_typeahead() {
|
||||
let mut input = vec![b'x'; 2_048];
|
||||
@@ -672,6 +751,7 @@ mod imp {
|
||||
&mut tty,
|
||||
Duration::from_secs(/*secs*/ 1),
|
||||
StartupKeyboardEnhancementProbe::Query,
|
||||
/*query_identity*/ false,
|
||||
&mut buffer,
|
||||
)
|
||||
.expect("read terminal responses after buffered typeahead");
|
||||
@@ -685,6 +765,7 @@ mod imp {
|
||||
bg: (17, 17, 17),
|
||||
}),
|
||||
keyboard_enhancement_supported: Some(true),
|
||||
terminal_app_over_ssh: Some(false),
|
||||
}
|
||||
);
|
||||
let mut expected_replay = vec![b'x'; 2_048];
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Recognize Terminal.app's device attributes without interpreting pasted text as a reply.
|
||||
|
||||
/// A complete pair is required; missing or incomplete replies remain unknown.
|
||||
pub(super) fn is_apple_terminal(input: &[u8]) -> Option<bool> {
|
||||
let primary = device_attributes(input, b"[?")?;
|
||||
let secondary = device_attributes(input, b"[>")?;
|
||||
Some(primary == b"1;2" && secondary == b"1;95;0")
|
||||
}
|
||||
|
||||
fn device_attributes<'a>(input: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
|
||||
let mut inside_paste = false;
|
||||
for sequence in input.split(|byte| *byte == b'\x1b').skip(/*n*/ 1) {
|
||||
match sequence {
|
||||
[b'[', b'2', b'0', b'0', b'~', ..] => inside_paste = true,
|
||||
[b'[', b'2', b'0', b'1', b'~', ..] => inside_paste = false,
|
||||
_ if !inside_paste => {
|
||||
if let Some(payload) = sequence.strip_prefix(prefix)
|
||||
&& let Some(end) = payload
|
||||
.iter()
|
||||
.take(/*n*/ 64)
|
||||
.position(|byte| (0x40..=0x7e).contains(byte))
|
||||
&& payload[end] == b'c'
|
||||
{
|
||||
return Some(&payload[..end]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "terminal_identity_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Cover observed terminal signatures, fragmented replies, and pasted lookalikes.
|
||||
|
||||
use super::is_apple_terminal;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn distinguishes_the_native_terminal_replies() {
|
||||
for (primary, secondary, expected) in [
|
||||
("1;2", "1;95;0", true),
|
||||
("62;22;52", "1;10;0", false),
|
||||
("64;1;2;4;6;17;18;21;22;52", "64;2500;0", false),
|
||||
("62;52;", "1;4000;46", false),
|
||||
("65;4;6;18;22", "1;277;0", false),
|
||||
("6", "0;2600;1", false),
|
||||
("1;2", "0;95;0", false),
|
||||
("6", "1;95;0", false),
|
||||
("1;2", "1;95;00", false),
|
||||
("1;2", "1;95;1", false),
|
||||
] {
|
||||
let primary = format!("\x1b[?{primary}c");
|
||||
let secondary = format!("\x1b[>{secondary}c");
|
||||
for input in [
|
||||
format!("{primary}{secondary}"),
|
||||
format!("{secondary}{primary}"),
|
||||
] {
|
||||
assert_eq!(
|
||||
is_apple_terminal(input.as_bytes()),
|
||||
Some(expected),
|
||||
"{input:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_csi_replies_cannot_consume_typed_text_as_device_attributes() {
|
||||
for reply in [b"\x1b[?7u".as_slice(), b"\x1b[>4;2m".as_slice()] {
|
||||
let mut input = reply.to_vec();
|
||||
input.extend_from_slice(b"c\x1b[>1;95;0c");
|
||||
assert_eq!(is_apple_terminal(&input), None);
|
||||
input.extend_from_slice(b"\x1b[?1;2c");
|
||||
assert_eq!(is_apple_terminal(&input), Some(true));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_waits_for_complete_replies_and_ignores_pasted_signatures() {
|
||||
let signature = b"\x1b[?1;2c\x1b[>1;95;0c";
|
||||
for end in 0..signature.len() {
|
||||
assert_eq!(is_apple_terminal(&signature[..end]), None);
|
||||
}
|
||||
let mut input = b"typed\x1b[200~".to_vec();
|
||||
input.extend_from_slice(signature);
|
||||
assert_eq!(is_apple_terminal(&input), None);
|
||||
input.extend_from_slice(b"\x1b[201~more typed");
|
||||
assert_eq!(is_apple_terminal(&input), None);
|
||||
input.extend_from_slice(signature);
|
||||
assert_eq!(is_apple_terminal(&input), Some(true));
|
||||
}
|
||||
@@ -99,6 +99,7 @@ pub type Terminal = CustomTerminal<CrosstermBackend<Stdout>>;
|
||||
pub(crate) struct InitializedTerminal {
|
||||
pub(crate) terminal: Terminal,
|
||||
pub(crate) enhanced_keys_supported: bool,
|
||||
pub(crate) terminal_app_over_ssh: bool,
|
||||
pub(crate) stderr_guard: terminal_stderr::TerminalStderrGuard,
|
||||
}
|
||||
|
||||
@@ -474,6 +475,7 @@ pub(crate) fn init() -> Result<InitializedTerminal> {
|
||||
cursor_position: None,
|
||||
default_colors: None,
|
||||
keyboard_enhancement_supported: None,
|
||||
terminal_app_over_ssh: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -516,6 +518,12 @@ pub(crate) fn init() -> Result<InitializedTerminal> {
|
||||
let initialized_terminal = InitializedTerminal {
|
||||
terminal: tui,
|
||||
enhanced_keys_supported,
|
||||
#[cfg(unix)]
|
||||
terminal_app_over_ssh: startup_probe
|
||||
.terminal_app_over_ssh
|
||||
.unwrap_or(/*default*/ false),
|
||||
#[cfg(not(unix))]
|
||||
terminal_app_over_ssh: false,
|
||||
stderr_guard,
|
||||
};
|
||||
restore_guard.active = false;
|
||||
@@ -630,6 +638,8 @@ pub struct Tui {
|
||||
// True when terminal/tab is focused; updated internally from crossterm events
|
||||
terminal_focused: Arc<AtomicBool>,
|
||||
enhanced_keys_supported: bool,
|
||||
// Cache the startup result so later configuration loads use the same terminal policy.
|
||||
pub(crate) terminal_app_over_ssh: bool,
|
||||
notification_backend: Option<DesktopNotificationBackend>,
|
||||
notification_condition: NotificationCondition,
|
||||
scrollback: ScrollbackStrategy,
|
||||
@@ -701,6 +711,7 @@ impl Tui {
|
||||
alt_screen_active: Arc::new(AtomicBool::new(false)),
|
||||
terminal_focused: Arc::new(AtomicBool::new(true)),
|
||||
enhanced_keys_supported,
|
||||
terminal_app_over_ssh: false,
|
||||
notification_backend: Some(detect_backend(NotificationMethod::default())),
|
||||
notification_condition: NotificationCondition::default(),
|
||||
scrollback,
|
||||
|
||||
Reference in New Issue
Block a user