TUI composer has no mouse text selection (drag-select then Backspace/type-over)

Open 💬 1 comment Opened Aug 27, 2026 by alejandrocht
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

Summary

The TUI composer has no in-app mouse text selection: you can't drag to select a range of the prompt you're typing and then Backspace/type over it. Mouse events are currently discarded entirely (event_stream.rs explicitly skips Event::Mouse), so this isn't a partial/broken feature — it's just not there yet.

Related but distinct: #12882 asks for the same underlying gap via keyboard (Shift+Arrow); this issue is about mouse drag-select specifically. Fixing one doesn't fix the other.

Why this was probably never attempted the naive way

I found the history: mouse capture was tried before and reverted because it broke the terminal's own native text selection (EnableMouseCapture is all-or-nothing at the protocol level — once on, the terminal stops doing its own click-drag selection and hands raw coordinates to the app instead). That's the root cause behind #1247, #2140, #6327, #8306 — capturing mouse for one feature silently took away native copy/paste everywhere else, and the fix each time was to turn mouse capture back off rather than actually replace what it took away.

Proposed direction (verified working)

The correct fix for this class of problem isn't "capture mouse, hope for the best" — it's "capture mouse, then own selection completely in-app instead of relying on the terminal for it," the same approach Charm's Crush uses (internal/ui/model/ui.go: mouse capture is always on, and MouseClickMsg/MouseMotionMsg/MouseReleaseMsg are hit-tested against the current layout and routed to whichever widget owns that screen region, each with its own selection state and highlight rendering — never falling back to native terminal selection).

I implemented and verified this scoped to the composer only (not the transcript, to keep the diff reviewable):

  • TuiEvent::Mouse forwards left-button down/drag/up only (bare Moved/scroll are filtered so idle mouse movement never wakes the render loop; scroll wheel keeps working natively via the existing EnableAlternateScroll, unaffected).
  • TextArea gets a selection_anchor: Option<usize> and a grapheme-safe, unicode-correct inverse of the existing wrapping::cursor_position (wrapping::position_for_row_col) to hit-test a click's screen (row, col) back to a byte offset — reusing the same wrap/grapheme math the renderer already uses, so it never splits a multi-byte or double-width grapheme.
  • Backspace/Delete/typing a char while a selection is active replaces just that range via the existing TextArea::replace_range (this primitive already existed — no new deletion logic needed).
  • The selection highlights using the same reversed-video style the existing search-highlight path already uses (render_ref_styled_with_highlights already accepted extra (Range, Style) overlays — no new rendering plumbing needed either).
  • Scoped to the plain composer only: while a modal/popup view is on the BottomPane view stack, mouse events are ignored for now rather than growing BottomPaneView with a new method.
  • Vim mode is deliberately left out of the auto-replace-on-keypress behavior (selection can still be made there, it just doesn't consume itself on the next keypress) since that would conflict with Normal-mode command semantics — noted as a scoped-out follow-up, not silently guessed at.

Testing

  • cargo check -p codex-tui: clean, zero warnings.
  • Full cargo test -p codex-tui (3979 tests, RUST_MIN_STACK=64M — this machine's default thread stack overflows on a couple of unrelated pre-existing tests regardless of this change): 3977 passed, 2 failed. Both failures are pre-existing locale-dependent number-formatting snapshot mismatches (status_snapshot_includes_enterprise_monthly_credit_limit, rolling_rate_limit_snapshot_preserves_prior_individual_limit) — confirmed identical on a clean main checkout with none of this change applied, unrelated to mouse/composer.
  • Added 6 new tests: 3 for the wrap-position inverse function (round-trip consistency, wide-grapheme safety, out-of-bounds clamping) and 3 for the composer selection behavior (drag+Backspace, drag+type-replace, plain-click leaves no selection).

Reference implementation

I know external PRs aren't accepted per docs/contributing.md, so I'm not opening one — this is filed as an issue with the analysis and a working, tested branch for reference in case it's useful: https://github.com/alejandrocht/codex/tree/fix/tui-mouse-text-selection (diffs codex-rs/tui/src/{app.rs, tui.rs, tui/event_stream.rs, tui/screen_size.rs, bottom_pane/mod.rs, bottom_pane/chat_composer.rs, bottom_pane/textarea.rs, bottom_pane/textarea/wrapping.rs, chatwidget/interaction.rs} plus the ~10 other screens that needed a no-op arm added for the new TuiEvent::Mouse variant to stay exhaustive).

Happy to share more detail on any part of this if it's useful.

View original on GitHub ↗

1 Comment

etraut-openai contributor · 3 hours ago

You’re right that selection inside the composer is different from the terminal’s native text selection.

Currently, mouse selection is handled by your terminal emulator. That selects displayed text for copying; it doesn’t create a selection in Codex’s input buffer that Backspace or typing can replace.

A TUI can implement that behavior by enabling mouse reporting and handling selection itself. The complication, as you noted, is that conventional mouse reporting applies to the terminal as a whole, not just the composer. Handling events only inside the composer doesn’t restore native selection elsewhere — ignoring an event doesn’t hand it back to the terminal.

Alternate-screen mode is a separate feature: it provides a temporary screen buffer while preserving the normal screen underneath. It doesn’t enable mouse handling or clipboard integration. Its main trade-off is that output generally doesn’t accumulate in the terminal’s normal scrollback, so the app needs to provide its own history navigation.

Composer drag-selection is therefore feasible, but a complete solution needs to preserve a usable way to select and copy transcript text and scroll through history. Scoping the implementation to the composer keeps the code change smaller, but doesn’t necessarily keep its effects confined to the composer. Those broader interactions would need verification before enabling mouse capture.