From 276b67083a3acaacf8856d93feead06cc048fd31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 12 May 2026 22:56:42 +0800 Subject: [PATCH] Align file mention search with CLI path display Desktop search results were visually misleading when many directories shared the same basename, because the UI emphasized the basename and the server ranking allowed deep basename matches to outrank direct path-prefix matches. This makes search rows show the insertable relative path and ranks direct path-prefix matches before unrelated same-name directories. Constraint: Desktop @ file suggestions should match the CLI mental model for path search. Rejected: Keep basename plus parent-path rows | it hides the distinction between src/, src/hooks/, and desktop/src in the primary text. Confidence: high Scope-risk: narrow Directive: Preserve path-prefix ranking ahead of basename-only matches for directory searches. Tested: bun test src/server/__tests__/filesystem.test.ts Tested: cd desktop && bun run test -- FileSearchMenu.test.tsx ChatInput.test.tsx Tested: bun run check:server Tested: bun run check:desktop --- .../src/components/chat/ChatInput.test.tsx | 2 +- .../components/chat/FileSearchMenu.test.tsx | 31 +++++++++++++-- .../src/components/chat/FileSearchMenu.tsx | 38 +++++++++++++++---- src/server/__tests__/filesystem.test.ts | 25 ++++++++++++ src/server/api/filesystem.ts | 20 ++++++++-- 5 files changed, 100 insertions(+), 16 deletions(-) diff --git a/desktop/src/components/chat/ChatInput.test.tsx b/desktop/src/components/chat/ChatInput.test.tsx index 80185257..cd9953e2 100644 --- a/desktop/src/components/chat/ChatInput.test.tsx +++ b/desktop/src/components/chat/ChatInput.test.tsx @@ -520,7 +520,7 @@ describe('ChatInput file mentions', () => { }, }) - fireEvent.click(await screen.findByText('conditions.py')) + fireEvent.click(await screen.findByText('backend/src/conditions.py')) await waitFor(() => { expect(input.value).toBe('记一下这个文件讲了什么东西。') diff --git a/desktop/src/components/chat/FileSearchMenu.test.tsx b/desktop/src/components/chat/FileSearchMenu.test.tsx index b5a70cef..e44f4569 100644 --- a/desktop/src/components/chat/FileSearchMenu.test.tsx +++ b/desktop/src/components/chat/FileSearchMenu.test.tsx @@ -91,7 +91,7 @@ describe('FileSearchMenu', () => { />, ) - fireEvent.click(await screen.findByText('backend')) + fireEvent.click(await screen.findByText('backend/')) expect(onSelect).toHaveBeenCalledWith('/repo/backend', 'backend', true) expect(onNavigate).not.toHaveBeenCalled() @@ -132,11 +132,36 @@ describe('FileSearchMenu', () => { />, ) - fireEvent.click(await screen.findByText('pictactic')) + fireEvent.click(await screen.findByText('backend/src/pictactic')) expect(onSelect).toHaveBeenCalledWith('/repo/backend/src/pictactic', 'backend/src/pictactic', false) }) + it('renders search results as insertable paths instead of repeated basenames', async () => { + vi.mocked(filesystemApi.search).mockResolvedValueOnce({ + currentPath: '/repo', + parentPath: '/', + query: 'src', + entries: [ + { name: 'src', path: '/repo/src', relativePath: 'src', isDirectory: true }, + { name: 'hooks', path: '/repo/src/hooks', relativePath: 'src/hooks', isDirectory: true }, + { name: 'src', path: '/repo/desktop/src', relativePath: 'desktop/src', isDirectory: true }, + ], + }) + + render( + {}} + />, + ) + + expect(await screen.findByText('src/')).toBeInTheDocument() + expect(screen.getByText('src/hooks/')).toBeInTheDocument() + expect(screen.getByText('desktop/src/')).toBeInTheDocument() + }) + it('uses the resolved home root for typed folder filters when no workspace is selected', async () => { vi.mocked(filesystemApi.search).mockResolvedValueOnce({ currentPath: '/Users/nanmi', @@ -160,7 +185,7 @@ describe('FileSearchMenu', () => { />, ) - expect(await screen.findByText('workspace')).toBeInTheDocument() + expect(await screen.findByText('workspace/')).toBeInTheDocument() rerender( (({ cwd, fi return entry.name }, [cwd, rootPath]) + const getDisplayPath = useCallback((entry: DirEntry) => { + const relativePath = getRelativePath(entry).replace(/\\/g, '/') + if (!entry.isDirectory) return relativePath + return `${relativePath.replace(/\/+$/, '')}/` + }, [getRelativePath]) + const selectEntry = useCallback((entry: DirEntry) => { onSelect(entry.path, getRelativePath(entry), entry.isDirectory) }, [getRelativePath, onSelect]) @@ -194,6 +200,7 @@ export const FileSearchMenu = forwardRef(({ cwd, fi const renderEntry = (entry: DirEntry, index: number) => { const relativePath = getRelativePath(entry) + const displayPath = getDisplayPath(entry) const parentPath = relativePath.split('/').slice(0, -1).join('/') const selected = selectedIndex === index return ( @@ -208,7 +215,9 @@ export const FileSearchMenu = forwardRef(({ cwd, fi {entry.isDirectory ? (