Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions static/app/components/commandPalette/ui/commandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ export function CommandPalette({
const debouncedQuery = useDebouncedValue(state.query, 300);
const isFetchingQueries = useIsFetching({predicate: q => q.meta?.cmdk === true});
const isLoading =
(state.query.length > 0 && debouncedQuery !== state.query) || isFetchingQueries > 0;
state.list === 'active' &&
((state.query.length > 0 && debouncedQuery !== state.query) || isFetchingQueries > 0);
Comment thread
sentry[bot] marked this conversation as resolved.
const isEmptyPromptQuery =
state.action?.value.prompt !== undefined && (state.query.length === 0 || isLoading);

Expand All @@ -153,7 +154,7 @@ export function CommandPalette({
return nodes;
}, [store, state.action]);

const [actions, prefixMap, isSeerFallback] = useMemo<
const [computedActions, computedPrefixMap, computedIsSeerFallback] = useMemo<
[CMDKFlatItem[], Map<string, string[]>, boolean]
>(() => {
const [scored, scoredPrefixMap] = state.query
Expand Down Expand Up @@ -225,6 +226,22 @@ export function CommandPalette({
openForm,
]);

const frozenRef = useRef({
actions: computedActions,
prefixMap: computedPrefixMap,
isSeerFallback: computedIsSeerFallback,
});
if (state.list === 'active') {
frozenRef.current = {
actions: computedActions,
prefixMap: computedPrefixMap,
isSeerFallback: computedIsSeerFallback,
};
}
const actions = frozenRef.current.actions;
const prefixMap = frozenRef.current.prefixMap;
const isSeerFallback = frozenRef.current.isSeerFallback;

const analytics = useCommandPaletteAnalytics(isSeerFallback ? 0 : actions.length);
const mouseLeftResultsRef = useRef(false);

Expand Down Expand Up @@ -365,6 +382,10 @@ export function CommandPalette({
}
},
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
dispatch({type: 'freeze list'});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Premature empty state when frozen

Medium Severity

Tying isLoading to state.list === 'active' also hides the loading state used at line 657. If the user presses ArrowUp/Down while async results are still fetching and the visible list is empty, isLoading becomes false and CommandPaletteNoResults renders even though isFetchingQueries is still positive.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1d0f99b. Configure here.

if (
treeState.selectionManager.focusedKey === null &&
(e.key === 'ArrowDown' || e.key === 'ArrowUp')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export type CMDKNavStack = {
export type CommandPaletteState = {
action: CMDKNavStack | null;
input: React.RefObject<HTMLInputElement | null>;
// Controls whether the rendered action list updates from the collection store.
// 'frozen' keeps the visible list stable while the user navigates with the
// keyboard. Any other dispatched action resets to 'active'.
list: 'active' | 'frozen';
open: boolean;
// When true, action and query are cleared the next time the modal opens.
// Set by 'trigger action' so the close animation plays without a jarring
Expand Down Expand Up @@ -49,7 +53,8 @@ type CommandPaletteAction =
}
| {type: 'trigger action'}
| {type: 'pop action'}
| {type: 'reset on open'};
| {type: 'reset on open'}
| {type: 'freeze list'};

const CommandPaletteStateContext = createContext<CommandPaletteState | null>(null);
const CommandPaletteDispatchContext =
Expand All @@ -61,6 +66,8 @@ function commandPaletteReducer(
): CommandPaletteState {
const type = action.type;
switch (type) {
case 'freeze list':
return {...state, list: 'frozen'};
case 'toggle modal':
if (!state.open && state.resetOnOpen) {
return {
Expand All @@ -70,11 +77,13 @@ function commandPaletteReducer(
query: '',
resetOnOpen: false,
pendingReset: false,
list: 'active',
};
}
return {
...state,
open: !state.open,
list: 'active',
};
case 'reset':
return {
Expand All @@ -83,11 +92,12 @@ function commandPaletteReducer(
query: '',
pendingReset: false,
resetOnOpen: false,
list: 'active',
};
case 'reset on open':
return {...state, resetOnOpen: true};
return {...state, resetOnOpen: true, list: 'active'};
case 'set query':
return {...state, query: action.query};
return {...state, query: action.query, list: 'active'};
case 'push action':
return {
...state,
Expand All @@ -101,15 +111,17 @@ function commandPaletteReducer(
previous: state.action,
},
query: action.query ?? '',
list: 'active',
};
case 'pop action':
return {
...state,
action: state.action?.previous ?? null,
query: state.action?.value?.query ?? state.query,
list: 'active',
};
case 'trigger action':
return {...state, pendingReset: true};
return {...state, pendingReset: true, list: 'active'};
default:
unreachable(type);
return state;
Expand Down Expand Up @@ -149,6 +161,7 @@ export function CommandPaletteStateProvider({
open: false,
pendingReset: false,
resetOnOpen: false,
list: 'active',
});

return (
Expand Down
Loading