发布

  • fix(core): prevent TUI hint popup from permanently stealing focus (#36256)

    frostbyte_neo 发布于 2026-07-07 20:40:42 +00:00

    Current Behavior

    The fullscreen TUI can wedge into a state where q (quit), task-list
    navigation, and all mouse clicks/selection are unresponsive, while ?
    (help), F10, and F11 still work. Switching to inline mode and back with
    F11 restores normal behavior.

    Deterministic reproduction: focus an output pane, press any unhandled
    key (e.g. x) to trigger the "press 'i' to enter interactive mode"
    toast, then press F10 within 2 seconds. When the mouse-capture toast
    fades, the app is wedged.

    Root cause: focus was tracked in a one-slot register (focus +
    previous_focus), and update_focus recorded the current focus as
    "previous" even for no-op transitions. F10 dispatches its own ShowHint
    while the first hint is still focused, so previous_focus became
    Focus::HintPopup itself. Every focus-restore path (auto-dismiss, Esc,
    click-away) then self-looped, parking focus permanently on an invisible
    modal: the hint key branch consumed every key except those handled
    earlier in dispatch (Ctrl+C, F10-F12, ?), and active_modal_kind()
    absorbed every mouse event. Only F11 escaped, because switching modes
    constructs a fresh App.

    Expected Behavior

    Focus is tracked as a layer stack and popups can never wedge the UI:

    • focus_stack[0] is always a base layer (task list or output pane)
      that lateral navigation (Tab/Esc/click) replaces in place; popup layers
      (help, run report, hints) push above it
    • push_focus is a no-op for the already-focused layer and moves a
      buried layer instead of duplicating it — the stack can never hold the
      same layer twice, so dismissal can never self-loop (the original bug is
      structurally impossible)
    • close_popup removes a layer wherever it sits (popups can die while
      buried, e.g. a hint auto-expiring under the run report) and prunes
      revealed layers that are no longer active, so focus always lands on
      something visible
    • layer classification and liveness live on Focus itself (is_popup,
      is_active)
    • defense in depth retained: a hidden hint popup is never treated as a
      modal for keys or mouse, and a key arriving while focus points at one
      repairs the focus and falls through to its normal handler

    Regression tests drive the focus-stack API directly: the poisoning
    sequence via repeated push_focus, buried-popup pruning, hidden-hint
    key fall-through, modal hit-testing, and the hidden-popup geometry
    contract (a hidden popup reports no hit-test areas even before the next
    draw clears them). The full key-event → action-queue → ShowHint
    pipeline is not unit-tested — handle_action requires a real terminal
    backend — so the end-to-end F10-during-toast sequence was validated with
    the manual reproduction above. The full nx crate Rust suite passes.

    Related Issue(s)

    No linked issue — diagnosed from a user report of the TUI becoming
    unresponsive after toggling mouse capture (F10) while a hint toast was
    visible.


    Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>

    下载附件