[FEATURE] Mods: tell a Client when it gains and loses key focus (and show it), so Escape and click-away can be handled

Status Open
Reported on v2.1.300
Maintainer reply None cached
Activity 0 comments · opened Oct 15, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

I built a small selector as a mod Client in the AbovePrompt band: [ Effort < high > ]. A click gives it the keys, ←/→ move the choice and Enter confirms it with $.command.run({ command: 'effort', args }). It works well, but neither the module nor the plugin can know when the control has the keyboard, so it can't show it:

  • Focus is gained on a pointer down by the host (when the Client accepts keys). The module only learns about it if it also registers surface.onPointer.
  • Focus is lost on Escape, Ctrl+C, Ctrl+D or a click elsewhere. The host clears its own focus id and marks the key handled. Nothing reaches surface.onKey, the hooks module or the prompt (the types say it too: "Escape never arrives: it returns the focus").
  • The engine draws no focus mark on a focused Client, unlike a focused Button, which it inverts. Related: #101804, where the hardware cursor stays in the prompt while a Client has the keys.

So the control takes the keyboard silently, and after Escape it still looks active until a timeout I had to add. Users find that confusing: they can't tell whether their next keys go to the prompt or to the control.

Proposed Solution

Any one of these would do, in order of preference:

  1. surface.onFocus(fn) / surface.onBlur(fn) (or a read-only surface.isFocused plus a redraw when it changes), fired by the host on every gain and loss: pointer, Escape, Ctrl+C/Ctrl+D, click-away and unmount.
  2. A visible focus mark drawn by the engine on the focused Client, the way a focused Button is inverted, and optionally a prop so the module can style its own mark.
  3. Deliver Escape to surface.onKey (for example as { key: 'escape' }) before the host returns the focus, so the module can clean up.

A way to give the focus back to the prompt from a module would also help. Today the only way I found is to redraw the Client under a new key, so the focused instance unmounts.

Alternative Solutions

What I do today, on 2.1.300:

  • Treat the control as engaged from the pointer down or the first key, and end that state on Enter, on prompt.change (via a post to the hooks module), or after 5 s without keys. After Escape it keeps looking engaged until that timeout.
  • To drop the focus after Enter, redraw the Client under a new key (effort-<n>), so the focused instance unmounts and the prompt gets the keys again. It works, but it is a workaround.
  • The built-in Select handles focus and keys, but it opens a vertical list. I need an inline left/right selector with no menu.

Priority

Medium - Would be very helpful

Feature Category

Interactive mode (TUI)

Use Case Example

A one-line inline selector in the band above the prompt: click it, choose with the arrows, confirm with Enter, leave with Escape. The same applies to any keyboard-driven Client (a small editor, a picker, a game) that needs to show when it owns the keys and to undo its "active" look when the person leaves it.

Additional Context

Claude Code 2.1.300, Windows 11, Windows Terminal with herdr 0.9.4.

Related observation, not part of this request: registering surface.onPointer makes the host retain fine pointer reporting (DECSET 1016) for the whole terminal while the Client is drawn. herdr before 0.9.4 answered DECRQM 1016 as supported but kept sending cell coordinates (herdrdev/herdr#3675, fixed in herdr 0.9.4). So the whole UI lost the mouse while such a Client was on screen. #100654 may be the same family. It might be worth retaining fine pointer only when a module actually reads event.fine.

View original on GitHub ↗