From 45fff8f9f71766d5adc8994671aa36187528670c Mon Sep 17 00:00:00 2001 From: fuyou Date: Tue, 26 Aug 2025 11:51:27 +0800 Subject: [PATCH] Fix(command): line/block Comments Incorrectly Parsed as Slash Commands (#6957) Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com> --- .../src/ui/components/SuggestionsDisplay.tsx | 3 +- .../ui/components/messages/UserMessage.tsx | 3 +- .../src/ui/hooks/useCommandCompletion.test.ts | 77 +++++++++++++++++++ .../cli/src/ui/hooks/useCommandCompletion.tsx | 2 +- .../cli/src/ui/hooks/useGeminiStream.test.tsx | 36 +++++++++ packages/cli/src/ui/hooks/useGeminiStream.ts | 6 +- .../cli/src/ui/hooks/usePromptCompletion.ts | 5 +- .../cli/src/ui/utils/commandUtils.test.ts | 14 ++++ packages/cli/src/ui/utils/commandUtils.ts | 20 ++++- 9 files changed, 157 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.tsx index facdfee3..5a74707d 100644 --- a/packages/cli/src/ui/components/SuggestionsDisplay.tsx +++ b/packages/cli/src/ui/components/SuggestionsDisplay.tsx @@ -7,6 +7,7 @@ import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { PrepareLabel } from './PrepareLabel.js'; +import { isSlashCommand } from '../utils/commandUtils.js'; export interface Suggestion { label: string; value: string; @@ -52,7 +53,7 @@ export function SuggestionsDisplay({ ); const visibleSuggestions = suggestions.slice(startIndex, endIndex); - const isSlashCommandMode = userInput.startsWith('/'); + const isSlashCommandMode = isSlashCommand(userInput); let commandNameWidth = 0; if (isSlashCommandMode) { diff --git a/packages/cli/src/ui/components/messages/UserMessage.tsx b/packages/cli/src/ui/components/messages/UserMessage.tsx index 1fd3b3bd..a05964f3 100644 --- a/packages/cli/src/ui/components/messages/UserMessage.tsx +++ b/packages/cli/src/ui/components/messages/UserMessage.tsx @@ -8,6 +8,7 @@ import type React from 'react'; import { Text, Box } from 'ink'; import { Colors } from '../../colors.js'; import { SCREEN_READER_USER_PREFIX } from '../../constants.js'; +import { isSlashCommand as checkIsSlashCommand } from '../../utils/commandUtils.js'; interface UserMessageProps { text: string; @@ -16,7 +17,7 @@ interface UserMessageProps { export const UserMessage: React.FC = ({ text }) => { const prefix = '> '; const prefixWidth = prefix.length; - const isSlashCommand = text.startsWith('/'); + const isSlashCommand = checkIsSlashCommand(text); const textColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray; const borderColor = isSlashCommand ? Colors.AccentPurple : Colors.Gray; diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts index 543de584..5274d3d5 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts @@ -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', + ); + }); + }); }); diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.tsx b/packages/cli/src/ui/hooks/useCommandCompletion.tsx index e29a8860..398e3daa 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.tsx +++ b/packages/cli/src/ui/hooks/useCommandCompletion.tsx @@ -134,7 +134,7 @@ export function useCommandCompletion( if ( isPromptCompletionEnabled && trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH && - !trimmedText.startsWith('/') && + !isSlashCommand(trimmedText) && !trimmedText.includes('@') ) { return { diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 4ddb413b..4928b521 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -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', () => { diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 757396b7..b8004edb 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -40,7 +40,7 @@ import type { SlashCommandProcessorResult, } 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 { handleAtCommand } from './atCommandProcessor.js'; import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js'; @@ -271,7 +271,9 @@ export const useGeminiStream = ( await logger?.logMessage(MessageSenderType.USER, trimmedQuery); // Handle UI-only commands first - const slashCommandResult = await handleSlashCommand(trimmedQuery); + const slashCommandResult = isSlashCommand(trimmedQuery) + ? await handleSlashCommand(trimmedQuery) + : false; if (slashCommandResult) { switch (slashCommandResult.type) { diff --git a/packages/cli/src/ui/hooks/usePromptCompletion.ts b/packages/cli/src/ui/hooks/usePromptCompletion.ts index e7076a73..77a400cb 100644 --- a/packages/cli/src/ui/hooks/usePromptCompletion.ts +++ b/packages/cli/src/ui/hooks/usePromptCompletion.ts @@ -12,6 +12,7 @@ import { } from '@google/gemini-cli-core'; import type { Content, GenerateContentConfig } from '@google/genai'; 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_DEBOUNCE_MS = 250; @@ -81,7 +82,7 @@ export function usePromptCompletion({ if ( trimmedText.length < PROMPT_COMPLETION_MIN_LENGTH || !geminiClient || - trimmedText.startsWith('/') || + isSlashCommand(trimmedText) || trimmedText.includes('@') || !isPromptCompletionEnabled ) { @@ -237,7 +238,7 @@ export function usePromptCompletion({ const trimmedText = buffer.text.trim(); return ( trimmedText.length >= PROMPT_COMPLETION_MIN_LENGTH && - !trimmedText.startsWith('/') && + !isSlashCommand(trimmedText) && !trimmedText.includes('@') ); }, [buffer.text, isPromptCompletionEnabled, isCursorAtEnd]); diff --git a/packages/cli/src/ui/utils/commandUtils.test.ts b/packages/cli/src/ui/utils/commandUtils.test.ts index 5e5be00e..b48bb4c9 100644 --- a/packages/cli/src/ui/utils/commandUtils.test.ts +++ b/packages/cli/src/ui/utils/commandUtils.test.ts @@ -102,6 +102,20 @@ describe('commandUtils', () => { expect(isSlashCommand('path/to/file')).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', () => { diff --git a/packages/cli/src/ui/utils/commandUtils.ts b/packages/cli/src/ui/utils/commandUtils.ts index 2912899e..32bebceb 100644 --- a/packages/cli/src/ui/utils/commandUtils.ts +++ b/packages/cli/src/ui/utils/commandUtils.ts @@ -21,12 +21,28 @@ export const isAtCommand = (query: string): boolean => /** * 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. * @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 export const copyToClipboard = async (text: string): Promise => {