From 3b2d50fad6c5b1aa84951d1c16a96c1aa72a89f6 Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Fri, 27 Mar 2026 10:47:55 +0800 Subject: [PATCH] fix: @ file search stops working after selecting a slash command (#2518) --- .../src/ui/hooks/useCommandCompletion.test.ts | 89 +++++++++++++++++++ .../cli/src/ui/hooks/useCommandCompletion.tsx | 19 ++-- 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts index 659b99db0..fed160343 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts @@ -417,6 +417,95 @@ describe('useCommandCompletion', () => { }); }); + describe('Completion mode detection', () => { + it('should switch to AT mode when typing @ after a slash command (#2518)', async () => { + setupMocks({ + atSuggestions: [{ label: 'src/file.txt', value: 'src/file.txt' }], + }); + + const text = '/qc:create-issue @file'; + renderHook(() => + useCommandCompletion( + useTextBufferForTest(text), + testDirs, + testRootDir, + [], + mockCommandContext, + false, + mockConfig, + ), + ); + + await waitFor(() => { + expect(useAtCompletion).toHaveBeenLastCalledWith( + expect.objectContaining({ + enabled: true, + pattern: 'file', + }), + ); + }); + }); + + it('should remain in SLASH mode when no @ is typed after slash command', async () => { + setupMocks({ + slashSuggestions: [{ label: 'help', value: 'help' }], + }); + + const text = '/help'; + renderHook(() => + useCommandCompletion( + useTextBufferForTest(text), + testDirs, + testRootDir, + [], + mockCommandContext, + false, + mockConfig, + ), + ); + + await waitFor(() => { + expect(useSlashCompletion).toHaveBeenLastCalledWith( + expect.objectContaining({ + enabled: true, + query: '/help', + }), + ); + }); + }); + + it('should complete a file path when @ appears after a slash command', async () => { + setupMocks({ + atSuggestions: [{ label: 'src/index.ts', value: 'src/index.ts' }], + }); + + const text = '/review @src/ind'; + const { result } = renderHook(() => { + const textBuffer = useTextBufferForTest(text); + const completion = useCommandCompletion( + textBuffer, + testDirs, + testRootDir, + [], + mockCommandContext, + false, + mockConfig, + ); + return { ...completion, textBuffer }; + }); + + await waitFor(() => { + expect(result.current.suggestions.length).toBe(1); + }); + + act(() => { + result.current.handleAutocomplete(0); + }); + + expect(result.current.textBuffer.text).toBe('/review @src/index.ts '); + }); + }); + describe('handleAutocomplete', () => { it('should complete a partial command', async () => { setupMocks({ diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.tsx b/packages/cli/src/ui/hooks/useCommandCompletion.tsx index cb5d9f276..c78e9e46e 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.tsx +++ b/packages/cli/src/ui/hooks/useCommandCompletion.tsx @@ -74,15 +74,9 @@ export function useCommandCompletion( const { completionMode, query, completionStart, completionEnd } = useMemo(() => { const currentLine = buffer.lines[cursorRow] || ''; - if (cursorRow === 0 && isSlashCommand(currentLine.trim())) { - return { - completionMode: CompletionMode.SLASH, - query: currentLine, - completionStart: 0, - completionEnd: currentLine.length, - }; - } + // Check for @ completion first, so that typing @ after a slash command + // still triggers file search (see #2518). const codePoints = toCodePoints(currentLine); for (let i = cursorCol - 1; i >= 0; i--) { const char = codePoints[i]; @@ -121,6 +115,15 @@ export function useCommandCompletion( } } + if (cursorRow === 0 && isSlashCommand(currentLine.trim())) { + return { + completionMode: CompletionMode.SLASH, + query: currentLine, + completionStart: 0, + completionEnd: currentLine.length, + }; + } + return { completionMode: CompletionMode.IDLE, query: null,