mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-11 01:16:28 -07:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6e7f01566 | |||
| 3c240a8db3 | |||
| 45f2bc3822 | |||
| 327ba49b3d | |||
| 4b1ce5b1b2 |
@@ -22,7 +22,7 @@ import type { SelectionListItem } from '../hooks/useSelectionList.js';
|
||||
import { TabHeader, type Tab } from './shared/TabHeader.js';
|
||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||
import { Command } from '../key/keyMatchers.js';
|
||||
import { TextInput } from './shared/TextInput.js';
|
||||
import { AutocompleteTextInput } from './shared/AutocompleteTextInput.js';
|
||||
import { formatCommand } from '../key/keybindingUtils.js';
|
||||
import {
|
||||
useTextBuffer,
|
||||
@@ -396,10 +396,12 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
||||
|
||||
<Box flexDirection="row" marginBottom={1}>
|
||||
<Text color={theme.status.success}>{'> '}</Text>
|
||||
<TextInput
|
||||
<AutocompleteTextInput
|
||||
buffer={buffer}
|
||||
placeholder={placeholder}
|
||||
onSubmit={handleSubmit}
|
||||
availableWidth={availableWidth}
|
||||
suggestionsPosition="below"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -948,10 +950,12 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
/>
|
||||
)}
|
||||
<Text color={theme.text.primary}> </Text>
|
||||
<TextInput
|
||||
<AutocompleteTextInput
|
||||
buffer={customBuffer}
|
||||
placeholder={placeholder}
|
||||
focus={context.isSelected}
|
||||
availableWidth={availableWidth}
|
||||
suggestionsPosition="below"
|
||||
onSubmit={(val) => {
|
||||
if (question.multiSelect) {
|
||||
const fullAnswer = buildAnswerString(
|
||||
|
||||
@@ -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<InputPromptProps> = ({
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -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(
|
||||
<SuggestionsDisplay
|
||||
suggestions={mockSuggestions}
|
||||
activeIndex={0}
|
||||
isLoading={false}
|
||||
width={80}
|
||||
scrollOffset={0}
|
||||
userInput=""
|
||||
mode="reverse"
|
||||
/>,
|
||||
);
|
||||
@@ -116,7 +131,7 @@ describe('SuggestionsDisplay', () => {
|
||||
width={80}
|
||||
scrollOffset={0}
|
||||
userInput=""
|
||||
mode="reverse"
|
||||
mode="normal"
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
|
||||
@@ -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 (
|
||||
<Box flexDirection="column" paddingX={1} width={width}>
|
||||
{scrollOffset > 0 && <Text color={theme.text.primary}>▲</Text>}
|
||||
<Box
|
||||
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) => {
|
||||
const originalIndex = startIndex + index;
|
||||
@@ -153,7 +162,12 @@ export function SuggestionsDisplay({
|
||||
</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 && (
|
||||
<Text color="gray">
|
||||
({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`] = `
|
||||
" command1 Description 1
|
||||
command2 Description 2
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box } from 'ink';
|
||||
import { TextInput, type TextInputProps } from './TextInput.js';
|
||||
import { useCommandCompletion } from '../../hooks/useCommandCompletion.js';
|
||||
import { SuggestionsDisplay } from '../SuggestionsDisplay.js';
|
||||
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';
|
||||
|
||||
export interface AutocompleteTextInputProps extends TextInputProps {
|
||||
suggestionsPosition?: 'above' | 'below';
|
||||
availableWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around TextInput that provides @-mention autocomplete for files.
|
||||
*/
|
||||
export function AutocompleteTextInput(
|
||||
props: AutocompleteTextInputProps,
|
||||
): React.JSX.Element {
|
||||
const {
|
||||
suggestionsPosition = 'above',
|
||||
availableWidth = 80,
|
||||
...textInputProps
|
||||
} = props;
|
||||
const config = useConfig();
|
||||
const keyMatchers = useKeyMatchers();
|
||||
|
||||
const completion = useCommandCompletion({
|
||||
buffer: props.buffer,
|
||||
cwd: process.cwd(),
|
||||
slashCommands: [],
|
||||
shellModeActive: false,
|
||||
config,
|
||||
active: props.focus ?? true,
|
||||
});
|
||||
|
||||
const handleKeypress = (key: Key) => {
|
||||
if (!completion.showSuggestions) return false;
|
||||
|
||||
if (key.name === 'tab') {
|
||||
completion.handleAutocomplete(completion.activeSuggestionIndex);
|
||||
return true;
|
||||
}
|
||||
if (keyMatchers[Command.MOVE_UP](key)) {
|
||||
completion.navigateUp();
|
||||
return true;
|
||||
}
|
||||
if (keyMatchers[Command.MOVE_DOWN](key)) {
|
||||
completion.navigateDown();
|
||||
return true;
|
||||
}
|
||||
if (keyMatchers[Command.SUBMIT](key) && !completion.isPerfectMatch) {
|
||||
completion.handleAutocomplete(completion.activeSuggestionIndex);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
useKeypress(handleKeypress, {
|
||||
isActive: props.focus ?? true,
|
||||
priority: true,
|
||||
});
|
||||
|
||||
const suggestionsNode = completion.showSuggestions ? (
|
||||
<Box paddingRight={2}>
|
||||
<SuggestionsDisplay
|
||||
suggestions={completion.suggestions}
|
||||
activeIndex={completion.activeSuggestionIndex}
|
||||
isLoading={completion.isLoadingSuggestions}
|
||||
width={availableWidth}
|
||||
scrollOffset={completion.visibleStartIndex}
|
||||
userInput={props.buffer.text}
|
||||
mode={suggestionsPosition === 'above' ? 'reverse' : undefined}
|
||||
/>
|
||||
</Box>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{suggestionsPosition === 'above' && suggestionsNode}
|
||||
<TextInput {...textInputProps} />
|
||||
{suggestionsPosition === 'below' && suggestionsNode}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export interface UseCommandCompletionOptions {
|
||||
buffer: TextBuffer;
|
||||
cwd: string;
|
||||
slashCommands: readonly SlashCommand[];
|
||||
commandContext: CommandContext;
|
||||
commandContext?: CommandContext;
|
||||
reverseSearchActive?: boolean;
|
||||
shellModeActive: boolean;
|
||||
config?: Config;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user