Part of #557. Verified against origin/main (v0.29.0) on 2026-07-28.
Today
handle_mouse (crates/cli/src/ui/modern/run.rs:3225-3281) handles exactly four things:
ScrollUp / ScrollDown
Down/Drag/Up(Left) — line-granular transcript selection (TextSelection { start_line, end_line }, app.rs:577)
Down(Middle) — emits a toast telling you to use terminal paste
MouseEventKind::Moved has zero handlers, so hover does not exist. git grep hit_test|hover|-> Vec<Rect> over crates/cli/src/ui/ finds nothing — no UI element anywhere exposes a clickable region. Every widget draws into a Rect and forgets it.
For comparison, grok-build carries app/mouse.rs (1707 LOC) + input/mouse.rs (1451 LOC) with roughly 28-30 named hit rects.
The missing primitive
A hit-rect registry: a per-frame map of (Rect, Target) that each renderer registers into, with hover and click resolved centrally against it. Their render_menu(...) -> Vec<Rect> — documented as "Returns the Rect for each item row (for hit-testing clicks and hover)" — is that pattern in miniature.
Without it, each clickable element invents its own hit-testing. That is the duplicate-source-of-truth shape that produced most of the bugs found in the 2026-07-28 audit (conversation_len mirror, resume_loading bool, the overlay list in open_session_picker), and it is worth avoiding deliberately rather than by accident.
Register rows this unlocks
14 open rows depend on it:
| Row |
Capability |
Pri |
| D5-22 |
Mouse hover states over named hit rects |
P2 |
| D5-21 |
Scrollbar click and drag |
P2 |
| D5-34 |
1/2/3-click = caret / word / cell-or-line |
P2 |
| D2-21 |
Mouse caret placement, double/triple-click in composer |
P2 |
| D6-23 |
Status-bar items carry hit rects |
P2 |
| D4-18 |
Todo badge in the status bar, clickable |
P2 |
| D4-24 |
Persistent idle cue, clickable |
P2 |
| D5-19 |
Timeline rail with hover preview and click-to-jump |
P3 |
| D6-15 |
cwd in the status bar (clickable) |
P3 |
| D6-20 |
Context bar hover-swaps to a percentage at identical width |
P3 |
| D10-18 |
OSC 22 cursor shape on link hover |
P3 |
| D5-31 |
Middle-click paste reading X11 PRIMARY (currently a hint only) |
P2 |
| D3-10 |
OSC 8 hyperlinks emitted (LinkSpan is already collected and dropped) |
P1 |
| D10-12 |
OSC 8 gated by a capability matrix |
P1 |
markdown.rs:27 already produces LinkSpan { line, cols, url } and its doc says "Consumed by mouse/OSC-8 handling in a later milestone" — the data is being computed and thrown away at layout.rs:355, layout.rs:414 and render.rs:603.
Prerequisite: ship the escape hatch first
Hover requires capturing motion events, which makes native terminal click-drag copy worse than it is today. D5-30 (relinquish mouse capture — rated TRIVIAL, EnableMouseCapture is unconditional at run.rs:649/:711 and all four call sites exist) should land before this, not after. Capturing more input without an escape hatch is a regression for anyone who selects text with the mouse.
Acceptance
- One registry; no widget does its own hit-testing.
- Hover state is derived from
Moved and does not require a click.
- A user can disable mouse capture entirely (D5-30) and lose only the enhancements.
- Frame snapshots cover hover and selected states.
Part of #557. Verified against
origin/main(v0.29.0) on 2026-07-28.Today
handle_mouse(crates/cli/src/ui/modern/run.rs:3225-3281) handles exactly four things:ScrollUp/ScrollDownDown/Drag/Up(Left)— line-granular transcript selection (TextSelection { start_line, end_line }, app.rs:577)Down(Middle)— emits a toast telling you to use terminal pasteMouseEventKind::Movedhas zero handlers, so hover does not exist.git grep hit_test|hover|-> Vec<Rect>overcrates/cli/src/ui/finds nothing — no UI element anywhere exposes a clickable region. Every widget draws into aRectand forgets it.For comparison, grok-build carries
app/mouse.rs(1707 LOC) +input/mouse.rs(1451 LOC) with roughly 28-30 named hit rects.The missing primitive
A hit-rect registry: a per-frame map of
(Rect, Target)that each renderer registers into, with hover and click resolved centrally against it. Theirrender_menu(...) -> Vec<Rect>— documented as "Returns the Rect for each item row (for hit-testing clicks and hover)" — is that pattern in miniature.Without it, each clickable element invents its own hit-testing. That is the duplicate-source-of-truth shape that produced most of the bugs found in the 2026-07-28 audit (
conversation_lenmirror,resume_loadingbool, the overlay list inopen_session_picker), and it is worth avoiding deliberately rather than by accident.Register rows this unlocks
14 open rows depend on it:
LinkSpanis already collected and dropped)markdown.rs:27already producesLinkSpan { line, cols, url }and its doc says "Consumed by mouse/OSC-8 handling in a later milestone" — the data is being computed and thrown away atlayout.rs:355,layout.rs:414andrender.rs:603.Prerequisite: ship the escape hatch first
Hover requires capturing motion events, which makes native terminal click-drag copy worse than it is today. D5-30 (relinquish mouse capture — rated TRIVIAL,
EnableMouseCaptureis unconditional atrun.rs:649/:711and all four call sites exist) should land before this, not after. Capturing more input without an escape hatch is a regression for anyone who selects text with the mouse.Acceptance
Movedand does not require a click.