mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 16:17:51 +00:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user