From 870a10a7baf7e540789148719acbeb1bb6dcb4de Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sat, 18 Jul 2026 19:54:41 +0000 Subject: [PATCH] fix(tui): stabilize dialog mouse selection --- packages/tui/src/ui/dialog-select.tsx | 2 +- .../tui/test/cli/tui/dialog-select.test.tsx | 36 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/tui/src/ui/dialog-select.tsx b/packages/tui/src/ui/dialog-select.tsx index b972b651fefd..19c4bbea505b 100644 --- a/packages/tui/src/ui/dialog-select.tsx +++ b/packages/tui/src/ui/dialog-select.tsx @@ -283,7 +283,7 @@ export function DialogSelect(props: DialogSelectProps) { setTimeout(() => { if (filter.length > 0) { moveTo(0, true, false) - } else if (current && props.focusCurrent !== false) { + } else if (current && props.focusCurrent !== false && store.input !== "mouse") { const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current)) if (currentIndex >= 0) { moveTo(currentIndex, true) diff --git a/packages/tui/test/cli/tui/dialog-select.test.tsx b/packages/tui/test/cli/tui/dialog-select.test.tsx index 5e6a28e9bebf..4edc3c1fa5a3 100644 --- a/packages/tui/test/cli/tui/dialog-select.test.tsx +++ b/packages/tui/test/cli/tui/dialog-select.test.tsx @@ -79,7 +79,7 @@ async function renderSelect( return app } -async function mountSelect(root: string, initial: DialogSelectOption[]) { +async function mountSelect(root: string, initial: DialogSelectOption[], trackCurrent = false) { const state = path.join(root, "state") await mkdir(state, { recursive: true }) const config = createTuiResolvedConfig() @@ -105,6 +105,7 @@ async function mountSelect(root: string, initial: DialogSelectOption[]) function Harness() { const [options, setOptions] = createSignal(initial) + const [current, setCurrent] = createSignal(initial[0]?.value) replaceOptions = setOptions function Fixture() { @@ -114,7 +115,11 @@ async function mountSelect(root: string, initial: DialogSelectOption[]) moved.push(option.value)} + current={trackCurrent ? current() : undefined} + onMove={(option) => { + moved.push(option.value) + if (trackCurrent) setCurrent(option.value) + }} onSelect={(option) => selected.push(option.value)} /> )), @@ -272,3 +277,30 @@ test("selects a repopulated option after removing the only option", async () => select.app.renderer.destroy() } }) + +test("hover stays on the row when current follows selection", async () => { + await using tmp = await tmpdir() + const select = await mountSelect( + tmp.path, + ["first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth"].map((value) => ({ + title: value, + value, + })), + true, + ) + + try { + await select.app.mockMouse.moveTo(20, 11) + await select.app.flush() + await select.app.mockMouse.moveTo(20, 12) + await select.app.waitFor(() => select.moved.includes("second")) + select.moved.length = 0 + await select.app.mockMouse.moveTo(20, 14) + await select.app.waitFor(() => select.moved.length > 0) + await select.app.waitForVisualIdle() + + expect(select.moved).toEqual(["fourth"]) + } finally { + select.app.renderer.destroy() + } +})