mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-17 21:40:39 -07:00
refactor(cli): make CommandContext optional in completion hooks and improve SuggestionsDisplay UX
This commit is contained in:
@@ -43,10 +43,7 @@ import chalk from 'chalk';
|
|||||||
import stringWidth from 'string-width';
|
import stringWidth from 'string-width';
|
||||||
import { useShellHistory } from '../hooks/useShellHistory.js';
|
import { useShellHistory } from '../hooks/useShellHistory.js';
|
||||||
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
|
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
|
||||||
import {
|
import { useCommandCompletion } from '../hooks/useCommandCompletion.js';
|
||||||
useCommandCompletion,
|
|
||||||
CompletionMode,
|
|
||||||
} from '../hooks/useCommandCompletion.js';
|
|
||||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||||
import { Command } from '../key/keyMatchers.js';
|
import { Command } from '../key/keyMatchers.js';
|
||||||
import { formatCommand } from '../key/keybindingUtils.js';
|
import { formatCommand } from '../key/keybindingUtils.js';
|
||||||
@@ -1759,14 +1756,13 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
scrollOffset={activeCompletion.visibleStartIndex}
|
scrollOffset={activeCompletion.visibleStartIndex}
|
||||||
userInput={buffer.text}
|
userInput={buffer.text}
|
||||||
mode={
|
mode={
|
||||||
completion.completionMode === CompletionMode.AT ||
|
suggestionsPosition === 'above'
|
||||||
completion.completionMode === CompletionMode.SHELL
|
|
||||||
? 'reverse'
|
? 'reverse'
|
||||||
: buffer.text.startsWith('/') &&
|
: buffer.text.startsWith('/') &&
|
||||||
!reverseSearchActive &&
|
!reverseSearchActive &&
|
||||||
!commandSearchActive
|
!commandSearchActive
|
||||||
? 'slash'
|
? 'slash'
|
||||||
: 'reverse'
|
: 'normal'
|
||||||
}
|
}
|
||||||
expandedIndex={expandedSuggestionIndex}
|
expandedIndex={expandedSuggestionIndex}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ describe('SuggestionsDisplay', () => {
|
|||||||
width={80}
|
width={80}
|
||||||
scrollOffset={0}
|
scrollOffset={0}
|
||||||
userInput=""
|
userInput=""
|
||||||
mode="reverse"
|
mode="normal"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
@@ -40,7 +40,7 @@ describe('SuggestionsDisplay', () => {
|
|||||||
width={80}
|
width={80}
|
||||||
scrollOffset={0}
|
scrollOffset={0}
|
||||||
userInput=""
|
userInput=""
|
||||||
mode="reverse"
|
mode="normal"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||||
@@ -55,7 +55,7 @@ describe('SuggestionsDisplay', () => {
|
|||||||
width={80}
|
width={80}
|
||||||
scrollOffset={0}
|
scrollOffset={0}
|
||||||
userInput=""
|
userInput=""
|
||||||
mode="reverse"
|
mode="normal"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
@@ -72,7 +72,7 @@ describe('SuggestionsDisplay', () => {
|
|||||||
width={80}
|
width={80}
|
||||||
scrollOffset={0}
|
scrollOffset={0}
|
||||||
userInput=""
|
userInput=""
|
||||||
mode="reverse"
|
mode="normal"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
@@ -93,6 +93,21 @@ describe('SuggestionsDisplay', () => {
|
|||||||
width={80}
|
width={80}
|
||||||
scrollOffset={5}
|
scrollOffset={5}
|
||||||
userInput=""
|
userInput=""
|
||||||
|
mode="normal"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders reverse mode correctly', async () => {
|
||||||
|
const { lastFrame } = await render(
|
||||||
|
<SuggestionsDisplay
|
||||||
|
suggestions={mockSuggestions}
|
||||||
|
activeIndex={0}
|
||||||
|
isLoading={false}
|
||||||
|
width={80}
|
||||||
|
scrollOffset={0}
|
||||||
|
userInput=""
|
||||||
mode="reverse"
|
mode="reverse"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -116,7 +131,7 @@ describe('SuggestionsDisplay', () => {
|
|||||||
width={80}
|
width={80}
|
||||||
scrollOffset={0}
|
scrollOffset={0}
|
||||||
userInput=""
|
userInput=""
|
||||||
mode="reverse"
|
mode="normal"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ interface SuggestionsDisplayProps {
|
|||||||
width: number;
|
width: number;
|
||||||
scrollOffset: number;
|
scrollOffset: number;
|
||||||
userInput: string;
|
userInput: string;
|
||||||
mode: 'reverse' | 'slash';
|
mode?: 'reverse' | 'slash' | 'normal';
|
||||||
expandedIndex?: number;
|
expandedIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export function SuggestionsDisplay({
|
|||||||
width,
|
width,
|
||||||
scrollOffset,
|
scrollOffset,
|
||||||
userInput,
|
userInput,
|
||||||
mode,
|
mode = 'normal',
|
||||||
expandedIndex,
|
expandedIndex,
|
||||||
}: SuggestionsDisplayProps) {
|
}: SuggestionsDisplayProps) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -80,8 +80,17 @@ export function SuggestionsDisplay({
|
|||||||
mode === 'slash' ? Math.min(maxLabelLength, Math.floor(width * 0.5)) : 0;
|
mode === 'slash' ? Math.min(maxLabelLength, Math.floor(width * 0.5)) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" paddingX={1} width={width}>
|
<Box
|
||||||
{scrollOffset > 0 && <Text color={theme.text.primary}>▲</Text>}
|
flexDirection={mode === 'reverse' ? 'column-reverse' : 'column'}
|
||||||
|
paddingX={1}
|
||||||
|
width={width}
|
||||||
|
>
|
||||||
|
{scrollOffset > 0 && mode !== 'reverse' && (
|
||||||
|
<Text color={theme.text.primary}>▲</Text>
|
||||||
|
)}
|
||||||
|
{endIndex < suggestions.length && mode === 'reverse' && (
|
||||||
|
<Text color="gray">▼</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{visibleSuggestions.map((suggestion, index) => {
|
{visibleSuggestions.map((suggestion, index) => {
|
||||||
const originalIndex = startIndex + index;
|
const originalIndex = startIndex + index;
|
||||||
@@ -153,7 +162,12 @@ export function SuggestionsDisplay({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{endIndex < suggestions.length && <Text color="gray">▼</Text>}
|
{endIndex < suggestions.length && mode !== 'reverse' && (
|
||||||
|
<Text color="gray">▼</Text>
|
||||||
|
)}
|
||||||
|
{scrollOffset > 0 && mode === 'reverse' && (
|
||||||
|
<Text color={theme.text.primary}>▲</Text>
|
||||||
|
)}
|
||||||
{suggestions.length > MAX_SUGGESTIONS_TO_SHOW && (
|
{suggestions.length > MAX_SUGGESTIONS_TO_SHOW && (
|
||||||
<Text color="gray">
|
<Text color="gray">
|
||||||
({activeIndex + 1}/{suggestions.length})
|
({activeIndex + 1}/{suggestions.length})
|
||||||
|
|||||||
@@ -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`] = `
|
exports[`SuggestionsDisplay > renders suggestions list 1`] = `
|
||||||
" command1 Description 1
|
" command1 Description 1
|
||||||
command2 Description 2
|
command2 Description 2
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { useConfig } from '../../contexts/ConfigContext.js';
|
|||||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
import { Command } from '../../key/keyMatchers.js';
|
import { Command } from '../../key/keyMatchers.js';
|
||||||
import type { CommandContext } from '../../commands/types.js';
|
|
||||||
|
|
||||||
export interface AutocompleteTextInputProps extends TextInputProps {
|
export interface AutocompleteTextInputProps extends TextInputProps {
|
||||||
suggestionsPosition?: 'above' | 'below';
|
suggestionsPosition?: 'above' | 'below';
|
||||||
@@ -38,8 +37,6 @@ export function AutocompleteTextInput(
|
|||||||
buffer: props.buffer,
|
buffer: props.buffer,
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
slashCommands: [],
|
slashCommands: [],
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
||||||
commandContext: {} as unknown as CommandContext,
|
|
||||||
shellModeActive: false,
|
shellModeActive: false,
|
||||||
config,
|
config,
|
||||||
active: props.focus ?? true,
|
active: props.focus ?? true,
|
||||||
@@ -81,7 +78,7 @@ export function AutocompleteTextInput(
|
|||||||
width={availableWidth}
|
width={availableWidth}
|
||||||
scrollOffset={completion.visibleStartIndex}
|
scrollOffset={completion.visibleStartIndex}
|
||||||
userInput={props.buffer.text}
|
userInput={props.buffer.text}
|
||||||
mode="reverse"
|
mode={suggestionsPosition === 'above' ? 'reverse' : undefined}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : null;
|
) : null;
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export interface UseCommandCompletionOptions {
|
|||||||
buffer: TextBuffer;
|
buffer: TextBuffer;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
slashCommands: readonly SlashCommand[];
|
slashCommands: readonly SlashCommand[];
|
||||||
commandContext: CommandContext;
|
commandContext?: CommandContext;
|
||||||
reverseSearchActive?: boolean;
|
reverseSearchActive?: boolean;
|
||||||
shellModeActive: boolean;
|
shellModeActive: boolean;
|
||||||
config?: Config;
|
config?: Config;
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ interface PerfectMatchResult {
|
|||||||
function useCommandSuggestions(
|
function useCommandSuggestions(
|
||||||
query: string | null,
|
query: string | null,
|
||||||
parserResult: CommandParserResult,
|
parserResult: CommandParserResult,
|
||||||
commandContext: CommandContext,
|
commandContext: CommandContext | undefined,
|
||||||
getFzfForCommands: (
|
getFzfForCommands: (
|
||||||
commands: readonly SlashCommand[],
|
commands: readonly SlashCommand[],
|
||||||
) => FzfCommandCacheEntry | null,
|
) => FzfCommandCacheEntry | null,
|
||||||
@@ -181,6 +181,13 @@ function useCommandSuggestions(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!commandContext) {
|
||||||
|
debugLogger.warn(
|
||||||
|
'CommandContext is required for argument completion',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const showLoading = leafCommand.showCompletionLoading !== false;
|
const showLoading = leafCommand.showCompletionLoading !== false;
|
||||||
if (showLoading) {
|
if (showLoading) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -473,7 +480,7 @@ export interface UseSlashCompletionProps {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
query: string | null;
|
query: string | null;
|
||||||
slashCommands: readonly SlashCommand[];
|
slashCommands: readonly SlashCommand[];
|
||||||
commandContext: CommandContext;
|
commandContext?: CommandContext;
|
||||||
setSuggestions: (suggestions: Suggestion[]) => void;
|
setSuggestions: (suggestions: Suggestion[]) => void;
|
||||||
setIsLoadingSuggestions: (isLoading: boolean) => void;
|
setIsLoadingSuggestions: (isLoading: boolean) => void;
|
||||||
setIsPerfectMatch: (isMatch: boolean) => void;
|
setIsPerfectMatch: (isMatch: boolean) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user