From 5e534f165de14fbd8532152877da9b0512f89ffa Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 30 Jul 2026 20:15:25 -0400 Subject: [PATCH] fix(tui): preserve current selection across list updates --- packages/tui/src/ui/dialog-select.tsx | 4 ++-- .../tui/test/cli/tui/dialog-select.test.tsx | 20 ++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/tui/src/ui/dialog-select.tsx b/packages/tui/src/ui/dialog-select.tsx index fb6a33363904..e3733fdece00 100644 --- a/packages/tui/src/ui/dialog-select.tsx +++ b/packages/tui/src/ui/dialog-select.tsx @@ -240,7 +240,7 @@ export function DialogSelect(props: DialogSelectProps) { on( () => props.options, () => { - if (!props.preserveSelection) { + if (!props.preserveSelection && props.current === undefined) { const count = flat().length if (count === 0) return const next = reconcileSelection(store.selected, count) @@ -276,7 +276,7 @@ export function DialogSelect(props: DialogSelectProps) { setStore("selected", index) selection = option if (!moved) return - if (!props.preserveSelection || store.filter.length > 0) return + if ((!props.preserveSelection && props.current === undefined) || store.filter.length > 0) return scrollAfterLayout(false, option.value) return } diff --git a/packages/tui/test/cli/tui/dialog-select.test.tsx b/packages/tui/test/cli/tui/dialog-select.test.tsx index ebf7720bef61..228119875b42 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[], current?: string) { const state = path.join(root, "state") await mkdir(state, { recursive: true }) const config = createTuiResolvedConfig() @@ -114,6 +114,7 @@ async function mountSelect(root: string, initial: DialogSelectOption[]) moved.push(option.value)} onSelect={(option) => selected.push(option.value)} /> @@ -309,3 +310,20 @@ test("keeps the cursor index while options are temporarily empty", async () => { select.app.renderer.destroy() } }) + +test("keeps the current option selected when options reorder", async () => { + await using tmp = await tmpdir() + const options = ["first", "current", "third"].map((value) => ({ title: value, value })) + const select = await mountSelect(tmp.path, options, "current") + + try { + select.replaceOptions([options[1], options[2], options[0]]) + await select.app.waitForFrame((frame) => frame.indexOf("current") < frame.indexOf("third")) + select.app.mockInput.pressEnter() + await select.app.waitFor(() => select.selected.length === 1) + + expect(select.selected).toEqual(["current"]) + } finally { + select.app.renderer.destroy() + } +})