Re-pin entry to deferred tab results that rank higher

Track whether selection auto-follows the top-ranked result or was
manually positioned. Re-pin entry to tabs when they rank higher,
but preserve manual selection.
This commit is contained in:
Jinjing
2026-08-08 12:43:45 -07:00
parent 56cd69a1e2
commit 7acad444ed
2 changed files with 35 additions and 1 deletions
@@ -233,7 +233,28 @@ describe('TabBarCreateEntry tab results', () => {
expect(rows[3]).toContain('Open file')
})
it('keeps Enter on the row the user saw when a tab row arrives a render later', () => {
it('re-pins Enter to a deferred tab row that ranks above the auto-selected file', () => {
entryOptionsMock.options = [newFileOption]
tabSearchMock.resultsByQuery['add tab'] = [terminalResult()]
tabSearchMock.hold = ''
const onOpenEntry = vi.fn().mockResolvedValue(undefined)
const onDidOpenEntry = vi.fn()
renderEntry({ onDidOpenEntry, onOpenEntry })
setQuery('add tab')
expect(rowTexts()).toHaveLength(1)
tabSearchMock.hold = null
renderEntry({ onDidOpenEntry, onOpenEntry })
expect(rowTexts()[0]).toContain('Switch to tab')
submitForm()
expect(activationMocks.workspace).toHaveBeenCalledTimes(1)
expect(onDidOpenEntry).toHaveBeenCalledTimes(1)
expect(onOpenEntry).not.toHaveBeenCalled()
})
it('keeps a manually chosen row when a tab row arrives a render later', () => {
entryOptionsMock.options = [newFileOption]
tabSearchMock.resultsByQuery['add tab'] = [terminalResult()]
tabSearchMock.hold = ''
@@ -242,6 +263,8 @@ describe('TabBarCreateEntry tab results', () => {
setQuery('add tab')
expect(rowTexts()).toHaveLength(1)
// Arrow on the only row marks selection as user-owned, not auto-default.
pressKey(queryInput(), 'ArrowDown')
tabSearchMock.hold = null
renderEntry({ onOpenEntry })
@@ -84,6 +84,9 @@ export default function TabBarCreateEntry({
const [switchError, setSwitchError] = useState<string | null>(null)
const [selectedOptionId, setSelectedOptionId] = useState<string | null>(null)
const [selectedOptionQuery, setSelectedOptionQuery] = useState(query)
// True while selection still tracks ranking (no arrow/click yet). Deferred tab
// rows can prepend a better match; re-pin only while this stays true.
const [selectionIsAutoDefault, setSelectionIsAutoDefault] = useState(true)
const [lastMenuOpen, setLastMenuOpen] = useState(menuOpen)
const inputRef = useRef<HTMLInputElement>(null)
const imeEnter = useImeEnterGestureOwnership()
@@ -171,6 +174,7 @@ export default function TabBarCreateEntry({
setError(null)
setSwitchError(null)
setSelectedOptionId(null)
setSelectionIsAutoDefault(true)
}
}
@@ -198,10 +202,16 @@ export default function TabBarCreateEntry({
if (selectedOptionQuery !== query) {
setSelectedOptionQuery(query)
setSelectedOptionId(topOptionId)
setSelectionIsAutoDefault(true)
} else if (selectedOptionId === null && topOptionId !== null) {
// Why pin the top row by id: the tab search defers the query, so tab rows
// arrive a render later and would otherwise slide under an index-kept highlight.
setSelectedOptionId(topOptionId)
setSelectionIsAutoDefault(true)
} else if (selectionIsAutoDefault && topOptionId !== null && selectedOptionId !== topOptionId) {
// Files paint before deferred tab search; follow the new top while the user
// has not moved selection themselves.
setSelectedOptionId(topOptionId)
}
const selectedOptionIndex = selectedOptionId
? activeOptions.findIndex((option) => getActiveOptionId(option) === selectedOptionId)
@@ -294,6 +304,7 @@ export default function TabBarCreateEntry({
const delta = event.key === 'ArrowDown' ? 1 : -1
const nextIndex =
(activeSelectedIndex + delta + activeOptions.length) % activeOptions.length
setSelectionIsAutoDefault(false)
setSelectedOptionId(getActiveOptionId(activeOptions[nextIndex]))
return
}