fix(new-workspace): keep dropdown visible while typing within settled qu

Hold the last search results while the user extends or trims the query,
only hiding them when the query diverges completely. This prevents the
dropdown from flashing empty between debounced keystrokes and removes the
guard that made provider rows unselectable during typing.
This commit is contained in:
Jinjing
2026-07-29 16:51:45 -07:00
parent 6ae19764a9
commit 324ebc9dbd
3 changed files with 59 additions and 25 deletions
@@ -1081,7 +1081,7 @@ export default function SmartWorkspaceNameField({
}
}, [rows])
// Why: source rows lag debouncedQuery, so keep Enter off a stale row.
// Why: live input leads debounced search; freeze highlight until the query catches up.
const valueWithinSourceLimit = isSmartWorkspaceSourceQueryWithinLimit(value)
const debouncedQueryWithinSourceLimit = isSmartWorkspaceSourceQueryWithinLimit(debouncedQuery)
const trimmedValue = valueWithinSourceLimit ? value.trim() : ''
@@ -1151,10 +1151,8 @@ export default function SmartWorkspaceNameField({
const handleSelect = useCallback(
(row: RowEntry) => {
// Why: provider rows can lag the live input; typed-name / create-branch stay safe.
if (isQueryStale && row.kind !== 'use-name' && row.kind !== 'create-branch') {
return
}
// Why: select what is shown — held provider rows stay visible while the
// query is ahead of debounce, so blocking them made click/Enter no-ops.
if (row.kind === 'use-name' || row.kind === 'create-branch') {
// Why: "create new branch" has no ref to base from, so it uses the typed-name path (default base).
onValueChange(row.name)
@@ -1170,14 +1168,7 @@ export default function SmartWorkspaceNameField({
}
setOpen(false)
},
[
isQueryStale,
onBranchSelect,
onGitHubItemSelect,
onGitLabItemSelect,
onLinearIssueSelect,
onValueChange
]
[onBranchSelect, onGitHubItemSelect, onGitLabItemSelect, onLinearIssueSelect, onValueChange]
)
const applyEmojiReplacement = useCallback(
@@ -1609,19 +1600,12 @@ export default function SmartWorkspaceNameField({
}
if (open && rows.length > 0) {
const row = rows.find((entry) => entry.value === resolvedCommandValue)
if (
row &&
!(
isQueryStale &&
row.kind !== 'use-name' &&
row.kind !== 'create-branch'
)
) {
if (row) {
event.preventDefault()
handleSelect(row)
return
}
// No selectable row (stale provider arm); fall through to onPlainEnter.
// No highlighted row; fall through to onPlainEnter so the keypress isn't inert.
}
onPlainEnter?.()
}
@@ -128,6 +128,45 @@ describe('Branch source results', () => {
).toEqual([{ refName: 'origin/feature', localBranchName: 'feature' }])
})
it('keeps the last branch results while the user trims a prefix of the settled query', () => {
expect(
getVisibleBranchResults({
mode: 'branches',
value: 'fe',
selectedRepoId: 'repo-1',
resultRepoId: 'repo-1',
resultQuery: 'feat',
branches: [{ refName: 'origin/feature', localBranchName: 'feature' }]
})
).toEqual([{ refName: 'origin/feature', localBranchName: 'feature' }])
})
it('hides held branch results when the live query diverges from the settled query', () => {
expect(
getVisibleBranchResults({
mode: 'branches',
value: 'bug',
selectedRepoId: 'repo-1',
resultRepoId: 'repo-1',
resultQuery: 'feat',
branches: [{ refName: 'origin/feature', localBranchName: 'feature' }]
})
).toEqual([])
})
it('keeps held branch results across case-only edits of a prefix query', () => {
expect(
getVisibleBranchResults({
mode: 'branches',
value: 'Feat',
selectedRepoId: 'repo-1',
resultRepoId: 'repo-1',
resultQuery: 'feat',
branches: [{ refName: 'origin/feature', localBranchName: 'feature' }]
})
).toEqual([{ refName: 'origin/feature', localBranchName: 'feature' }])
})
it('uses stable cmdk values for typed-text actions', () => {
expect(
buildSmartWorkspaceSourceRows({
@@ -99,9 +99,20 @@ export function getVisibleBranchResults({
if (!selectedRepoId || resultRepoId !== selectedRepoId || resultQuery === null) {
return []
}
// Why: hold the last settled branch list while the user types ahead — blanking
// on query mismatch flashed the dropdown empty between keystrokes.
if (value.trim() === '' && resultQuery !== '') {
const currentQuery = value.trim()
// Why: hold the last settled list while the user extends/trims the query so the
// dropdown does not blank between debounced keystrokes. Drop the hold when the
// query diverges (e.g. "feat" → "bug") so unrelated rows do not linger.
if (currentQuery === '') {
return resultQuery === '' ? branches : []
}
const currentQueryKey = currentQuery.toLowerCase()
const resultQueryKey = resultQuery.toLowerCase()
if (
resultQueryKey !== currentQueryKey &&
!currentQueryKey.startsWith(resultQueryKey) &&
!resultQueryKey.startsWith(currentQueryKey)
) {
return []
}
return branches