77 */
88
99import * as React from 'react'
10- import { FileText , FileImage , FileVideo , FileCode } from 'lucide-react'
1110import { cn } from '@/lib/utils'
11+ import { FileTypeIcon } from '@/components/file-browser/FileTypeIcon'
1212
1313/** 图片扩展名 */
1414const IMAGE_EXTS = new Set ( [ 'png' , 'jpg' , 'jpeg' , 'gif' , 'webp' , 'svg' , 'bmp' ] )
@@ -52,17 +52,20 @@ function getExtension(filename: string): string {
5252 return filename . slice ( dot + 1 ) . toLowerCase ( )
5353}
5454
55- /** 根据扩展名获取文件图标 */
56- function getFileIcon ( ext : string ) : React . ReactElement {
57- const iconClass = 'size-3 shrink-0'
58- if ( IMAGE_EXTS . has ( ext ) ) return < FileImage className = { iconClass } />
59- if ( VIDEO_EXTS . has ( ext ) ) return < FileVideo className = { iconClass } />
60- if ( CODE_EXTS . has ( ext ) ) return < FileCode className = { iconClass } />
61- return < FileText className = { iconClass } />
55+ /**
56+ * 从路径中剥离末尾的行号/列号后缀(如 :42 或 :42:15)
57+ * Agent 模式下模型常输出 file_path:line_number 格式
58+ */
59+ function stripLineCol ( filePath : string ) : { path : string ; suffix : string } {
60+ const m = filePath . match ( / ^ ( .+ ?) ( : \d + (?: : \d + ) ? ) $ / )
61+ if ( m && ! m [ 1 ] ! . endsWith ( ':' ) ) {
62+ return { path : m [ 1 ] ! , suffix : m [ 2 ] ! }
63+ }
64+ return { path : filePath , suffix : '' }
6265}
6366
6467interface FilePathChipProps {
65- /** 文件路径(绝对或相对) */
68+ /** 文件路径(绝对或相对,可能带行号后缀 ) */
6669 filePath : string
6770 /** 基础目录路径(向后兼容,单值) */
6871 basePath ?: string
@@ -73,11 +76,12 @@ interface FilePathChipProps {
7376
7477/** 文件路径芯片 — 可点击,触发文件预览 */
7578export function FilePathChip ( { filePath, basePath, basePaths, className } : FilePathChipProps ) : React . ReactElement {
76- const filename = getFileName ( filePath )
77- const ext = getExtension ( filename )
78-
7979 const trimmedPath = filePath . trim ( )
80- const isAbsolute = trimmedPath . startsWith ( '/' ) || / ^ [ A - Z ] : \\ / . test ( trimmedPath )
80+ const { path : cleanPath , suffix : lineColSuffix } = stripLineCol ( trimmedPath )
81+
82+ const filename = getFileName ( cleanPath )
83+
84+ const isAbsolute = cleanPath . startsWith ( '/' ) || / ^ [ A - Z ] : \\ / . test ( cleanPath )
8185
8286 // 候选基础目录列表:优先使用 basePaths;否则退化到 basePath 单值
8387 const candidateBases = React . useMemo < string [ ] > ( ( ) => {
@@ -91,19 +95,17 @@ export function FilePathChip({ filePath, basePath, basePaths, className }: FileP
9195 if ( isAbsolute ) return trimmedPath
9296 if ( candidateBases . length > 0 ) {
9397 const base = candidateBases [ 0 ] !
94- return base . endsWith ( '/' ) ? `${ base } ${ trimmedPath } ` : `${ base } /${ trimmedPath } `
98+ return base . endsWith ( '/' ) ? `${ base } ${ cleanPath } ` : `${ base } /${ cleanPath } `
9599 }
96100 return trimmedPath
97- } , [ trimmedPath , isAbsolute , candidateBases ] )
101+ } , [ trimmedPath , cleanPath , isAbsolute , candidateBases ] )
98102
99103 const handleClick = React . useCallback ( ( ) => {
100- // 绝对路径直接预览;相对路径把候选 basePaths 交给主进程依次尝试
101- const target = isAbsolute ? trimmedPath : trimmedPath
102- const bases = isAbsolute ? undefined : ( candidateBases . length > 0 ? candidateBases : undefined )
103- window . electronAPI . previewFile ( target , bases ) . catch ( ( error : unknown ) => {
104+ const bases = candidateBases . length > 0 ? candidateBases : undefined
105+ window . electronAPI . previewFile ( cleanPath , bases ) . catch ( ( error : unknown ) => {
104106 console . error ( '[FilePathChip] 预览文件失败:' , error )
105107 } )
106- } , [ trimmedPath , isAbsolute , candidateBases ] )
108+ } , [ cleanPath , candidateBases ] )
107109
108110 return (
109111 < button
@@ -118,8 +120,8 @@ export function FilePathChip({ filePath, basePath, basePaths, className }: FileP
118120 className
119121 ) }
120122 >
121- { getFileIcon ( ext ) }
122- < span className = "truncate max-w-[240px]" > { filename } </ span >
123+ < FileTypeIcon name = { filename } isDirectory = { false } size = { 14 } />
124+ < span className = "truncate max-w-[240px]" > { filename } { lineColSuffix } </ span >
123125 </ button >
124126 )
125127}
@@ -135,15 +137,18 @@ export function isAbsoluteFilePath(text: string): boolean {
135137 const trimmed = text . trim ( )
136138 if ( trimmed . length < 2 ) return false
137139
140+ // 剥离末尾行号后缀再检测
141+ const { path : clean } = stripLineCol ( trimmed )
142+
138143 // macOS/Linux 绝对路径:以 / 开头,至少两级
139- if ( trimmed . startsWith ( '/' ) && / ^ \/ [ ^ \n ] + \/ [ ^ \n ] + $ / . test ( trimmed ) ) {
144+ if ( clean . startsWith ( '/' ) && / ^ \/ [ ^ \n ] + \/ [ ^ \n ] + $ / . test ( clean ) ) {
140145 // 排除常见的非路径模式(如 /regex/ 模式)
141- if ( trimmed . endsWith ( '/' ) && ! trimmed . includes ( '.' ) ) return false
146+ if ( clean . endsWith ( '/' ) && ! clean . includes ( '.' ) ) return false
142147 return true
143148 }
144149
145150 // Windows 绝对路径
146- if ( / ^ [ A - Z ] : \\ / . test ( trimmed ) ) return true
151+ if ( / ^ [ A - Z ] : \\ / . test ( clean ) ) return true
147152
148153 return false
149154}
@@ -160,16 +165,19 @@ export function isRelativeFilePath(text: string): boolean {
160165 const trimmed = text . trim ( )
161166 if ( trimmed . length < 3 ) return false
162167
168+ // 剥离末尾行号后缀再检测
169+ const { path : clean } = stripLineCol ( trimmed )
170+
163171 // 提取扩展名
164- const ext = getExtension ( trimmed )
172+ const ext = getExtension ( clean )
165173 if ( ! ext || ! ALL_PREVIEWABLE_EXTS . has ( ext ) ) return false
166174
167175 // 必须看起来像文件路径:允许 字母数字、点、横线、下划线、斜杠
168176 // 排除含空格或特殊字符的(太可能是其他内容)
169- if ( ! / ^ [ \w . / @ - ] + $ / . test ( trimmed ) ) return false
177+ if ( ! / ^ [ \w . / @ - ] + $ / . test ( clean ) ) return false
170178
171179 // 排除以点开头的隐藏文件(如 .gitignore),但保留含子路径的目录相对路径(如 .context/file.md)
172- if ( trimmed . startsWith ( '.' ) && ! trimmed . startsWith ( './' ) && ! trimmed . includes ( '/' ) ) return false
180+ if ( clean . startsWith ( '.' ) && ! clean . startsWith ( './' ) && ! clean . includes ( '/' ) ) return false
173181
174182 return true
175183}
0 commit comments