diff --git a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx index 891ec922682..4d65c87bd47 100644 --- a/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx +++ b/src/renderer/src/components/new-workspace/SmartWorkspaceNameField.tsx @@ -832,8 +832,8 @@ export default function SmartWorkspaceNameField({ return } let stale = false - setBranches([]) - setBranchResultsSource(null) + // Why: keep prior branch rows until this request settles; visibility already + // holds the last list while the user types ahead of the debounced query. setBranchesLoading(true) void searchRuntimeRepoBaseRefDetails( selectedRepoOwnerSettings, @@ -1144,10 +1144,17 @@ export default function SmartWorkspaceNameField({ ) ?? null const loading = githubLoading || gitlabLoading || branchesLoading || linearLoading - const ActiveInputIcon = mode === 'text' ? CaseSensitive : loading ? LoaderCircle : Search + // Why: only spin on first load — not on every in-flight refresh while rows stay visible. + const showSearchSpinner = loading && searchResultRows.length === 0 + const ActiveInputIcon = + mode === 'text' ? CaseSensitive : showSearchSpinner ? LoaderCircle : Search 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 + } 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) @@ -1163,7 +1170,14 @@ export default function SmartWorkspaceNameField({ } setOpen(false) }, - [onBranchSelect, onGitHubItemSelect, onGitLabItemSelect, onLinearIssueSelect, onValueChange] + [ + isQueryStale, + onBranchSelect, + onGitHubItemSelect, + onGitLabItemSelect, + onLinearIssueSelect, + onValueChange + ] ) const applyEmojiReplacement = useCallback( @@ -1413,7 +1427,14 @@ export default function SmartWorkspaceNameField({ > { + // Why: cmdk re-emits when the item list reshapes; ignore while the query + // lags so the highlight cannot thrash mid-typing. + if (isQueryStale) { + return + } + setCommandValue(next) + }} shouldFilter={false} className="overflow-visible bg-transparent" > @@ -1499,7 +1520,7 @@ export default function SmartWorkspaceNameField({ 0) { const row = rows.find((entry) => entry.value === resolvedCommandValue) - if (row) { + if ( + row && + !( + isQueryStale && + row.kind !== 'use-name' && + row.kind !== 'create-branch' + ) + ) { event.preventDefault() handleSelect(row) return } - // No highlighted row (e.g. cleared stale GitHub/Linear results); fall through to onPlainEnter so the keypress isn't inert. + // No selectable row (stale provider arm); fall through to onPlainEnter. } onPlainEnter?.() } diff --git a/src/renderer/src/components/new-workspace/smart-workspace-command-value.test.ts b/src/renderer/src/components/new-workspace/smart-workspace-command-value.test.ts index 329c61c526c..4d030b29803 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-command-value.test.ts +++ b/src/renderer/src/components/new-workspace/smart-workspace-command-value.test.ts @@ -13,7 +13,7 @@ describe('resolveSmartWorkspaceCommandValue', () => { expect( resolveSmartWorkspaceCommandValue({ currentValue: 'github-12', - rows: [row('use-name', 'use-name-fix'), row('github', 'github-12')], + rows: [row('use-name', 'use-name'), row('github', 'github-12')], isQueryStale: false, sourceIntent: null }) @@ -24,40 +24,51 @@ describe('resolveSmartWorkspaceCommandValue', () => { expect( resolveSmartWorkspaceCommandValue({ currentValue: 'github-12', - rows: [row('use-name', 'use-name-fix'), row('branch', 'branch-main')], + rows: [row('use-name', 'use-name'), row('branch', 'branch-main')], isQueryStale: false, sourceIntent: null }) - ).toBe('use-name-fix') + ).toBe('use-name') }) - it('uses typed-text rows while source results are stale', () => { + it('freezes the current arm while the query is ahead of debounced search', () => { expect( resolveSmartWorkspaceCommandValue({ currentValue: 'github-12', - rows: [row('use-name', 'use-name-fix'), row('github', 'github-12')], + rows: [row('use-name', 'use-name'), row('github', 'github-12')], isQueryStale: true, sourceIntent: null }) - ).toBe('use-name-fix') + ).toBe('github-12') }) - it('clears selection while stale source-only rows have no typed fallback', () => { + it('falls back to typed-text when a frozen arm is no longer rendered', () => { expect( resolveSmartWorkspaceCommandValue({ currentValue: 'github-12', - rows: [row('github', 'github-12')], + rows: [row('use-name', 'use-name'), row('github', 'github-99')], isQueryStale: true, sourceIntent: null }) - ).toBe('') + ).toBe('use-name') + }) + + it('falls back to the first provider row when stale with no typed-text', () => { + expect( + resolveSmartWorkspaceCommandValue({ + currentValue: 'github-12', + rows: [row('github', 'github-99')], + isQueryStale: true, + sourceIntent: null + }) + ).toBe('github-99') }) it('prefers matching source-intent rows once fresh results arrive', () => { expect( resolveSmartWorkspaceCommandValue({ - currentValue: 'use-name-123', - rows: [row('use-name', 'use-name-123'), row('github', 'github-123')], + currentValue: 'use-name', + rows: [row('use-name', 'use-name'), row('github', 'github-123')], isQueryStale: false, sourceIntent: 'github' }) @@ -65,8 +76,8 @@ describe('resolveSmartWorkspaceCommandValue', () => { expect( resolveSmartWorkspaceCommandValue({ - currentValue: 'use-name-gitlab-url', - rows: [row('use-name', 'use-name-gitlab-url'), row('gitlab', 'gitlab-123')], + currentValue: 'use-name', + rows: [row('use-name', 'use-name'), row('gitlab', 'gitlab-123')], isQueryStale: false, sourceIntent: 'gitlab' }) @@ -74,8 +85,8 @@ describe('resolveSmartWorkspaceCommandValue', () => { expect( resolveSmartWorkspaceCommandValue({ - currentValue: 'use-name-eng-123', - rows: [row('use-name', 'use-name-eng-123'), row('linear', 'linear-ENG-123')], + currentValue: 'use-name', + rows: [row('use-name', 'use-name'), row('linear', 'linear-ENG-123')], isQueryStale: false, sourceIntent: 'linear' }) diff --git a/src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts b/src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts index c3c2d152cb4..8fd9744ded2 100644 --- a/src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts +++ b/src/renderer/src/components/new-workspace/smart-workspace-source-results.test.ts @@ -102,7 +102,7 @@ describe('Branch source results', () => { expect(rows).toEqual([]) }) - it('hides branch results from a stale Branch-mode query', () => { + it('hides branch results after the input is cleared while a prior query is still held', () => { expect( getVisibleBranchResults({ mode: 'branches', @@ -115,6 +115,49 @@ describe('Branch source results', () => { ).toEqual([]) }) + it('keeps the last branch results while the user types ahead of the settled query', () => { + expect( + getVisibleBranchResults({ + mode: 'branches', + value: 'featu', + 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({ + mode: 'smart', + value: 'refund-flow', + branches: [], + githubItems: [], + gitlabItems: [], + linearIssues: [], + gitlabAvailable: false, + linearAvailable: false, + resultLimit: 12 + })[0] + ).toMatchObject({ kind: 'use-name', value: 'use-name', name: 'refund-flow' }) + + expect( + buildSmartWorkspaceSourceRows({ + mode: 'branches', + value: 'new-branch', + branches: [], + githubItems: [], + gitlabItems: [], + linearIssues: [], + gitlabAvailable: false, + linearAvailable: false, + resultLimit: 12 + })[0] + ).toMatchObject({ kind: 'create-branch', value: 'create-branch', name: 'new-branch' }) + }) + it('keeps matching empty-query branch results visible in Branch mode', () => { expect( getVisibleBranchResults({ diff --git a/src/shared/new-workspace/smart-workspace-command-value.ts b/src/shared/new-workspace/smart-workspace-command-value.ts index 58d0e4289d3..b8f88e95a8a 100644 --- a/src/shared/new-workspace/smart-workspace-command-value.ts +++ b/src/shared/new-workspace/smart-workspace-command-value.ts @@ -28,9 +28,14 @@ export function resolveSmartWorkspaceCommandValue({ return currentValue } + // Why: freeze the arm while the live input is ahead of debounced search so the + // highlight does not thrash to use-name / empty / first-row on every keystroke. if (isQueryStale) { + if (rows.some((row) => row.value === currentValue)) { + return currentValue + } const typedTextRow = rows.find((row) => row.kind === 'use-name' || row.kind === 'create-branch') - return typedTextRow?.value ?? '' + return typedTextRow?.value ?? rows[0]?.value ?? '' } if (sourceIntent === 'github') { diff --git a/src/shared/new-workspace/smart-workspace-source-results.ts b/src/shared/new-workspace/smart-workspace-source-results.ts index 68041990910..127573ddd63 100644 --- a/src/shared/new-workspace/smart-workspace-source-results.ts +++ b/src/shared/new-workspace/smart-workspace-source-results.ts @@ -93,11 +93,15 @@ export function getVisibleBranchResults({ if (!isSmartWorkspaceSourceQueryWithinLimit(value)) { return [] } - const currentQuery = value.trim() if (mode !== 'branches' && mode !== 'smart') { return [] } - if (!selectedRepoId || resultRepoId !== selectedRepoId || resultQuery !== currentQuery) { + 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 !== '') { return [] } return branches @@ -130,7 +134,8 @@ export function buildSmartWorkspaceSourceRows({ const trimmed = value.trim() const nextRows: SmartWorkspaceSourceRow[] = [] if (trimmed && mode === 'smart') { - nextRows.push({ kind: 'use-name', value: `use-name-${trimmed}`, name: trimmed }) + // Why: stable cmdk value — embedding the query remounted the row every keystroke. + nextRows.push({ kind: 'use-name', value: 'use-name', name: trimmed }) } if (mode === 'text') { return nextRows @@ -159,7 +164,7 @@ export function buildSmartWorkspaceSourceRows({ (branch) => branch.refName === trimmed || branch.localBranchName === trimmed ) if (trimmed && mode === 'branches' && !branchExactMatch) { - nextRows.push({ kind: 'create-branch', value: `create-branch-${trimmed}`, name: trimmed }) + nextRows.push({ kind: 'create-branch', value: 'create-branch', name: trimmed }) } nextRows.push( ...branches.map((branch) => ({