feat(tui2): tune scrolling inpu based on (#8357)

## TUI2: Normalize Mouse Scroll Input Across Terminals (Wheel +
Trackpad)

This changes TUI2 scrolling to a stream-based model that normalizes
terminal scroll event density into consistent wheel behavior (default:
~3 transcript lines per physical wheel notch) while keeping trackpad
input higher fidelity via fractional accumulation.

Primary code: `codex-rs/tui2/src/tui/scrolling/mouse.rs`

Doc of record (model + probe-derived data):
`codex-rs/tui2/docs/scroll_input_model.md`

### Why

Terminals encode both mouse wheels and trackpads as discrete scroll
up/down events with direction but no magnitude, and they vary widely in
how many raw events they emit per physical wheel notch (commonly 1, 3,
or 9+). Timing alone doesn’t reliably distinguish wheel vs trackpad, so
cadence-based heuristics are unstable across terminals/hardware.

This PR treats scroll input as short *streams* separated by silence or
direction flips, normalizes raw event density into tick-equivalents,
coalesces redraws for dense streams, and exposes explicit config
overrides.

### What Changed

#### Scroll Model (TUI2)

- Stream detection
  - Start a stream on the first scroll event.
  - End a stream on an idle gap (`STREAM_GAP_MS`) or a direction flip.
- Normalization
- Convert raw events into tick-equivalents using per-terminal
`tui.scroll_events_per_tick`.
- Wheel-like vs trackpad-like behavior
- Wheel-like: fixed “classic” lines per wheel notch; flush immediately
for responsiveness.
- Trackpad-like: fractional accumulation + carry across stream
boundaries; coalesce flushes to ~60Hz to avoid floods and reduce “stop
lag / overshoot”.
- Trackpad divisor is intentionally capped: `min(scroll_events_per_tick,
3)` so terminals with dense wheel ticks (e.g. 9 events per notch) don’t
make trackpads feel artificially slow.
- Auto mode (default)
  - Start conservatively as trackpad-like (avoid overshoot).
- Promote to wheel-like if the first tick-worth of events arrives
quickly.
- Fallback for 1-event-per-tick terminals (no tick-completion timing
signal).

#### Trackpad Acceleration

Some terminals produce relatively low vertical event density for
trackpad gestures, which makes large/faster swipes feel sluggish even
when small motions feel correct. To address that, trackpad-like streams
apply a bounded multiplier based on event count:

- `multiplier = clamp(1 + abs(events) / scroll_trackpad_accel_events,
1..scroll_trackpad_accel_max)`

The multiplier is applied to the trackpad stream’s computed line delta
(including carried fractional remainder). Defaults are conservative and
bounded.

#### Config Knobs (TUI2)

All keys live under `[tui]`:

- `scroll_wheel_lines`: lines per physical wheel notch (default: 3).
- `scroll_events_per_tick`: raw vertical scroll events per physical
wheel notch (terminal-specific default; fallback: 3).
- Wheel-like per-event contribution: `scroll_wheel_lines /
scroll_events_per_tick`.
- `scroll_trackpad_lines`: baseline trackpad sensitivity (default: 1).
- Trackpad-like per-event contribution: `scroll_trackpad_lines /
min(scroll_events_per_tick, 3)`.
- `scroll_trackpad_accel_events` / `scroll_trackpad_accel_max`: bounded
trackpad acceleration (defaults: 30 / 3).
- `scroll_mode = auto|wheel|trackpad`: force behavior or use the
heuristic (default: `auto`).
- `scroll_wheel_tick_detect_max_ms`: auto-mode promotion threshold (ms).
- `scroll_wheel_like_max_duration_ms`: auto-mode fallback for
1-event-per-tick terminals (ms).
- `scroll_invert`: invert scroll direction (applies to wheel +
trackpad).

Config docs: `docs/config.md` and field docs in
`codex-rs/core/src/config/types.rs`.

#### App Integration

- The app schedules follow-up ticks to close idle streams (via
`ScrollUpdate::next_tick_in` and `schedule_frame_in`) and finalizes
streams on draw ticks.
  - `codex-rs/tui2/src/app.rs`

#### Docs

- Single doc of record describing the model + preserved probe
findings/spec:
  - `codex-rs/tui2/docs/scroll_input_model.md`

#### Other (jj-only friendliness)

- `codex-rs/tui2/src/diff_render.rs`: prefer stable cwd-relative paths
when the file is under the cwd even if there’s no `.git`.

### Terminal Defaults

Per-terminal defaults are derived from scroll-probe logs (see doc).
Notable:

- Ghostty currently defaults to `scroll_events_per_tick = 3` even though
logs measured ~9 in one setup. This is a deliberate stopgap; if your
Ghostty build emits ~9 events per wheel notch, set:

  ```toml
  [tui]
  scroll_events_per_tick = 9
  ```

### Testing

- `just fmt`
- `just fix -p codex-core --allow-no-vcs`
- `cargo test -p codex-core --lib` (pass)
- `cargo test -p codex-tui2` (scroll tests pass; remaining failures are
known flaky VT100 color tests in `insert_history`)

### Review Focus

- Stream finalization + frame scheduling in `codex-rs/tui2/src/app.rs`.
- Auto-mode promotion thresholds and the 1-event-per-tick fallback
behavior.
- Trackpad divisor cap (`min(events_per_tick, 3)`) and acceleration
defaults.
- Ghostty default tradeoff (3 vs ~9) and whether we should change it.
This commit is contained in:
Josh McKinney
2025-12-20 12:48:12 -08:00
committed by GitHub
parent a6974087e5
commit 63942b883c
9 changed files with 2219 additions and 13 deletions

View File

@@ -885,6 +885,54 @@ notifications = [ "agent-turn-complete", "approval-requested" ]
# Disable terminal animations (welcome screen, status shimmer, spinner).
# Defaults to true.
animations = false
# TUI2 mouse scrolling (wheel + trackpad)
#
# Terminals emit different numbers of raw scroll events per physical wheel notch (commonly 1, 3,
# or 9+). TUI2 normalizes raw event density into consistent wheel behavior (default: ~3 lines per
# wheel notch) while keeping trackpad input higher fidelity via fractional accumulation.
#
# See `codex-rs/tui2/docs/scroll_input_model.md` for the model and probe data.
# Override *wheel* event density (raw events per physical wheel notch). TUI2 only.
#
# Wheel-like per-event contribution is:
# - `scroll_wheel_lines / scroll_events_per_tick`
#
# Trackpad-like streams use `min(scroll_events_per_tick, 3)` as the divisor so dense wheel ticks
# (e.g. 9 events per notch) do not make trackpads feel artificially slow.
scroll_events_per_tick = 3
# Override wheel scroll lines per physical wheel notch (classic feel). TUI2 only.
scroll_wheel_lines = 3
# Override baseline trackpad sensitivity (lines per tick-equivalent). TUI2 only.
#
# Trackpad-like per-event contribution is:
# - `scroll_trackpad_lines / min(scroll_events_per_tick, 3)`
scroll_trackpad_lines = 1
# Trackpad acceleration (optional). TUI2 only.
# These keep small swipes precise while letting large/faster swipes cover more content.
#
# Concretely, TUI2 computes:
# - `multiplier = clamp(1 + abs(events) / scroll_trackpad_accel_events, 1..scroll_trackpad_accel_max)`
#
# The multiplier is applied to the trackpad-like streams computed line delta (including any
# carried fractional remainder).
scroll_trackpad_accel_events = 30
scroll_trackpad_accel_max = 3
# Force scroll interpretation. TUI2 only.
# Valid values: "auto" (default), "wheel", "trackpad"
scroll_mode = "auto"
# Auto-mode heuristic tuning. TUI2 only.
scroll_wheel_tick_detect_max_ms = 12
scroll_wheel_like_max_duration_ms = 200
# Invert scroll direction for mouse wheel/trackpad. TUI2 only.
scroll_invert = false
```
> [!NOTE]
@@ -892,6 +940,23 @@ animations = false
> [!NOTE] > `tui.notifications` is builtin and limited to the TUI session. For programmatic or crossenvironment notifications—or to integrate with OSspecific notifiers—use the toplevel `notify` option to run an external program that receives event JSON. The two settings are independent and can be used together.
Scroll settings (`tui.scroll_events_per_tick`, `tui.scroll_wheel_lines`, `tui.scroll_trackpad_lines`, `tui.scroll_trackpad_accel_*`, `tui.scroll_mode`, `tui.scroll_wheel_*`, `tui.scroll_invert`) currently apply to the TUI2 viewport scroll implementation.
> [!NOTE] > `tui.scroll_events_per_tick` has terminal-specific defaults derived from mouse scroll probe logs
> collected on macOS for a small set of terminals:
>
> - Terminal.app: 3
> - Warp: 9
> - WezTerm: 1
> - Alacritty: 3
> - Ghostty: 3 (stopgap; one probe measured ~9)
> - iTerm2: 1
> - VS Code terminal: 1
> - Kitty: 3
>
> We should augment these defaults with data from more terminals and other platforms over time.
> Unknown terminals fall back to 3 and can be overridden via `tui.scroll_events_per_tick`.
## Authentication and authorization
### Forcing a login method
@@ -975,6 +1040,15 @@ Valid values:
| `file_opener` | `vscode` \| `vscode-insiders` \| `windsurf` \| `cursor` \| `none` | URI scheme for clickable citations (default: `vscode`). |
| `tui` | table | TUIspecific options. |
| `tui.notifications` | boolean \| array<string> | Enable desktop notifications in the tui (default: true). |
| `tui.scroll_events_per_tick` | number | Raw events per wheel notch (normalization input; default: terminal-specific; fallback: 3). |
| `tui.scroll_wheel_lines` | number | Lines per physical wheel notch in wheel-like mode (default: 3). |
| `tui.scroll_trackpad_lines` | number | Baseline trackpad sensitivity in trackpad-like mode (default: 1). |
| `tui.scroll_trackpad_accel_events` | number | Trackpad acceleration: events per +1x speed in TUI2 (default: 30). |
| `tui.scroll_trackpad_accel_max` | number | Trackpad acceleration: max multiplier in TUI2 (default: 3). |
| `tui.scroll_mode` | `auto` \| `wheel` \| `trackpad` | How to interpret scroll input in TUI2 (default: `auto`). |
| `tui.scroll_wheel_tick_detect_max_ms` | number | Auto-mode threshold (ms) for promoting a stream to wheel-like behavior (default: 12). |
| `tui.scroll_wheel_like_max_duration_ms` | number | Auto-mode fallback duration (ms) used for 1-event-per-tick terminals (default: 200). |
| `tui.scroll_invert` | boolean | Invert mouse scroll direction in TUI2 (default: false). |
| `hide_agent_reasoning` | boolean | Hide model reasoning events. |
| `check_for_update_on_startup` | boolean | Check for Codex updates on startup (default: true). Set to `false` only if updates are centrally managed. |
| `show_raw_agent_reasoning` | boolean | Show raw reasoning (when available). |