mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 04:48:09 -07:00
fix(cli): fix Tab completion for /chat and /resume subcommand menu
- Change synthetic "list" suggestion to use insertValue: 'list' instead of submitValue, so Tab inserts text into the input bar rather than immediately executing the command - Add partial === '' guard so the synthetic entry only appears at the subcommand selection stage (e.g. /chat , not /chat li) - Rename the manual checkpoint subcommand from "list" to "checkpoints" to avoid name collision with the synthetic auto-saved "list" entry - Add trailing space after Tab-completing commands with subcommands (e.g. /chat → /chat ) so the subcommand menu appears
This commit is contained in:
@@ -71,7 +71,7 @@ const getSavedChatTags = async (
|
||||
};
|
||||
|
||||
const listCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
name: 'checkpoints',
|
||||
description: 'List saved manual conversation checkpoints',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
|
||||
@@ -1102,6 +1102,39 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should autocomplete the synthetic auto suggestion on Tab (not submit)', async () => {
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [
|
||||
{
|
||||
label: 'list',
|
||||
value: 'list',
|
||||
insertValue: 'list',
|
||||
},
|
||||
],
|
||||
activeSuggestionIndex: 0,
|
||||
});
|
||||
props.buffer.setText('/chat ');
|
||||
|
||||
const { stdin, unmount } = await renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
{
|
||||
uiActions,
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\t');
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0),
|
||||
);
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('queues a message when Tab is pressed during generation', async () => {
|
||||
props.buffer.setText('A new prompt');
|
||||
props.streamingState = StreamingState.Responding;
|
||||
|
||||
@@ -1066,7 +1066,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
handleSubmit(suggestion.submitValue.trim());
|
||||
return true;
|
||||
}
|
||||
|
||||
const { isArgumentCompletion, leafCommand } =
|
||||
completion.slashCompletionRange;
|
||||
|
||||
|
||||
@@ -1,95 +1,5 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor correctly 'at the beginning of a line' in a multiline block 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > first line │
|
||||
│ second line │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor correctly 'at the end of a line' in a multiline block 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > first line │
|
||||
│ second line │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor correctly 'in the middle of a line' in a multiline block 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > first line │
|
||||
│ second line │
|
||||
│ third line │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > multi-line scenarios > should display cursor on a blank line in a multiline block 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > first line │
|
||||
│ │
|
||||
│ third line │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'after multi-byte unicode characters' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > 👍A │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the beginning of the line' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the end of a line with unicode cha…' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello 👍 │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the end of a short line with unico…' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > 👍 │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'at the end of the line' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'for multi-byte unicode characters' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello 👍 world │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'mid-word' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello world │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on a highlighted token' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > run @path/to/file │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on a space between words' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello world │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > Highlighting and Cursor Display > single-line scenarios > should display cursor correctly 'on an empty line' 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > Type your message or @path/to/file │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > History Navigation and Completion Suppression > should not render suggestions during history navigation 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> second message
|
||||
@@ -168,18 +78,32 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > multiline rendering > should correctly render multiline input including blank lines 1`] = `
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
│ > hello │
|
||||
│ │
|
||||
│ world │
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────"
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 5`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 6`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = `
|
||||
|
||||
@@ -442,11 +442,18 @@ export function useCommandCompletion({
|
||||
if (completionMode === CompletionMode.SLASH) {
|
||||
const command =
|
||||
slashCompletionRange.getCommandFromSuggestion(suggestion);
|
||||
// Don't add a space if the command has an action (can be executed)
|
||||
// and doesn't have a completion function (doesn't REQUIRE more arguments)
|
||||
// Don't add a space if the command has an action (can be executed),
|
||||
// doesn't have a completion function (doesn't REQUIRE more arguments),
|
||||
// AND doesn't have subcommands (no further navigation expected).
|
||||
const isExecutableCommand = !!(command && command.action);
|
||||
const requiresArguments = !!(command && command.completion);
|
||||
shouldAddSpace = !isExecutableCommand || requiresArguments;
|
||||
const hasSubCommands = !!(
|
||||
command &&
|
||||
command.subCommands &&
|
||||
command.subCommands.length > 0
|
||||
);
|
||||
shouldAddSpace =
|
||||
!isExecutableCommand || requiresArguments || hasSubCommands;
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -476,7 +476,7 @@ describe('useSlashCompletion', () => {
|
||||
expect(chatResult.current.suggestions[0]).toMatchObject({
|
||||
label: 'list',
|
||||
sectionTitle: 'auto',
|
||||
submitValue: '/chat',
|
||||
insertValue: 'list',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -496,7 +496,7 @@ describe('useSlashCompletion', () => {
|
||||
expect(resumeResult.current.suggestions[0]).toMatchObject({
|
||||
label: 'list',
|
||||
sectionTitle: 'auto',
|
||||
submitValue: '/resume',
|
||||
insertValue: 'list',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -513,6 +513,53 @@ describe('useSlashCompletion', () => {
|
||||
unmountResume();
|
||||
});
|
||||
|
||||
it('should prioritize matching subcommands over the synthetic auto entry when typing /chat subcommand prefixes', async () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
name: 'chat',
|
||||
description: 'Chat command',
|
||||
action: vi.fn(),
|
||||
subCommands: [
|
||||
createTestCommand({
|
||||
name: 'list',
|
||||
description: 'List checkpoints',
|
||||
suggestionGroup: 'checkpoints',
|
||||
action: vi.fn(),
|
||||
}),
|
||||
createTestCommand({
|
||||
name: 'resume',
|
||||
description: 'Resume a saved chat',
|
||||
action: vi.fn(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const { result, unmount } = await renderHook(() =>
|
||||
useTestHarnessForSlashCompletion(
|
||||
true,
|
||||
'/chat resu',
|
||||
slashCommands,
|
||||
mockCommandContext,
|
||||
),
|
||||
);
|
||||
|
||||
await resolveMatch();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.suggestions).toHaveLength(1);
|
||||
expect(result.current.suggestions[0]).toMatchObject({
|
||||
label: 'resume',
|
||||
value: 'resume',
|
||||
});
|
||||
expect(
|
||||
result.current.suggestions.some((s) => s.sectionTitle === 'auto'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should sort exact altName matches to the top', async () => {
|
||||
const slashCommands = [
|
||||
createTestCommand({
|
||||
|
||||
@@ -336,16 +336,14 @@ function useCommandSuggestions(
|
||||
matchesCommand(leafCommand, commandPathParts[0])))
|
||||
);
|
||||
|
||||
if (isTopLevelChatOrResumeContext) {
|
||||
const canonicalParentName = leafCommand.name;
|
||||
if (isTopLevelChatOrResumeContext && partial === '') {
|
||||
const autoSectionSuggestion: Suggestion = {
|
||||
label: 'list',
|
||||
value: 'list',
|
||||
insertValue: canonicalParentName,
|
||||
insertValue: 'list',
|
||||
description: 'Browse auto-saved chats',
|
||||
commandKind: CommandKind.BUILT_IN,
|
||||
sectionTitle: 'auto',
|
||||
submitValue: `/${canonicalParentName}`,
|
||||
};
|
||||
setSuggestions([autoSectionSuggestion, ...finalSuggestions]);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user