fix(dash): only act on key Press events in overlay dispatch - #89
Conversation
The dash event loop dispatched each operational overlay (serve wizard, engine manager, onboarding, …) straight to its `on_key` via `if state.<overlay>.is_some()` arms that ran BEFORE the general `handle_key` — the only place filtering non-Press key events. Terminals that also emit Release/Repeat events (Windows Terminal / ConPTY under WSL, kitty keyboard protocol) thus delivered each keystroke to overlays twice. For the serve wizard's model picker this meant Enter chose+closed the picker on Press, then the Release/Repeat echo re-opened it seeded with the just-chosen model as a filter — so the model could never be selected. This hit both the bare-`rocm` launcher (focused Serve) and the full dash, since both share this loop. Extract `is_actionable_key(KeyEventKind)` (Press only), add a top-priority arm that swallows non-Press key events above every key arm so the Press-only invariant covers overlays too, and route `handle_key` through the same predicate. Regression test: `only_press_key_events_are_actionable`. Signed-off-by: Michael Roy <michael.roy@amd.com>
rominf
left a comment
There was a problem hiding this comment.
Reviewed the change and verified the behavior manually — the model picker now selects on Enter in both the launcher serve flow and the dash serve wizard. LGTM.
The diagnosis is right: the overlay dispatch arms in event_loop call their on_key directly and never filtered KeyEventKind, while the general handle_key already dropped non-Press — so overlays double-fired on terminals that emit a second event per keystroke. A single Press-only swallow arm above all key arms closes the gap, and routing handle_key through the same is_actionable_key predicate is a clean DRY-up. Nice test coverage with only_press_key_events_are_actionable.
I checked the obvious worry — that swallowing Repeat would break held-key auto-repeat (scrolling a job console, holding Backspace in a field). It doesn't: the app never pushes PushKeyboardEnhancementFlags, so the unix backend never emits Repeat; OS auto-repeat arrives as repeated Press and stays actionable; and the pre-existing handle_key already dropped non-Press, so overlays are just being brought in line with the rest of the UI.
A few optional, non-blocking nits:
app/mod.rs:1671—let _ = k;is redundant.kis already used by the guard, so an empty arm body (=> {}) compiles cleanly without an unused-binding warning.app/mod.rs:1670— the Press-only invariant for overlays now relies on this arm staying first among the key arms; a future key arm inserted above it would silently re-introduce the double-fire. Not a problem today, just worth a note near the arm (which the comment already partly does).ui/launcher.rs:279still hardcodesk.kind != KeyEventKind::Pressrather than the newis_actionable_key. Fine as-is (separate module/loop), just means the "shared gate" is shared only withinmod.rs.
None of these block. Approving.
Reconcile PR #97 (configured-endpoint served-model discovery) with main's PR #100 startup local-first detection (EAI-7347). Conflicts in crates/rocm-dash-tui/src/app/{chat.rs,mod.rs} resolved as a union that keeps both behaviors: - Preserve main's local-engine detection: should_detect_local_chat, StartupChatOutcome/startup_chat_outcome, detect_local_chat_with_probe, and the #89 Press-only overlay key gate (is_actionable_key). - Port PR #97's /v1/models discovery for a *configured* chat endpoint into a new helper, chat::discover_configured_chat_model, wired only on the StartupChatOutcome::Configured path. Startup matrix (verified by unit tests in app::chat::tests): - Configured URL + no explicit model + reachable -> adopt served /v1/models id. - Configured URL + explicit model -> config precedence, never overridden. - Configured URL + unreachable -> never probed, never replaced (no fetch timeout; an unreachable explicit CLI/env URL is left untouched). - Local detected / OAuth paths unchanged from main. RED before the port: configured_endpoint_adopts_served_model_when_no_model_set failed against the merged-main stub (got "local-model", expected served id); GREEN after wiring the helper. Full workspace tests, clippy -D warnings, and scripts/smoke_local.py all pass. Signed-off-by: Michael Roy <michael.roy@amd.com>
Problem
In both the bare-`rocm` launcher (focused Serve a model) and the full dash, you could not select a model in the serve wizard's recipe picker. Pressing Enter on a highlighted model re-opened the picker with that model pre-filled as the filter query instead of choosing it — so the model was never actually selected and the serve could not proceed.
Root cause
The dash event loop (
crates/rocm-dash-tui/src/app/mod.rs) dispatches each operational overlay (serve wizard, engine manager, onboarding, …) directly to itson_keyviaif state.<overlay>.is_some()match arms. Those arms run before the generalhandle_key, which was the only place filtering out non-Presskey events.Terminals that also emit
Release/Repeatkey events (Windows Terminal / ConPTY under WSL, and any terminal with the kitty keyboard protocol) therefore delivered each keystroke to overlays twice:The model picker itself was correct; the bug was purely in the event-loop wiring, which is why it hit both front ends (they share the loop).
Fix
is_actionable_key(KeyEventKind) -> bool(Press only).handle_keythrough the same predicate (DRY with the existingrelease_events_are_ignoredbehavior).The mouse path was audited and is immune to the same "one action → multiple events" class:
resolve_mouseonly acts onDown(Left)and the four scroll kinds. The only two terminal event-reading sites are this loop and the launcher loop (which already filtered to Press) — both are now covered.Test plan
only_press_key_events_are_actionable(new) andrelease_events_are_ignoredpasscargo test -p rocm-dash-tuiapp-module suite (156 tests) greencargo clippy -p rocm-dash-tui --all-targets -- -D warningsclean