Fix(command): line/block Comments Incorrectly Parsed as Slash Commands (#6957)

Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
This commit is contained in:
fuyou
2025-08-26 11:51:27 +08:00
committed by GitHub
parent 97ce197f38
commit 45fff8f9f7
9 changed files with 157 additions and 9 deletions

View File

@@ -7,6 +7,7 @@
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { Colors } from '../colors.js'; import { Colors } from '../colors.js';
import { PrepareLabel } from './PrepareLabel.js'; import { PrepareLabel } from './PrepareLabel.js';
import { isSlashCommand } from '../utils/commandUtils.js';
export interface Suggestion { export interface Suggestion {
label: string; label: string;
value: string; value: string;
@@ -52,7 +53,7 @@ export function SuggestionsDisplay({
); );
const visibleSuggestions = suggestions.slice(startIndex, endIndex); const visibleSuggestions = suggestions.slice(startIndex, endIndex);
const isSlashCommandMode = userInput.startsWith('/'); const isSlashCommandMode = isSlashCommand(userInput);
let commandNameWidth = 0; let commandNameWidth = 0;
if (isSlashCommandMode) { if (isSlashCommandMode) {

View File

@@ -8,6 +8,7 @@ import type React from 'react';
import { Text, Box } from 'ink'; import { Text, Box } from 'ink';
import { Colors } from '../../colors.js'; import { Colors } from '../../colors.js';
import { SCREEN_READER_USER_PREFIX } from '../../constants.js'; import { SCREEN_READER_USER_PREFIX } from '../../constants.js';
import { isSlashCommand as checkIsSlashCommand } from '../../utils/commandUtils.js';
interface UserMessageProps { interface UserMessageProps {
text: string; text: string;
@@ -16,7 +17,7 @@ interface UserMessageProps {
export const UserMessage: React.FC<UserMessageProps> = ({ text }) => { export const UserMessage: React.FC<UserMessageProps> = ({ text }) => {
const prefix = '> '; const prefix = '> ';
const prefixWidth = prefix.length; const prefixWidth = prefix.length;
const isSlashCommand = text.startsWith('/'); const isSlashCommand = checkIsSlashCommand(text);
const textColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray; const textColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray;
const borderColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray; const borderColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray;

View File

@@ -516,4 +516,81 @@ describe('useCommandCompletion', () => {
); );
}); });
}); });
describe('prompt completion filtering', () => {
it('should not trigger prompt completion for line comments', async () => {
const mockConfig = {
getEnablePromptCompletion: () => true,
} as Config;
const { result } = renderHook(() => {
const textBuffer = useTextBufferForTest('// This is a line comment');
const completion = useCommandCompletion(
textBuffer,
testDirs,
testRootDir,
[],
mockCommandContext,
false,
mockConfig,
);
return { ...completion, textBuffer };
});
// Should not trigger prompt completion for comments
expect(result.current.suggestions.length).toBe(0);
});
it('should not trigger prompt completion for block comments', async () => {
const mockConfig = {
getEnablePromptCompletion: () => true,
} as Config;
const { result } = renderHook(() => {
const textBuffer = useTextBufferForTest(
'/* This is a block comment */',
);
const completion = useCommandCompletion(
textBuffer,
testDirs,
testRootDir,
[],
mockCommandContext,
false,
mockConfig,
);
return { ...completion, textBuffer };
});
// Should not trigger prompt completion for comments
expect(result.current.suggestions.length).toBe(0);
});
it('should trigger prompt completion for regular text when enabled', async () => {
const mockConfig = {
getEnablePromptCompletion: () => true,
} as Config;
const { result } = renderHook(() => {
const textBuffer = useTextBufferForTest(
'This is regular text that should trigger completion',
);
const completion = useCommandCompletion(
textBuffer,
testDirs,
testRootDir,
[],
mockCommandContext,
false,
mockConfig,
);
return { ...completion, textBuffer };
});
// This test verifies that comments are filtered out while regular text is not
expect(result.current.textBuffer.text).toBe(
'This is regular text that should trigger completion',
);
});
});
}); });

View File

@@ -134,7 +134,7 @@ export function useCommandCompletion(
if ( if (
isPromptCompletionEnabled && isPromptCompletionEnabled &&
trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH && trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH &&
!trimmedText.startsWith('/') && !isSlashCommand(trimmedText) &&
!trimmedText.includes('@') !trimmedText.includes('@')
) { ) {
return { return {

View File

@@ -1089,6 +1089,42 @@ describe('useGeminiStream', () => {
); );
}); });
}); });
it('should not call handleSlashCommand for line comments', async () => {
const { result, mockSendMessageStream: localMockSendMessageStream } =
renderTestHook();
await act(async () => {
await result.current.submitQuery('// This is a line comment');
});
await waitFor(() => {
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
expect(localMockSendMessageStream).toHaveBeenCalledWith(
'// This is a line comment',
expect.any(AbortSignal),
expect.any(String),
);
});
});
it('should not call handleSlashCommand for block comments', async () => {
const { result, mockSendMessageStream: localMockSendMessageStream } =
renderTestHook();
await act(async () => {
await result.current.submitQuery('/* This is a block comment */');
});
await waitFor(() => {
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
expect(localMockSendMessageStream).toHaveBeenCalledWith(
'/* This is a block comment */',
expect.any(AbortSignal),
expect.any(String),
);
});
});
}); });
describe('Memory Refresh on save_memory', () => { describe('Memory Refresh on save_memory', () => {

View File

@@ -40,7 +40,7 @@ import type {
SlashCommandProcessorResult, SlashCommandProcessorResult,
} from '../types.js'; } from '../types.js';
import { StreamingState, MessageType, ToolCallStatus } from '../types.js'; import { StreamingState, MessageType, ToolCallStatus } from '../types.js';
import { isAtCommand } from '../utils/commandUtils.js'; import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js';
import { useShellCommandProcessor } from './shellCommandProcessor.js'; import { useShellCommandProcessor } from './shellCommandProcessor.js';
import { handleAtCommand } from './atCommandProcessor.js'; import { handleAtCommand } from './atCommandProcessor.js';
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js'; import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
@@ -271,7 +271,9 @@ export const useGeminiStream = (
await logger?.logMessage(MessageSenderType.USER, trimmedQuery); await logger?.logMessage(MessageSenderType.USER, trimmedQuery);
// Handle UI-only commands first // Handle UI-only commands first
const slashCommandResult = await handleSlashCommand(trimmedQuery); const slashCommandResult = isSlashCommand(trimmedQuery)
? await handleSlashCommand(trimmedQuery)
: false;
if (slashCommandResult) { if (slashCommandResult) {
switch (slashCommandResult.type) { switch (slashCommandResult.type) {

View File

@@ -12,6 +12,7 @@ import {
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { Content, GenerateContentConfig } from '@google/genai'; import type { Content, GenerateContentConfig } from '@google/genai';
import type { TextBuffer } from '../components/shared/text-buffer.js'; import type { TextBuffer } from '../components/shared/text-buffer.js';
import { isSlashCommand } from '../utils/commandUtils.js';
export const PROMPT_COMPLETION_MIN_LENGTH = 5; export const PROMPT_COMPLETION_MIN_LENGTH = 5;
export const PROMPT_COMPLETION_DEBOUNCE_MS = 250; export const PROMPT_COMPLETION_DEBOUNCE_MS = 250;
@@ -81,7 +82,7 @@ export function usePromptCompletion({
if ( if (
trimmedText.length < PROMPT_COMPLETION_MIN_LENGTH || trimmedText.length < PROMPT_COMPLETION_MIN_LENGTH ||
!geminiClient || !geminiClient ||
trimmedText.startsWith('/') || isSlashCommand(trimmedText) ||
trimmedText.includes('@') || trimmedText.includes('@') ||
!isPromptCompletionEnabled !isPromptCompletionEnabled
) { ) {
@@ -237,7 +238,7 @@ export function usePromptCompletion({
const trimmedText = buffer.text.trim(); const trimmedText = buffer.text.trim();
return ( return (
trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH && trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH &&
!trimmedText.startsWith('/') && !isSlashCommand(trimmedText) &&
!trimmedText.includes('@') !trimmedText.includes('@')
); );
}, [buffer.text, isPromptCompletionEnabled, isCursorAtEnd]); }, [buffer.text, isPromptCompletionEnabled, isCursorAtEnd]);

View File

@@ -102,6 +102,20 @@ describe('commandUtils', () => {
expect(isSlashCommand('path/to/file')).toBe(false); expect(isSlashCommand('path/to/file')).toBe(false);
expect(isSlashCommand(' /help')).toBe(false); expect(isSlashCommand(' /help')).toBe(false);
}); });
it('should return false for line comments starting with //', () => {
expect(isSlashCommand('// This is a comment')).toBe(false);
expect(isSlashCommand('// check if variants base info all filled.')).toBe(
false,
);
expect(isSlashCommand('//comment without space')).toBe(false);
});
it('should return false for block comments starting with /*', () => {
expect(isSlashCommand('/* This is a block comment */')).toBe(false);
expect(isSlashCommand('/*\n * Multi-line comment\n */')).toBe(false);
expect(isSlashCommand('/*comment without space*/')).toBe(false);
});
}); });
describe('copyToClipboard', () => { describe('copyToClipboard', () => {

View File

@@ -21,12 +21,28 @@ export const isAtCommand = (query: string): boolean =>
/** /**
* Checks if a query string potentially represents an '/' command. * Checks if a query string potentially represents an '/' command.
* It triggers if the query starts with '/' * It triggers if the query starts with '/' but excludes code comments like '//' and '/*'.
* *
* @param query The input query string. * @param query The input query string.
* @returns True if the query looks like an '/' command, false otherwise. * @returns True if the query looks like an '/' command, false otherwise.
*/ */
export const isSlashCommand = (query: string): boolean => query.startsWith('/'); export const isSlashCommand = (query: string): boolean => {
if (!query.startsWith('/')) {
return false;
}
// Exclude line comments that start with '//'
if (query.startsWith('//')) {
return false;
}
// Exclude block comments that start with '/*'
if (query.startsWith('/*')) {
return false;
}
return true;
};
// Copies a string snippet to the clipboard for different platforms // Copies a string snippet to the clipboard for different platforms
export const copyToClipboard = async (text: string): Promise<void> => { export const copyToClipboard = async (text: string): Promise<void> => {