fix(web): display Windows file search paths correctly (#1311)

* fix(web): display Windows file search paths correctly

* fix(hub): scope path normalization to Windows
This commit is contained in:
Ananovo
2026-08-02 20:04:46 +08:00
committed by GitHub
parent a6f302ebd1
commit fb6f697555
3 changed files with 76 additions and 7 deletions
+61
View File
@@ -90,4 +90,65 @@ describe('file search route', () => {
]
})
})
it('normalizes ripgrep path separators before deriving file names and directories', async () => {
const session = {
id: 'session-1',
namespace: 'default',
active: true,
metadata: { path: 'C:\\project' }
} as unknown as Session
const engine = {
resolveSessionAccess: () => ({ ok: true as const, sessionId: 'session-1', session }),
runRipgrep: async () => ({
success: true,
stdout: 'src\\nested\\file.ts\nroot.ts\n'
}),
statFiles: async (_sessionId: string, paths: string[]) => ({
success: true,
entries: paths.map((path) => ({ path, size: 10, modified: 100 }))
})
} as unknown as Partial<SyncEngine>
const response = await buildApp(engine).request('/api/sessions/session-1/files?query=.ts')
expect(response.status).toBe(200)
expect(await response.json()).toEqual({
success: true,
files: [
{ fileName: 'file.ts', filePath: 'src/nested', fullPath: 'src/nested/file.ts', fileType: 'file', size: 10, modified: 100 },
{ fileName: 'root.ts', filePath: '', fullPath: 'root.ts', fileType: 'file', size: 10, modified: 100 },
]
})
})
it('preserves backslashes in file names for non-Windows sessions', async () => {
const session = {
id: 'session-1',
namespace: 'default',
active: true,
metadata: { path: '/project' }
} as unknown as Session
const engine = {
resolveSessionAccess: () => ({ ok: true as const, sessionId: 'session-1', session }),
runRipgrep: async () => ({
success: true,
stdout: 'src/file\\name.ts\n'
}),
statFiles: async (_sessionId: string, paths: string[]) => ({
success: true,
entries: paths.map((path) => ({ path, size: 10, modified: 100 }))
})
} as unknown as Partial<SyncEngine>
const response = await buildApp(engine).request('/api/sessions/session-1/files?query=.ts')
expect(response.status).toBe(200)
expect(await response.json()).toEqual({
success: true,
files: [
{ fileName: 'file\\name.ts', filePath: 'src', fullPath: 'src/file\\name.ts', fileType: 'file', size: 10, modified: 100 },
]
})
})
})
+12
View File
@@ -21,6 +21,14 @@ const generatedImageSchema = z.object({
imageId: z.string().min(1)
})
function normalizeFileSearchPath(path: string): string {
return path.replaceAll('\\', '/')
}
function isWindowsSessionPath(path: string): boolean {
return /^[A-Za-z]:[\\/]/.test(path) || path.startsWith('\\\\')
}
function parseBooleanParam(value: string | undefined): boolean | undefined {
if (value === 'true') return true
if (value === 'false') return false
@@ -227,10 +235,14 @@ export function createGitRoutes(getSyncEngine: () => SyncEngine | null): Hono<We
}
const stdout = result.stdout ?? ''
const normalizePath = isWindowsSessionPath(sessionPath)
? normalizeFileSearchPath
: (path: string) => path
const paths = stdout
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map(normalizePath)
.slice(0, limit)
const metadataResult = await runRpc(() => engine.statFiles(sessionResult.sessionId, paths))
+3 -7
View File
@@ -260,8 +260,7 @@ function SearchResultRow(props: {
onOpen: () => void
showDivider: boolean
}) {
const { t, locale } = useTranslation()
const subtitle = getProjectRootLabel(props.file.filePath, t)
const { locale } = useTranslation()
const metadata = formatFileMetadata(props.file.size, props.file.modified, locale)
const icon = props.file.fileType === 'file'
? <FileIcon fileName={props.file.fileName} size={22} />
@@ -275,11 +274,8 @@ function SearchResultRow(props: {
>
{icon}
<div className="min-w-0 flex-1">
<div className="truncate font-medium">{props.file.fileName}</div>
<div className="flex min-w-0 items-center gap-2 text-xs text-[var(--app-hint)]">
<span className="truncate">{subtitle}</span>
{metadata ? <span className="shrink-0">{metadata}</span> : null}
</div>
<div className="truncate font-medium">{props.file.fullPath}</div>
{metadata ? <div className="text-xs text-[var(--app-hint)]">{metadata}</div> : null}
</div>
</button>
)