From 88c2a06024bf28702792e046e8c3d7ffcfa8109d Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Wed, 6 May 2026 15:12:47 -0500 Subject: [PATCH] fix(tui): default mouse capture off in JetBrains JediTerm (#878, #898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JetBrains' JediTerm — the terminal embedded in PyCharm, IDEA, CLion, WebStorm, GoLand, etc. — advertises mouse support but forwards SGR mouse-event escape sequences as raw input characters rather than interpreting them. Users see the composer auto-fill with garbled characters when they move the mouse over the TUI window. The workaround was already a one-flag fix (`--no-mouse-capture` or `[tui] mouse_capture = false` in config) but discovering it required finding a maintainer comment on a related issue. Auto-detect via `TERMINAL_EMULATOR=JetBrains-JediTerm` (the env var JediTerm sets) and default `mouse_capture` off for that environment, mirroring the existing Windows handling. Explicit `--mouse-capture` or `[tui] mouse_capture = true` still wins, so power users who don't hit the issue can opt back in. Implementation: - `default_mouse_capture_enabled` now takes `terminal_emulator: Option<&str>` so the function is pure and trivially testable. The CLI entry point reads the env var once and passes it through. - `should_use_mouse_capture` keeps the same public signature; tests call `should_use_mouse_capture_with` which takes the env explicitly, removing test sensitivity to the host's actual TERMINAL_EMULATOR. - Match is `eq_ignore_ascii_case` because JetBrains has occasionally varied the casing across releases. Tests: - 4 new tests covering JetBrains default-off, case-insensitive match, CLI override, and config-file override. - Existing 6 mouse-capture tests retained, all passing. - `cargo test -p deepseek-tui --bin deepseek-tui --all-features terminal_mode_tests --locked` → 10/10 pass. - `cargo clippy -p deepseek-tui --bins --all-features --locked -- -D warnings` clean. - `cargo fmt --all -- --check` clean. Docs in `docs/MODES.md` and `docs/CONFIGURATION.md` updated. Co-Authored-By: Claude Opus 4.7 --- crates/tui/src/main.rs | 115 +++++++++++++++++++++++++++++++++++++---- docs/CONFIGURATION.md | 2 +- docs/MODES.md | 2 +- 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index e8a225f8..c8d6c1ce 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -3466,6 +3466,16 @@ 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(); + should_use_mouse_capture_with(cli, config, use_alt_screen, terminal_emulator.as_deref()) +} + +fn should_use_mouse_capture_with( + cli: &Cli, + config: &Config, + use_alt_screen: bool, + terminal_emulator: Option<&str>, +) -> bool { if !use_alt_screen || cli.no_mouse_capture { return false; } @@ -3476,11 +3486,26 @@ fn should_use_mouse_capture(cli: &Cli, config: &Config, use_alt_screen: bool) -> .tui .as_ref() .and_then(|tui| tui.mouse_capture) - .unwrap_or_else(default_mouse_capture_enabled) + .unwrap_or_else(|| default_mouse_capture_enabled(terminal_emulator)) } -fn default_mouse_capture_enabled() -> bool { - !cfg!(windows) +/// Whether to enable terminal mouse capture by default for this platform/host. +/// +/// Returns `false` on Windows (legacy console mouse-mode reporting is flaky; +/// `--mouse-capture` opts in) and on JetBrains' JediTerm, which advertises +/// mouse support but delivers SGR mouse-event escape sequences as raw text +/// in the input stream — visible to users as garbled characters in the +/// composer when they move the mouse over the TUI (#878, #898). The user +/// can still opt back in with `[tui] mouse_capture = true` in +/// `~/.deepseek/config.toml` or `--mouse-capture`. +fn default_mouse_capture_enabled(terminal_emulator: Option<&str>) -> bool { + if cfg!(windows) { + return false; + } + if matches!(terminal_emulator, Some(t) if t.eq_ignore_ascii_case("JetBrains-JediTerm")) { + return false; + } + true } fn is_zellij() -> bool { @@ -4236,7 +4261,7 @@ mod terminal_mode_tests { let cli = parse_cli(&["deepseek"]); let config = Config::default(); - assert!(should_use_mouse_capture(&cli, &config, true)); + assert!(should_use_mouse_capture_with(&cli, &config, true, None)); } #[test] @@ -4245,7 +4270,7 @@ mod terminal_mode_tests { let cli = parse_cli(&["deepseek"]); let config = Config::default(); - assert!(!should_use_mouse_capture(&cli, &config, true)); + assert!(!should_use_mouse_capture_with(&cli, &config, true, None)); } #[test] @@ -4253,7 +4278,7 @@ mod terminal_mode_tests { let cli = parse_cli(&["deepseek", "--no-mouse-capture"]); let config = Config::default(); - assert!(!should_use_mouse_capture(&cli, &config, true)); + assert!(!should_use_mouse_capture_with(&cli, &config, true, None)); } #[test] @@ -4270,7 +4295,7 @@ mod terminal_mode_tests { ..Config::default() }; - assert!(!should_use_mouse_capture(&cli, &config, true)); + assert!(!should_use_mouse_capture_with(&cli, &config, true, None)); } #[test] @@ -4278,7 +4303,7 @@ mod terminal_mode_tests { let cli = parse_cli(&["deepseek", "--mouse-capture"]); let config = Config::default(); - assert!(should_use_mouse_capture(&cli, &config, true)); + assert!(should_use_mouse_capture_with(&cli, &config, true, None)); } #[test] @@ -4295,7 +4320,7 @@ mod terminal_mode_tests { ..Config::default() }; - assert!(should_use_mouse_capture(&cli, &config, true)); + assert!(should_use_mouse_capture_with(&cli, &config, true, None)); } #[test] @@ -4303,7 +4328,77 @@ mod terminal_mode_tests { let cli = parse_cli(&["deepseek", "--mouse-capture"]); let config = Config::default(); - assert!(!should_use_mouse_capture(&cli, &config, false)); + assert!(!should_use_mouse_capture_with(&cli, &config, false, None)); + } + + // Issue #878 / #898: JetBrains JediTerm advertises mouse support but + // forwards SGR mouse-event escapes as raw input characters, producing + // the "input box auto-fills with garbled characters when I move the + // mouse" failure mode in PyCharm/IDEA terminals. Default the capture + // off when we see TERMINAL_EMULATOR=JetBrains-JediTerm; explicit + // config / --mouse-capture still wins. + + #[test] + fn mouse_capture_defaults_off_in_jetbrains_jediterm() { + let cli = parse_cli(&["deepseek"]); + let config = Config::default(); + + assert!(!should_use_mouse_capture_with( + &cli, + &config, + true, + Some("JetBrains-JediTerm"), + )); + } + + #[test] + fn jetbrains_default_off_is_case_insensitive() { + let cli = parse_cli(&["deepseek"]); + let config = Config::default(); + + // JetBrains has occasionally varied the casing across releases; + // a case-insensitive match keeps the protection in place. + assert!(!should_use_mouse_capture_with( + &cli, + &config, + true, + Some("jetbrains-jediterm"), + )); + } + + #[test] + fn mouse_capture_flag_overrides_jetbrains_default() { + let cli = parse_cli(&["deepseek", "--mouse-capture"]); + let config = Config::default(); + + assert!(should_use_mouse_capture_with( + &cli, + &config, + true, + Some("JetBrains-JediTerm"), + )); + } + + #[test] + fn config_mouse_capture_true_overrides_jetbrains_default() { + let cli = parse_cli(&["deepseek"]); + let config = Config { + tui: Some(crate::config::TuiConfig { + alternate_screen: None, + mouse_capture: Some(true), + terminal_probe_timeout_ms: None, + status_items: None, + osc8_links: None, + }), + ..Config::default() + }; + + assert!(should_use_mouse_capture_with( + &cli, + &config, + true, + Some("JetBrains-JediTerm"), + )); } } diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index a0b047cb..25b1af30 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -394,7 +394,7 @@ If you are upgrading from older releases: `false`. When `true`, the notification body includes the elapsed duration and the turn's cost in the configured display currency. - `tui.alternate_screen` (string, optional): `auto`, `always`, or `never`. `auto` disables the alternate screen in Zellij; `--no-alt-screen` forces inline mode. Set `never` or run with `--no-alt-screen` when you want real terminal scrollback. -- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and `false` on Windows when the alternate screen is active): enable internal mouse scrolling, transcript selection, and right-click context actions. TUI-owned drag selection copies only user/assistant transcript text. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in on Windows. +- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals when the alternate screen is active; `false` on Windows and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, and right-click context actions. TUI-owned drag selection copies only user/assistant transcript text. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. - `tui.terminal_probe_timeout_ms` (int, optional, default `500`): startup terminal-mode probe timeout in milliseconds. Values are clamped to `100..=5000`; timeout emits a warning and aborts startup instead of hanging indefinitely. - `tui.osc8_links` (bool, optional, default `true`): emit OSC 8 escape sequences around URLs in transcript output so terminals that support them (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) render them as Cmd+click hyperlinks. Terminals without OSC 8 support render the plain URL and ignore the escape. Set `false` for terminals that misrender the sequence; selection/clipboard output always strips the escapes. - `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`). diff --git a/docs/MODES.md b/docs/MODES.md index 7b320294..d4755641 100644 --- a/docs/MODES.md +++ b/docs/MODES.md @@ -84,7 +84,7 @@ Run `deepseek --help` for the canonical list. Common flags: - `-c, --continue`: resume the most recent session in this workspace - `--max-subagents `: clamp to `1..=20` - `--no-alt-screen`: run inline without the alternate screen buffer -- `--mouse-capture` / `--no-mouse-capture`: opt in or out of internal mouse scrolling, transcript selection, and right-click context actions. Mouse capture is enabled by default on non-Windows terminals so drag selection copies only user/assistant transcript text; hold Shift while dragging or use `--no-mouse-capture` for raw terminal selection. On Windows it defaults off to avoid CMD/terminal mouse escape sequences being inserted into the prompt; use `--mouse-capture` to opt in. +- `--mouse-capture` / `--no-mouse-capture`: opt in or out of internal mouse scrolling, transcript selection, and right-click context actions. Mouse capture is enabled by default on non-Windows terminals so drag selection copies only user/assistant transcript text; hold Shift while dragging or use `--no-mouse-capture` for raw terminal selection. It defaults off on Windows (CMD/terminal mouse-escape spam in the prompt) and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where the terminal advertises mouse support but forwards SGR mouse events as raw text (#878, #898). Use `--mouse-capture` to opt in anywhere it's defaulted off. - `--profile `: select config profile - `--config `: config file path - `-v, --verbose`: verbose logging