feat(cli): Suppress slash command execution and suggestions in shell … (#11380)

This commit is contained in:
Jainam M
2025-10-17 23:00:27 +05:30
committed by GitHub
parent 795e5134c7
commit 659b0557be
6 changed files with 154 additions and 36 deletions

View File

@@ -789,6 +789,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -816,6 +817,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -843,6 +845,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -870,6 +873,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -897,6 +901,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -925,6 +930,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -952,6 +958,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -980,6 +987,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -1008,6 +1016,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -1036,6 +1045,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -1064,6 +1074,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -1094,6 +1105,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -1122,6 +1134,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -1152,6 +1165,7 @@ describe('InputPrompt', () => {
mockSlashCommands,
mockCommandContext,
false,
false,
expect.any(Object),
);
@@ -1161,7 +1175,6 @@ describe('InputPrompt', () => {
describe('vim mode', () => {
it('should not call buffer.handleInput when vim mode is enabled and vim handles the input', async () => {
props.vimModeEnabled = true;
props.vimHandleInput = vi.fn().mockReturnValue(true); // Mock that vim handled it.
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
@@ -1177,7 +1190,6 @@ describe('InputPrompt', () => {
});
it('should call buffer.handleInput when vim mode is enabled but vim does not handle the input', async () => {
props.vimModeEnabled = true;
props.vimHandleInput = vi.fn().mockReturnValue(false); // Mock that vim did NOT handle it.
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,

View File

@@ -155,6 +155,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
slashCommands,
commandContext,
reverseSearchActive,
shellModeActive,
config,
);

View File

@@ -121,6 +121,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -146,6 +147,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
);
return { completion, textBuffer };
@@ -179,6 +181,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -207,6 +210,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -233,6 +237,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -246,6 +251,56 @@ describe('useCommandCompletion', () => {
);
});
});
it.each([
{
shellModeActive: false,
expectedSuggestions: 1,
expectedShowSuggestions: true,
description:
'should show slash command suggestions when shellModeActive is false',
},
{
shellModeActive: true,
expectedSuggestions: 0,
expectedShowSuggestions: false,
description:
'should not show slash command suggestions when shellModeActive is true',
},
])(
'$description',
async ({
shellModeActive,
expectedSuggestions,
expectedShowSuggestions,
}) => {
setupMocks({
slashSuggestions: [{ label: 'clear', value: 'clear' }],
});
const { result } = renderHook(() => {
const textBuffer = useTextBufferForTest('/');
const completion = useCommandCompletion(
textBuffer,
testDirs,
testRootDir,
[],
mockCommandContext,
false,
shellModeActive, // Parameterized shellModeActive
mockConfig,
);
return { ...completion, textBuffer };
});
await waitFor(() => {
expect(result.current.suggestions.length).toBe(expectedSuggestions);
expect(result.current.showSuggestions).toBe(
expectedShowSuggestions,
);
});
},
);
});
describe('Navigation', () => {
@@ -272,6 +327,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -293,6 +349,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -313,6 +370,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -339,6 +397,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -368,6 +427,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -405,6 +465,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
),
);
@@ -435,6 +496,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
);
return { ...completion, textBuffer };
@@ -465,6 +527,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
);
return { ...completion, textBuffer };
@@ -498,6 +561,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
);
return { ...completion, textBuffer };
@@ -530,6 +594,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
);
return { ...completion, textBuffer };
@@ -562,6 +627,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
);
return { ...completion, textBuffer };
@@ -594,6 +660,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
);
return { ...completion, textBuffer };
@@ -619,6 +686,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
);
return { ...completion, textBuffer };
@@ -644,6 +712,7 @@ describe('useCommandCompletion', () => {
[],
mockCommandContext,
false,
false,
mockConfig,
);
return { ...completion, textBuffer };

View File

@@ -51,6 +51,7 @@ export function useCommandCompletion(
slashCommands: readonly SlashCommand[],
commandContext: CommandContext,
reverseSearchActive: boolean = false,
shellModeActive: boolean,
config?: Config,
): UseCommandCompletionReturn {
const {
@@ -163,7 +164,7 @@ export function useCommandCompletion(
});
const slashCompletionRange = useSlashCompletion({
enabled: completionMode === CompletionMode.SLASH,
enabled: completionMode === CompletionMode.SLASH && !shellModeActive,
query,
slashCommands,
commandContext,

View File

@@ -1207,6 +1207,39 @@ describe('useGeminiStream', () => {
);
});
});
it('should not call handleSlashCommand is shell mode is active', async () => {
const { result } = renderHook(() =>
useGeminiStream(
new MockedGeminiClientClass(mockConfig),
[],
mockAddItem,
mockConfig,
mockLoadedSettings,
() => {},
mockHandleSlashCommand,
true,
() => 'vscode' as EditorType,
() => {},
() => Promise.resolve(),
false,
() => {},
() => {},
() => {},
() => {},
80,
24,
),
);
await act(async () => {
await result.current.submitQuery('/about');
});
await waitFor(() => {
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
});
});
});
describe('Memory Refresh on save_memory', () => {

View File

@@ -327,41 +327,43 @@ export const useGeminiStream = (
onDebugMessage(`User query: '${trimmedQuery}'`);
await logger?.logMessage(MessageSenderType.USER, trimmedQuery);
// Handle UI-only commands first
const slashCommandResult = isSlashCommand(trimmedQuery)
? await handleSlashCommand(trimmedQuery)
: false;
if (!shellModeActive) {
// Handle UI-only commands first
const slashCommandResult = isSlashCommand(trimmedQuery)
? await handleSlashCommand(trimmedQuery)
: false;
if (slashCommandResult) {
switch (slashCommandResult.type) {
case 'schedule_tool': {
const { toolName, toolArgs } = slashCommandResult;
const toolCallRequest: ToolCallRequestInfo = {
callId: `${toolName}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
name: toolName,
args: toolArgs,
isClientInitiated: true,
prompt_id,
};
scheduleToolCalls([toolCallRequest], abortSignal);
return { queryToSend: null, shouldProceed: false };
}
case 'submit_prompt': {
localQueryToSendToGemini = slashCommandResult.content;
if (slashCommandResult) {
switch (slashCommandResult.type) {
case 'schedule_tool': {
const { toolName, toolArgs } = slashCommandResult;
const toolCallRequest: ToolCallRequestInfo = {
callId: `${toolName}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
name: toolName,
args: toolArgs,
isClientInitiated: true,
prompt_id,
};
scheduleToolCalls([toolCallRequest], abortSignal);
return { queryToSend: null, shouldProceed: false };
}
case 'submit_prompt': {
localQueryToSendToGemini = slashCommandResult.content;
return {
queryToSend: localQueryToSendToGemini,
shouldProceed: true,
};
}
case 'handled': {
return { queryToSend: null, shouldProceed: false };
}
default: {
const unreachable: never = slashCommandResult;
throw new Error(
`Unhandled slash command result type: ${unreachable}`,
);
return {
queryToSend: localQueryToSendToGemini,
shouldProceed: true,
};
}
case 'handled': {
return { queryToSend: null, shouldProceed: false };
}
default: {
const unreachable: never = slashCommandResult;
throw new Error(
`Unhandled slash command result type: ${unreachable}`,
);
}
}
}
}