fix(new-workspace): align held provider results with live typing

Cap prefix hold by length delta, hide GitHub/GitLab/Linear rows when the
field is cleared ahead of debounce, and re-sync the cmdk arm when search
settles so the highlight cannot lag the resolved selection.
This commit is contained in:
Jinjing
2026-07-29 17:08:40 -07:00
parent 324ebc9dbd
commit 5ea9380116
3 changed files with 140 additions and 11 deletions
@@ -57,6 +57,7 @@ import {
getBranchSearchRequest,
getSmartWorkspaceEmptyHint,
getVisibleBranchResults,
getVisibleHeldProviderResults,
isSmartWorkspaceSourceQueryWithinLimit,
type SmartNameMode,
type SmartWorkspaceSourceRow
@@ -558,6 +559,11 @@ export default function SmartWorkspaceNameField({
return
}
let stale = false
// Why: empty-query search must not briefly paint the previous non-empty result set
// once debounce catches a cleared field.
if (debouncedQuery.trim() === '') {
setGithubItems([])
}
const directNumber = normalizedGhQuery.directNumber
const directLink = parsedGhLink
if (directLink !== null && handledCrossRepoUrlRef.current !== debouncedQuery.trim()) {
@@ -875,6 +881,10 @@ export default function SmartWorkspaceNameField({
let stale = false
setLinearLoading(true)
const trimmed = debouncedQuery.trim()
// Why: empty-query list must not briefly paint the previous non-empty result set.
if (trimmed === '') {
setLinearIssues([])
}
const request = trimmed
? searchLinearIssues(trimmed, RESULT_LIMIT, { sourceContext: linearSourceContext })
: listLinearIssues(
@@ -992,6 +1002,10 @@ export default function SmartWorkspaceNameField({
setGitlabLoading(true)
// Why: thread the typed query so the GitLab API filters MRs by name/number (shouldQueryGitlab already gates oversized queries).
const trimmedQuery = debouncedQuery.trim() || undefined
// Why: empty-query list must not briefly paint the previous non-empty result set.
if (trimmedQuery === undefined) {
setGitlabItems([])
}
void Promise.all(
repoBackedSearchTargets.map((target) =>
listGitLabMRsForSource({
@@ -1051,11 +1065,23 @@ export default function SmartWorkspaceNameField({
selectedRepoId: selectedRepo?.id ?? null,
value
}),
githubItems,
githubItems: getVisibleHeldProviderResults({
items: githubItems,
value,
debouncedQuery
}),
gitlabAvailable: gitlabSourceAvailable,
gitlabItems,
gitlabItems: getVisibleHeldProviderResults({
items: gitlabItems,
value,
debouncedQuery
}),
linearAvailable,
linearIssues,
linearIssues: getVisibleHeldProviderResults({
items: linearIssues,
value,
debouncedQuery
}),
mode,
resultLimit: RESULT_LIMIT,
value
@@ -1063,6 +1089,7 @@ export default function SmartWorkspaceNameField({
[
branches,
branchResultsSource,
debouncedQuery,
githubItems,
gitlabSourceAvailable,
gitlabItems,
@@ -1115,6 +1142,14 @@ export default function SmartWorkspaceNameField({
isQueryStale,
sourceIntent
})
// Why: while isQueryStale, cmdk onValueChange is ignored; re-sync the stored arm
// when the query settles so commandValue cannot lag resolvedCommandValue.
useEffect(() => {
if (isQueryStale || commandValue === resolvedCommandValue) {
return
}
setCommandValue(resolvedCommandValue)
}, [commandValue, isQueryStale, resolvedCommandValue])
const activeEmojiShortcode = useMemo(
() => getActiveWorkspaceEmojiShortcode(value, emojiCursor),
[emojiCursor, value]
@@ -5,7 +5,9 @@ import {
getBranchSearchRequest,
getSmartWorkspaceEmptyHint,
getVisibleBranchResults,
isSmartWorkspaceSourceQueryWithinLimit
getVisibleHeldProviderResults,
isSmartWorkspaceSourceQueryWithinLimit,
shouldHoldSourceResultsForQuery
} from './smart-workspace-source-results'
describe('Branch source results', () => {
@@ -154,6 +156,25 @@ describe('Branch source results', () => {
).toEqual([])
})
it('drops a short settled query once the live query grows far beyond a typing delta', () => {
// Why: prefix-only hold would keep "f" results under "fix-unrelated-payment-bug".
expect(
getVisibleBranchResults({
mode: 'branches',
value: 'fix-unrelated-payment-bug',
selectedRepoId: 'repo-1',
resultRepoId: 'repo-1',
resultQuery: 'f',
branches: [{ refName: 'origin/foo', localBranchName: 'foo' }]
})
).toEqual([])
expect(
shouldHoldSourceResultsForQuery({ resultQuery: 'f', value: 'fix-unrelated-payment-bug' })
).toBe(false)
expect(shouldHoldSourceResultsForQuery({ resultQuery: 'feat', value: 'featu' })).toBe(true)
expect(shouldHoldSourceResultsForQuery({ resultQuery: 'feat', value: 'feature/x' })).toBe(false)
})
it('keeps held branch results across case-only edits of a prefix query', () => {
expect(
getVisibleBranchResults({
@@ -167,6 +188,36 @@ describe('Branch source results', () => {
).toEqual([{ refName: 'origin/feature', localBranchName: 'feature' }])
})
it('hides held provider results immediately when the field is cleared ahead of debounce', () => {
expect(
getVisibleHeldProviderResults({
items: [{ id: 'pr-1' }],
value: '',
debouncedQuery: 'fix'
})
).toEqual([])
})
it('keeps held provider results while the user types ahead of debounce', () => {
expect(
getVisibleHeldProviderResults({
items: [{ id: 'pr-1' }],
value: 'fix',
debouncedQuery: 'fi'
})
).toEqual([{ id: 'pr-1' }])
})
it('shows provider results once the cleared field and debounce are both empty', () => {
expect(
getVisibleHeldProviderResults({
items: [{ id: 'default-1' }],
value: '',
debouncedQuery: ''
})
).toEqual([{ id: 'default-1' }])
})
it('uses stable cmdk values for typed-text actions', () => {
expect(
buildSmartWorkspaceSourceRows({
@@ -75,6 +75,29 @@ export function getBranchSearchRequest({
return { repoId: selectedRepoId, query: trimmedQuery, limit }
}
/**
* Why: provider arrays lag the live input (200ms debounce). Keep them while the
* user is still typing, but hide immediately when the field is cleared so prior
* non-empty results cannot stay selectable until debounce catches up.
*/
export function getVisibleHeldProviderResults<T>({
items,
value,
debouncedQuery
}: {
items: readonly T[]
value: string
debouncedQuery: string
}): T[] {
if (!isSmartWorkspaceSourceQueryWithinLimit(value)) {
return []
}
if (value.trim() === '' && debouncedQuery.trim() !== '') {
return []
}
return items.slice()
}
export function getVisibleBranchResults({
branches,
mode,
@@ -106,18 +129,38 @@ export function getVisibleBranchResults({
if (currentQuery === '') {
return resultQuery === '' ? branches : []
}
const currentQueryKey = currentQuery.toLowerCase()
const resultQueryKey = resultQuery.toLowerCase()
if (
resultQueryKey !== currentQueryKey &&
!currentQueryKey.startsWith(resultQueryKey) &&
!resultQueryKey.startsWith(currentQueryKey)
) {
if (!shouldHoldSourceResultsForQuery({ resultQuery, value: currentQuery })) {
return []
}
return branches
}
/** Max |live − settled| length while still treating a prefix as "still typing". */
const SOURCE_RESULT_HOLD_MAX_DELTA = 4
/**
* Why: prefix-only hold lets a settled "f" stick under "fix-unrelated-…" for the
* whole next debounce. Cap the length delta so hold covers fast typing, not long
* continuations of a short settled query.
*/
export function shouldHoldSourceResultsForQuery({
resultQuery,
value
}: {
resultQuery: string
value: string
}): boolean {
const currentQueryKey = value.trim().toLowerCase()
const resultQueryKey = resultQuery.trim().toLowerCase()
if (resultQueryKey === currentQueryKey) {
return true
}
if (!currentQueryKey.startsWith(resultQueryKey) && !resultQueryKey.startsWith(currentQueryKey)) {
return false
}
return Math.abs(currentQueryKey.length - resultQueryKey.length) <= SOURCE_RESULT_HOLD_MAX_DELTA
}
export function buildSmartWorkspaceSourceRows({
branches,
githubItems,