Skip to content

feat(tui): Shift+drag text selection while the mouse is on - #278

Merged
plombeer31 merged 4 commits into
mainfrom
feat/shift-drag-selection
Aug 31, 2026
Merged

feat(tui): Shift+drag text selection while the mouse is on#278
plombeer31 merged 4 commits into
mainfrom
feat/shift-drag-selection

Conversation

@plombeer31

@plombeer31 plombeer31 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

While mouse reporting (1002/1006) is on, the terminal suppresses its own drag-to-select. That is a terminal-level constraint — no TUI can read what the terminal never reports, and plain-drag selection under mouse mode is impossible by design. The fix hands reporting back to the terminal for a 10-second window, on two triggers:

Plain drag (terminal-agnostic, the primary path). A modifier turned out to be the wrong sole trigger: some terminals bypass reporting natively on Shift (kitty, WezTerm, Windows Terminal), iTerm2 reserves Option instead, and others swallow modified clicks outright — the app never sees the gesture. So the drag itself is the trigger now: every gesture the app understands starts on a live mouse target, which means a press nobody claims (message text, panel prose, empty rail) followed by held motion is the operator dragging across inert text — selecting. MouseTargetRegistry.dispatch reports whether an event was consumed, a small tracker arms on an unclaimed left press and fires once on the first held motion, and the passthrough suspends reporting for 10s with a notice: drag again to select, then copy the way you normally would. Native selection then works in every terminal, no modifier. Drags starting on live targets (composer text, rows, buttons) are claimed presses and never trigger it.

Hint chip. The idle strip at the bottom of the screen now carries [drag] select text, so the gesture is written down where the other commands live. The chip is clickable: a click opens the pause window directly (a third "chip" trigger with the "drag to select" phrasing), so the very next drag selects — no double-drag needed.

Modified press (instant, where terminals deliver it). A Shift- or Alt-modified press opens the same window immediately — the terminals that report those get selection on the very drag that asked for it.

  • MouseTrackingController gains suspend()/resume()/isSuspended() — the disable/enable sequences without giving up the controller or its crash-restore registration.
  • Decoded events are swallowed while suspended; a consumed trigger press never reaches the hit test.
  • The window auto-resumes; /mouse off during it stays off. /mouse off remains the permanent way to hand selection to the terminal.
  • README/AGENTS lead with the plain-drag path and document the native instant paths (Shift: kitty/WezTerm/GNOME/Windows Terminal; Option: iTerm2); stale pre-1002 mode descriptions corrected.

Tests: controller suspend/resume semantics, dispatch consumed-ness (claimed / dead-content / captured pointer), drag-intent (fires once per unclaimed press; claimed press never; wheel inert; re-arms after release), wiring (drag suspends + notifies + auto-resumes; alt and shift presses open the window; /mouse off mid-window stays off). npm run lint clean.

While mouse reporting (1002/1006) is on, the terminal suppresses its
own drag-to-select — no TUI can lift that. Most terminals bypass
reporting for a Shift-modified drag natively; Apple Terminal has no
bypass and reports the shifted press instead. That report is the
trigger: the finished-but-never-wired selection-passthrough module now
suspends reporting for 10s with a chat notice, so native selection and
copy work uniformly.

- MouseTrackingController grows suspend()/resume()/isSuspended():
  the same escape sequences, but the controller and its restore-net
  registration stay live. Both idempotent, both no-ops after
  disable(), and disable() mid-suspension writes nothing more — the
  terminal is already clean.
- tui-command wires the passthrough between decode and emit: a
  consumed shift-press never reaches the hit test, and reports still
  in flight during the window are swallowed. /mouse off during the
  window stays off; the pending auto-resume finds nothing to resume.
- selection-passthrough exported from the mouse barrel; its header no
  longer claims 1002 is off (it is on since the composer drag).
- AGENTS.md and README mouse sections describe the real modes
  (1002+1006) and the shift+drag path; /mouse off stays documented as
  the permanent form.
The Shift trigger turned out to be terminal-dependent in the wrong
direction: some terminals bypass reporting natively on Shift (kitty,
WezTerm, Windows Terminal), iTerm2 reserves Option instead, and others
swallow modified clicks entirely — the app never sees the press and the
trigger is dead. Real-world result: Shift+drag over chat did nothing.

A modifier can be a shortcut but not the only path. The terminal-
agnostic trigger is the drag itself: every gesture the app understands
starts on a live target, so a press nobody claims followed by held
motion is the operator dragging across inert text — selecting. Detect
it, suspend for the usual 10s, and say to drag again.

- MouseTargetRegistry.dispatch reports a captured event as consumed
  even when the capturer declines it: while captured the event went to
  nobody else, and a composer drag must never read as dead content.
- drag-intent.ts arms on an unclaimed left press and fires once, on
  the first held-motion report; release or the next press resets it,
  wheel reports never participate. Lives behind registry.dispatch in
  the TuiApp subscription; a new optional onSelectionDragIntent
  callback carries the signal to the host.
- selection-passthrough grows beginWindow(trigger): the same
  suspension, timer and auto-resume the modifier path uses, with a
  drag-specific notice ("drag again") because the triggering gesture
  is already lost to reporting. observe() now also fires on an
  alt-modified press — the iTerm2-flavoured analogue of the shifted
  one. The tui-command emit gate keeps swallowing in-flight reports
  mid-window, which also starves the tracker of stale motion.
- README and AGENTS.md lead with the plain-drag path; Shift/Option
  stay documented as the instant paths some terminals provide
  natively, /mouse off as the permanent form.
… the first drag

Text selection is drag-twice — the first drag pauses reporting, the
second selects — and nothing on screen said so. A [drag] select text
chip now sits in the idle strip; clicking it opens the pause window
directly (a new "chip" trigger with the modifier phrasing, since no
drag has been lost yet), so the very next drag selects. Sheds early:
the gesture itself works without ever reading the hint.
…tion

# Conflicts:
#	src/tui/components/hotkey-chips.ts
#	src/tui/components/hotkey-hint.test.tsx
@plombeer31
plombeer31 merged commit 76fb4ee into main Aug 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant