From 45f2bc3822139a4bcafcc0c63294df8a79cc082d Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Tue, 5 May 2026 08:49:40 -0400 Subject: [PATCH] refactor(cli): make CommandContext optional in completion hooks and improve SuggestionsDisplay UX --- .../cli/src/ui/components/InputPrompt.tsx | 10 +++----- .../ui/components/SuggestionsDisplay.test.tsx | 25 +++++++++++++++---- .../src/ui/components/SuggestionsDisplay.tsx | 24 ++++++++++++++---- .../SuggestionsDisplay.test.tsx.snap | 7 ++++++ .../shared/AutocompleteTextInput.tsx | 5 +--- .../cli/src/ui/hooks/useCommandCompletion.tsx | 2 +- .../cli/src/ui/hooks/useSlashCompletion.ts | 11 ++++++-- 7 files changed, 60 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 67fefe0656..f020fd8062 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -43,10 +43,7 @@ import chalk from 'chalk'; import stringWidth from 'string-width'; import { useShellHistory } from '../hooks/useShellHistory.js'; import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js'; -import { - useCommandCompletion, - CompletionMode, -} from '../hooks/useCommandCompletion.js'; +import { useCommandCompletion } from '../hooks/useCommandCompletion.js'; import { useKeypress, type Key } from '../hooks/useKeypress.js'; import { Command } from '../key/keyMatchers.js'; import { formatCommand } from '../key/keybindingUtils.js'; @@ -1759,14 +1756,13 @@ export const InputPrompt: React.FC = ({ scrollOffset={activeCompletion.visibleStartIndex} userInput={buffer.text} mode={ - completion.completionMode === CompletionMode.AT || - completion.completionMode === CompletionMode.SHELL + suggestionsPosition === 'above' ? 'reverse' : buffer.text.startsWith('/') && !reverseSearchActive && !commandSearchActive ? 'slash' - : 'reverse' + : 'normal' } expandedIndex={expandedSuggestionIndex} /> diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.test.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.test.tsx index c28d52332c..8aca7fabe9 100644 --- a/packages/cli/src/ui/components/SuggestionsDisplay.test.tsx +++ b/packages/cli/src/ui/components/SuggestionsDisplay.test.tsx @@ -25,7 +25,7 @@ describe('SuggestionsDisplay', () => { width={80} scrollOffset={0} userInput="" - mode="reverse" + mode="normal" />, ); expect(lastFrame()).toMatchSnapshot(); @@ -40,7 +40,7 @@ describe('SuggestionsDisplay', () => { width={80} scrollOffset={0} userInput="" - mode="reverse" + mode="normal" />, ); expect(lastFrame({ allowEmpty: true })).toBe(''); @@ -55,7 +55,7 @@ describe('SuggestionsDisplay', () => { width={80} scrollOffset={0} userInput="" - mode="reverse" + mode="normal" />, ); expect(lastFrame()).toMatchSnapshot(); @@ -72,7 +72,7 @@ describe('SuggestionsDisplay', () => { width={80} scrollOffset={0} userInput="" - mode="reverse" + mode="normal" />, ); expect(lastFrame()).toMatchSnapshot(); @@ -93,6 +93,21 @@ describe('SuggestionsDisplay', () => { width={80} scrollOffset={5} userInput="" + mode="normal" + />, + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders reverse mode correctly', async () => { + const { lastFrame } = await render( + , ); @@ -116,7 +131,7 @@ describe('SuggestionsDisplay', () => { width={80} scrollOffset={0} userInput="" - mode="reverse" + mode="normal" />, ); expect(lastFrame()).toMatchSnapshot(); diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.tsx index c17341faae..ef38658d11 100644 --- a/packages/cli/src/ui/components/SuggestionsDisplay.tsx +++ b/packages/cli/src/ui/components/SuggestionsDisplay.tsx @@ -28,7 +28,7 @@ interface SuggestionsDisplayProps { width: number; scrollOffset: number; userInput: string; - mode: 'reverse' | 'slash'; + mode?: 'reverse' | 'slash' | 'normal'; expandedIndex?: number; } @@ -42,7 +42,7 @@ export function SuggestionsDisplay({ width, scrollOffset, userInput, - mode, + mode = 'normal', expandedIndex, }: SuggestionsDisplayProps) { if (isLoading) { @@ -80,8 +80,17 @@ export function SuggestionsDisplay({ mode === 'slash' ? Math.min(maxLabelLength, Math.floor(width * 0.5)) : 0; return ( - - {scrollOffset > 0 && } + + {scrollOffset > 0 && mode !== 'reverse' && ( + + )} + {endIndex < suggestions.length && mode === 'reverse' && ( + + )} {visibleSuggestions.map((suggestion, index) => { const originalIndex = startIndex + index; @@ -153,7 +162,12 @@ export function SuggestionsDisplay({ ); })} - {endIndex < suggestions.length && } + {endIndex < suggestions.length && mode !== 'reverse' && ( + + )} + {scrollOffset > 0 && mode === 'reverse' && ( + + )} {suggestions.length > MAX_SUGGESTIONS_TO_SHOW && ( ({activeIndex + 1}/{suggestions.length}) diff --git a/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap index 3c79a534a2..e93a5f83ef 100644 --- a/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap @@ -32,6 +32,13 @@ exports[`SuggestionsDisplay > renders loading state 1`] = ` " `; +exports[`SuggestionsDisplay > renders reverse mode correctly 1`] = ` +" command3 Description 3 + command2 Description 2 + command1 Description 1 +" +`; + exports[`SuggestionsDisplay > renders suggestions list 1`] = ` " command1 Description 1 command2 Description 2 diff --git a/packages/cli/src/ui/components/shared/AutocompleteTextInput.tsx b/packages/cli/src/ui/components/shared/AutocompleteTextInput.tsx index 6b39e8ee6a..b2f76a7586 100644 --- a/packages/cli/src/ui/components/shared/AutocompleteTextInput.tsx +++ b/packages/cli/src/ui/components/shared/AutocompleteTextInput.tsx @@ -13,7 +13,6 @@ import { useConfig } from '../../contexts/ConfigContext.js'; import { useKeypress, type Key } from '../../hooks/useKeypress.js'; import { useKeyMatchers } from '../../hooks/useKeyMatchers.js'; import { Command } from '../../key/keyMatchers.js'; -import type { CommandContext } from '../../commands/types.js'; export interface AutocompleteTextInputProps extends TextInputProps { suggestionsPosition?: 'above' | 'below'; @@ -38,8 +37,6 @@ export function AutocompleteTextInput( buffer: props.buffer, cwd: process.cwd(), slashCommands: [], - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - commandContext: {} as unknown as CommandContext, shellModeActive: false, config, active: props.focus ?? true, @@ -81,7 +78,7 @@ export function AutocompleteTextInput( width={availableWidth} scrollOffset={completion.visibleStartIndex} userInput={props.buffer.text} - mode="reverse" + mode={suggestionsPosition === 'above' ? 'reverse' : undefined} /> ) : null; diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.tsx b/packages/cli/src/ui/hooks/useCommandCompletion.tsx index 4f89d69ff1..26346cd708 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.tsx +++ b/packages/cli/src/ui/hooks/useCommandCompletion.tsx @@ -67,7 +67,7 @@ export interface UseCommandCompletionOptions { buffer: TextBuffer; cwd: string; slashCommands: readonly SlashCommand[]; - commandContext: CommandContext; + commandContext?: CommandContext; reverseSearchActive?: boolean; shellModeActive: boolean; config?: Config; diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.ts b/packages/cli/src/ui/hooks/useSlashCompletion.ts index 3124a8b620..bb226d07db 100644 --- a/packages/cli/src/ui/hooks/useSlashCompletion.ts +++ b/packages/cli/src/ui/hooks/useSlashCompletion.ts @@ -145,7 +145,7 @@ interface PerfectMatchResult { function useCommandSuggestions( query: string | null, parserResult: CommandParserResult, - commandContext: CommandContext, + commandContext: CommandContext | undefined, getFzfForCommands: ( commands: readonly SlashCommand[], ) => FzfCommandCacheEntry | null, @@ -181,6 +181,13 @@ function useCommandSuggestions( return; } + if (!commandContext) { + debugLogger.warn( + 'CommandContext is required for argument completion', + ); + return; + } + const showLoading = leafCommand.showCompletionLoading !== false; if (showLoading) { setIsLoading(true); @@ -473,7 +480,7 @@ export interface UseSlashCompletionProps { enabled: boolean; query: string | null; slashCommands: readonly SlashCommand[]; - commandContext: CommandContext; + commandContext?: CommandContext; setSuggestions: (suggestions: Suggestion[]) => void; setIsLoadingSuggestions: (isLoading: boolean) => void; setIsPerfectMatch: (isMatch: boolean) => void;