fix(tui): default composer_arrows_scroll on when mouse capture is off
On platforms where mouse capture is disabled by default (Windows CMD / legacy conhost), the terminal sends mouse-wheel events as Up/Down arrow- key sequences. Without composer_arrows_scroll those sequences cycle the input history instead of scrolling the transcript (#1443). Set the default for composer_arrows_scroll to !use_mouse_capture so that terminals that forward wheel events as arrows get page-scrolling out of the box, while terminals with real mouse capture (Windows Terminal, Linux, macOS) keep the existing history-navigation default. The explicit [tui] composer_arrows_scroll config key still overrides the derived default in both directions. Also enable mouse capture by default for ConEmu/Cmder (ConEmuPID env var), which handles VT mouse-mode reporting cleanly, giving those users in-app scrolling without needing --mouse-capture. Fixes #1443 Signed-off-by: CrepuscularIRIS <serenitygp@qq.com>
This commit is contained in:
committed by
Hunter Bown
parent
ae45d1054b
commit
dde1e5e2f1
+43
-15
@@ -3648,12 +3648,14 @@ fn should_use_alt_screen(_cli: &Cli, _config: &Config) -> bool {
|
||||
fn should_use_mouse_capture(cli: &Cli, config: &Config, use_alt_screen: bool) -> bool {
|
||||
let terminal_emulator = std::env::var("TERMINAL_EMULATOR").ok();
|
||||
let wt_session = std::env::var("WT_SESSION").ok().filter(|s| !s.is_empty());
|
||||
let conemu_pid = std::env::var("ConEmuPID").ok().filter(|s| !s.is_empty());
|
||||
should_use_mouse_capture_with(
|
||||
cli,
|
||||
config,
|
||||
use_alt_screen,
|
||||
terminal_emulator.as_deref(),
|
||||
wt_session.as_deref(),
|
||||
conemu_pid.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3663,6 +3665,7 @@ fn should_use_mouse_capture_with(
|
||||
use_alt_screen: bool,
|
||||
terminal_emulator: Option<&str>,
|
||||
wt_session: Option<&str>,
|
||||
conemu_pid: Option<&str>,
|
||||
) -> bool {
|
||||
if !use_alt_screen || cli.no_mouse_capture {
|
||||
return false;
|
||||
@@ -3674,15 +3677,16 @@ fn should_use_mouse_capture_with(
|
||||
.tui
|
||||
.as_ref()
|
||||
.and_then(|tui| tui.mouse_capture)
|
||||
.unwrap_or_else(|| default_mouse_capture_enabled(terminal_emulator, wt_session))
|
||||
.unwrap_or_else(|| default_mouse_capture_enabled(terminal_emulator, wt_session, conemu_pid))
|
||||
}
|
||||
|
||||
/// Whether to enable terminal mouse capture by default for this platform/host.
|
||||
///
|
||||
/// On Windows the default depends on the host: Windows Terminal (which sets
|
||||
/// `WT_SESSION`) handles mouse-mode reporting cleanly, so default-on there
|
||||
/// gives users in-app text selection and keeps the application's selection
|
||||
/// clamped to the transcript area (#1169). Legacy conhost stays default-off
|
||||
/// `WT_SESSION`) and ConEmu/Cmder (which set `ConEmuPID`) handle mouse-mode
|
||||
/// reporting cleanly, so default-on there gives users in-app text selection
|
||||
/// and keeps the application's selection clamped to the transcript area
|
||||
/// (#1169). Legacy conhost (CMD without either env var) stays default-off
|
||||
/// because its mouse-mode reporting can leak SGR escape sequences as raw
|
||||
/// text into the composer (#878 / #898).
|
||||
///
|
||||
@@ -3693,9 +3697,10 @@ fn should_use_mouse_capture_with(
|
||||
fn default_mouse_capture_enabled(
|
||||
terminal_emulator: Option<&str>,
|
||||
wt_session: Option<&str>,
|
||||
conemu_pid: Option<&str>,
|
||||
) -> bool {
|
||||
if cfg!(windows) {
|
||||
return wt_session.is_some();
|
||||
return wt_session.is_some() || conemu_pid.is_some();
|
||||
}
|
||||
if matches!(terminal_emulator, Some(t) if t.eq_ignore_ascii_case("JetBrains-JediTerm")) {
|
||||
return false;
|
||||
@@ -4661,21 +4666,21 @@ mod terminal_mode_tests {
|
||||
let config = Config::default();
|
||||
|
||||
assert!(should_use_mouse_capture_with(
|
||||
&cli, &config, true, None, None
|
||||
&cli, &config, true, None, None, None
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn mouse_capture_defaults_off_on_legacy_windows_console() {
|
||||
// Legacy conhost (no `WT_SESSION`) keeps the v0.8.x default-off
|
||||
// behavior: mouse-mode reporting on legacy console can leak SGR
|
||||
// escapes into the composer.
|
||||
// Legacy conhost (no `WT_SESSION` and no `ConEmuPID`) keeps the
|
||||
// v0.8.x default-off behavior: mouse-mode reporting on legacy console
|
||||
// can leak SGR escapes into the composer.
|
||||
let cli = parse_cli(&["deepseek"]);
|
||||
let config = Config::default();
|
||||
|
||||
assert!(!should_use_mouse_capture_with(
|
||||
&cli, &config, true, None, None
|
||||
&cli, &config, true, None, None, None
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4696,6 +4701,25 @@ mod terminal_mode_tests {
|
||||
true,
|
||||
None,
|
||||
Some("{a3a3b3a8-aa00-0000-0000-000000000000}"),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
// ConEmu/Cmder sets `ConEmuPID` and handles VT mouse-mode reporting
|
||||
// cleanly; default mouse capture on there so users get in-app scrolling.
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn mouse_capture_defaults_on_in_conemu() {
|
||||
let cli = parse_cli(&["deepseek"]);
|
||||
let config = Config::default();
|
||||
|
||||
assert!(should_use_mouse_capture_with(
|
||||
&cli,
|
||||
&config,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
Some("12345"),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4705,7 +4729,7 @@ mod terminal_mode_tests {
|
||||
let config = Config::default();
|
||||
|
||||
assert!(!should_use_mouse_capture_with(
|
||||
&cli, &config, true, None, None
|
||||
&cli, &config, true, None, None, None
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4726,7 +4750,7 @@ mod terminal_mode_tests {
|
||||
};
|
||||
|
||||
assert!(!should_use_mouse_capture_with(
|
||||
&cli, &config, true, None, None
|
||||
&cli, &config, true, None, None, None
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4736,7 +4760,7 @@ mod terminal_mode_tests {
|
||||
let config = Config::default();
|
||||
|
||||
assert!(should_use_mouse_capture_with(
|
||||
&cli, &config, true, None, None
|
||||
&cli, &config, true, None, None, None
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4757,7 +4781,7 @@ mod terminal_mode_tests {
|
||||
};
|
||||
|
||||
assert!(should_use_mouse_capture_with(
|
||||
&cli, &config, true, None, None
|
||||
&cli, &config, true, None, None, None
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4767,7 +4791,7 @@ mod terminal_mode_tests {
|
||||
let config = Config::default();
|
||||
|
||||
assert!(!should_use_mouse_capture_with(
|
||||
&cli, &config, false, None, None
|
||||
&cli, &config, false, None, None, None
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4789,6 +4813,7 @@ mod terminal_mode_tests {
|
||||
true,
|
||||
Some("JetBrains-JediTerm"),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4805,6 +4830,7 @@ mod terminal_mode_tests {
|
||||
true,
|
||||
Some("jetbrains-jediterm"),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4819,6 +4845,7 @@ mod terminal_mode_tests {
|
||||
true,
|
||||
Some("JetBrains-JediTerm"),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -4844,6 +4871,7 @@ mod terminal_mode_tests {
|
||||
true,
|
||||
Some("JetBrains-JediTerm"),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,7 +786,10 @@ pub struct App {
|
||||
pub use_alt_screen: bool,
|
||||
pub use_mouse_capture: bool,
|
||||
/// When true, plain Up/Down on an empty composer scroll the transcript
|
||||
/// instead of navigating input history (#1117 opt-in).
|
||||
/// instead of navigating input history. Defaults to `true` when mouse
|
||||
/// capture is off: terminals that convert mouse-wheel events to arrow-key
|
||||
/// sequences (e.g. Windows CMD without `WT_SESSION`) get page-scrolling
|
||||
/// without any explicit config (#1443).
|
||||
pub composer_arrows_scroll: bool,
|
||||
pub use_bracketed_paste: bool,
|
||||
pub use_paste_burst_detection: bool,
|
||||
@@ -1542,7 +1545,7 @@ impl App {
|
||||
.tui
|
||||
.as_ref()
|
||||
.and_then(|tui| tui.composer_arrows_scroll)
|
||||
.unwrap_or(false),
|
||||
.unwrap_or(!use_mouse_capture),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1022,6 +1022,30 @@ fn create_test_app() -> App {
|
||||
App::new(options, &Config::default())
|
||||
}
|
||||
|
||||
fn create_test_options() -> TuiOptions {
|
||||
TuiOptions {
|
||||
model: "deepseek-v4-pro".to_string(),
|
||||
workspace: PathBuf::from("."),
|
||||
config_path: None,
|
||||
config_profile: None,
|
||||
allow_shell: false,
|
||||
use_alt_screen: true,
|
||||
use_mouse_capture: false,
|
||||
use_bracketed_paste: true,
|
||||
max_subagents: 1,
|
||||
skills_dir: PathBuf::from("."),
|
||||
memory_path: PathBuf::from("memory.md"),
|
||||
notes_path: PathBuf::from("notes.txt"),
|
||||
mcp_config_path: PathBuf::from("mcp.json"),
|
||||
use_memory: false,
|
||||
start_in_agent_mode: false,
|
||||
skip_onboarding: false,
|
||||
yolo: false,
|
||||
resume_session_id: None,
|
||||
initial_input: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_message(role: &str, text: &str) -> Message {
|
||||
Message {
|
||||
role: role.to_string(),
|
||||
@@ -4567,9 +4591,12 @@ fn checklist_write_renders_dedicated_card() {
|
||||
#[test]
|
||||
fn history_arrow_handles_empty_input() {
|
||||
let mut app = create_test_app();
|
||||
// Explicitly disable arrows-scroll so this test covers the
|
||||
// history-navigation path regardless of the mouse-capture default.
|
||||
app.composer_arrows_scroll = false;
|
||||
app.input_history.push("previous prompt".to_string());
|
||||
|
||||
// Default: empty composer Up navigates input history (#1117).
|
||||
// With arrows-scroll off: empty composer Up navigates input history (#1117).
|
||||
assert!(handle_composer_history_arrow(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
|
||||
@@ -4582,6 +4609,9 @@ fn history_arrow_handles_empty_input() {
|
||||
#[test]
|
||||
fn history_arrow_handles_whitespace_input() {
|
||||
let mut app = create_test_app();
|
||||
// Explicitly disable arrows-scroll so this test covers the
|
||||
// history-navigation path regardless of the mouse-capture default.
|
||||
app.composer_arrows_scroll = false;
|
||||
app.input = " ".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
app.input_history.push("previous prompt".to_string());
|
||||
@@ -4660,6 +4690,56 @@ fn composer_arrows_scroll_nonempty_still_navigates_history() {
|
||||
assert_eq!(app.input, "previous prompt");
|
||||
}
|
||||
|
||||
// #1443: when mouse capture is off (e.g. Windows CMD), arrow-scroll
|
||||
// must default to true so mouse-wheel events (sent as arrow keys by
|
||||
// the terminal) scroll the transcript rather than cycling history.
|
||||
#[test]
|
||||
fn composer_arrows_scroll_defaults_true_without_mouse_capture() {
|
||||
let options = TuiOptions {
|
||||
use_mouse_capture: false,
|
||||
..create_test_options()
|
||||
};
|
||||
let app = App::new(options, &Config::default());
|
||||
assert!(
|
||||
app.composer_arrows_scroll,
|
||||
"arrows-scroll must default to true when mouse capture is off"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_arrows_scroll_defaults_false_with_mouse_capture() {
|
||||
let options = TuiOptions {
|
||||
use_mouse_capture: true,
|
||||
..create_test_options()
|
||||
};
|
||||
let app = App::new(options, &Config::default());
|
||||
assert!(
|
||||
!app.composer_arrows_scroll,
|
||||
"arrows-scroll must default to false when mouse capture is on"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composer_arrows_scroll_config_overrides_default() {
|
||||
let config = Config {
|
||||
tui: Some(crate::config::TuiConfig {
|
||||
composer_arrows_scroll: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
..Config::default()
|
||||
};
|
||||
// Even with mouse_capture off, explicit config=false wins.
|
||||
let options = TuiOptions {
|
||||
use_mouse_capture: false,
|
||||
..create_test_options()
|
||||
};
|
||||
let app = App::new(options, &config);
|
||||
assert!(
|
||||
!app.composer_arrows_scroll,
|
||||
"explicit config=false must override the mouse-capture-derived default"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notification_settings_tui_always_keeps_configured_method_no_threshold() {
|
||||
let config = Config {
|
||||
|
||||
Reference in New Issue
Block a user