fix(tui): preserve selection under a stationary pointer (#47138)

This commit is contained in:
Kit Langton
2026-09-03 22:04:29 -04:00
committed by GitHub
Unverified
parent b3733e9517
commit cb852434b1
9 changed files with 13 additions and 51 deletions
@@ -87,7 +87,6 @@ export function Autocomplete(props: {
index: 0,
selected: 0,
visible: false as AutocompleteRef["visible"],
input: "keyboard" as "keyboard" | "mouse",
})
const [positionTick, setPositionTick] = createSignal(0)
@@ -151,14 +150,6 @@ export function Autocomplete(props: {
setSearch(next ? next : "")
})
// When the filter changes due to how TUI works, the mousemove might still be triggered
// via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard so
// that the mouseover event doesn't trigger when filtering.
createEffect(() => {
filter()
setStore("input", "keyboard")
})
function insertPart(
text: string,
part:
@@ -724,7 +715,6 @@ export function Autocomplete(props: {
title: "Previous autocomplete item",
group: "Autocomplete",
run() {
setStore("input", "keyboard")
move(-1)
},
},
@@ -733,7 +723,6 @@ export function Autocomplete(props: {
title: "Next autocomplete item",
group: "Autocomplete",
run() {
setStore("input", "keyboard")
move(1)
},
},
@@ -942,17 +931,8 @@ export function Autocomplete(props: {
: undefined
}
flexDirection="row"
onMouseMove={() => {
setStore("input", "mouse")
}}
onMouseOver={() => {
if (store.input !== "mouse") return
moveTo(index)
}}
onMouseDown={() => {
setStore("input", "mouse")
moveTo(index)
}}
onMouseMove={() => moveTo(index)}
onMouseDown={() => moveTo(index)}
onMouseUp={() => select()}
>
<text
+1 -1
View File
@@ -438,7 +438,7 @@ export function RunFormBody(props: {
flexDirection="row"
gap={1}
alignItems="flex-start"
onMouseOver={() => setState((previous) => formSetSelected(previous, index()))}
onMouseMove={() => setState((previous) => formSetSelected(previous, index()))}
backgroundColor={active() ? props.theme.formfieldFocusedBg : "transparent"}
onMouseUp={() => choose(index())}
>
+1 -1
View File
@@ -54,7 +54,7 @@ function buttons(
paddingLeft={1}
paddingRight={1}
backgroundColor={option === selected ? theme.actionFocusedBg : transparent}
onMouseOver={() => {
onMouseMove={() => {
if (!disabled) onHover(option)
}}
onMouseUp={() => {
@@ -125,7 +125,7 @@ export function ShellTab(props: { sessionID: string }) {
backgroundColor={
active() ? theme.background.action.primary.focused : theme.background.action.primary.default
}
onMouseOver={() => setStore("selected", index())}
onMouseMove={() => setStore("selected", index())}
onMouseUp={() => {
setStore("selected", index())
open()
@@ -215,7 +215,7 @@ export function SubagentsTab(props: { sessionID: string }) {
? theme.background.action.primary.selected
: theme.background.action.primary.default
}
onMouseOver={() => setStore("selected", index())}
onMouseMove={() => setStore("selected", index())}
onMouseUp={() => {
setStore("selected", index())
navigate({ type: "session", sessionID: entry.sessionID })
@@ -84,7 +84,7 @@ export function TerminalsTab(props: { sessionID: string; visibleTerminalID?: str
? theme.background.action.primary.selected
: theme.background.action.primary.default
}
onMouseOver={() => setSelected(index())}
onMouseMove={() => setSelected(index())}
onMouseUp={() => {
setSelected(index())
select()
+2 -2
View File
@@ -907,7 +907,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
}
return (
<box
onMouseOver={() => setStore("selected", i())}
onMouseMove={() => setStore("selected", i())}
onMouseDown={() => setStore("selected", i())}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
@@ -961,7 +961,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
</For>
<Show when={custom()}>
<box
onMouseOver={() => setStore("selected", rows().length)}
onMouseMove={() => setStore("selected", rows().length)}
onMouseDown={() => setStore("selected", rows().length)}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
@@ -591,7 +591,7 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
? theme.background.action.primary.focused
: theme.background.action.primary.default
}
onMouseOver={() => setStore("selected", option)}
onMouseMove={() => setStore("selected", option)}
onMouseUp={() => {
setStore("selected", option)
props.onSelect(option)
+3 -21
View File
@@ -112,7 +112,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
const [store, setStore] = createStore({
selected: 0,
filter: "",
input: "keyboard" as "keyboard" | "mouse",
})
const [focusedAction, setFocusedAction] = createSignal<number>()
const actionFocused = createMemo(() => focusedAction() !== undefined)
@@ -201,12 +200,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
return result
})
// When the filter changes due to how TUI works, the mousemove might still be triggered
// via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard
// that the mouseover event doesn't trigger when filtering.
createEffect(() => {
filtered()
setStore("input", "keyboard")
setFocusedAction(undefined)
})
@@ -384,7 +379,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
function submit() {
if (props.locked) return
setStore("input", "keyboard")
const index = focusedAction()
if (index !== undefined) {
trigger(actionItems()[index])
@@ -418,7 +412,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
title: "Previous item",
group: "Dialog",
run() {
setStore("input", "keyboard")
move(-1)
},
},
@@ -427,7 +420,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
title: "Next item",
group: "Dialog",
run() {
setStore("input", "keyboard")
move(1)
},
},
@@ -436,7 +428,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
title: "Page up",
group: "Dialog",
run() {
setStore("input", "keyboard")
move(-10)
},
},
@@ -445,7 +436,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
title: "Page down",
group: "Dialog",
run() {
setStore("input", "keyboard")
move(10)
},
},
@@ -455,7 +445,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
group: "Dialog",
run() {
if (props.locked) return
setStore("input", "keyboard")
moveTo(0)
},
},
@@ -465,7 +454,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
group: "Dialog",
run() {
if (props.locked) return
setStore("input", "keyboard")
moveTo(flat().length - 1)
},
},
@@ -538,7 +526,6 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
function trigger(item: Action | undefined) {
if (props.locked || !item || isActionDisabled(item)) return
setStore("input", "keyboard")
if (item.selection === "none") {
item.onTrigger()
return
@@ -709,21 +696,16 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
position="relative"
onMouseMove={() => {
if (props.locked) return
setStore("input", "mouse")
setFocusedAction(undefined)
const index = flat().findIndex((x) => isDeepEqual(x.value, option.value))
if (index === -1 || index === store.selected) return
moveTo(index)
}}
onMouseUp={() => {
if (props.locked) return
option.onSelect?.(dialog)
props.onSelect?.(option)
}}
onMouseOver={() => {
if (props.locked) return
if (store.input !== "mouse") return
const index = flat().findIndex((x) => isDeepEqual(x.value, option.value))
if (index === -1) return
moveTo(index)
}}
onMouseDown={() => {
if (props.locked) return
const index = flat().findIndex((x) => isDeepEqual(x.value, option.value))