Skip to content

Commit af6312f

Browse files
fix: @ 文件引用 review 反馈 + 代码简化
review 改动: - 修复 matchEntries 模糊匹配 bug(nameLower[i] 原本与整个 q 比较,应为 q[qi]) - 修复 remarkMentions 中 mValue 的 TS 类型错误(?? 链 + 默认值) - 移除 file-mention-suggestion 的 any 类型,改为 SuggestionProps<FileIndexEntry> - 移除 file-mention-suggestion 两处调试 console.log 交互增强: - 鼠标双击目录选中并插入 @ 引用(180ms 延迟判定单/双击) 代码简化: - 合并 SessionSection / WorkspaceSection 为单个 FileSection(label) - 简化 splitEntries,移除永不命中的 fallback dead code - 空状态移除多余的 TooltipProvider / MentionErrorBoundary 包裹
1 parent f16e1e8 commit af6312f

4 files changed

Lines changed: 53 additions & 64 deletions

File tree

apps/electron/src/main/ipc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2038,7 +2038,7 @@ export function registerIpcHandlers(): void {
20382038
if (nameLower.includes(q) || pathLower.includes(q)) return true
20392039
let qi = 0
20402040
for (let i = 0; i < nameLower.length && qi < q.length; i++) {
2041-
if (nameLower[i] === q) qi++
2041+
if (nameLower[i] === q[qi]) qi++
20422042
}
20432043
return qi === q.length
20442044
})

apps/electron/src/renderer/components/ai-elements/message.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export function remarkMentions() {
306306
parts.push({ type: 'text', value: text.slice(lastIdx, m.index) })
307307
}
308308
const mType: MentionType = m[1] ? 'file' : m[2] ? 'skill' : 'mcp'
309-
const mValue = m[1] || m[2] || m[3]
309+
const mValue = m[1] ?? m[2] ?? m[3] ?? ''
310310
// 新版 htmlToMarkdown 已 encodeURIComponent,旧消息是原始路径
311311
const alreadyEncoded = /%[0-9A-Fa-f]{2}/.test(mValue)
312312
const safeValue = alreadyEncoded ? mValue : encodeURIComponent(mValue)

apps/electron/src/renderer/components/file-browser/FileMentionList.tsx

Lines changed: 46 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* 交互:
1212
* - 文件夹初始折叠,Tab 键展开/折叠,→/← 方向键辅助
1313
* - 任何时候按 Enter 完成 @ 引用(文件或目录均可)
14+
* - 鼠标单击文件夹:展开/折叠;双击文件夹:选中并插入 @ 引用
1415
*/
1516

1617
import * as React from 'react'
@@ -299,13 +300,9 @@ export const FileMentionList = React.forwardRef<FileMentionRef, FileMentionListP
299300
// 无匹配结果
300301
if (!hasResults) {
301302
return (
302-
<TooltipProvider>
303-
<MentionErrorBoundary>
304-
<div className="rounded-lg border bg-popover p-2 shadow-lg text-[11px] text-muted-foreground">
305-
无匹配文件
306-
</div>
307-
</MentionErrorBoundary>
308-
</TooltipProvider>
303+
<div className="rounded-lg border bg-popover p-2 shadow-lg text-[11px] text-muted-foreground">
304+
无匹配文件
305+
</div>
309306
)
310307
}
311308

@@ -318,7 +315,8 @@ export const FileMentionList = React.forwardRef<FileMentionRef, FileMentionListP
318315
>
319316
{/* 会话文件 */}
320317
{hasSession && (
321-
<SessionSection
318+
<FileSection
319+
label="会话文件"
322320
tree={sessionTreeWithState}
323321
selectedIndex={selectedIndex}
324322
baseIndex={0}
@@ -330,7 +328,8 @@ export const FileMentionList = React.forwardRef<FileMentionRef, FileMentionListP
330328

331329
{/* 工作区文件 */}
332330
{hasWorkspace && (
333-
<WorkspaceSection
331+
<FileSection
332+
label="工作区文件"
334333
tree={workspaceTreeWithState}
335334
selectedIndex={selectedIndex}
336335
baseIndex={sessionVisible.length}
@@ -348,15 +347,17 @@ export const FileMentionList = React.forwardRef<FileMentionRef, FileMentionListP
348347

349348
// ===== 子组件 =====
350349

351-
/** 会话文件区域 */
352-
function SessionSection({
350+
/** 分组区域(会话文件 / 工作区文件) */
351+
function FileSection({
352+
label,
353353
tree,
354354
selectedIndex,
355355
baseIndex,
356356
onSelect,
357357
onToggle,
358358
setSelectedIndex,
359359
}: {
360+
label: string
360361
tree: FileTreeNode[]
361362
selectedIndex: number
362363
baseIndex: number
@@ -368,41 +369,7 @@ function SessionSection({
368369
<div>
369370
<div className="flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-medium bg-primary/10 text-primary border-b border-border/50">
370371
<Folder className="size-3" />
371-
<span>会话文件</span>
372-
</div>
373-
<TreeNodeList
374-
nodes={tree}
375-
selectedIndex={selectedIndex}
376-
baseIndex={baseIndex}
377-
onSelect={onSelect}
378-
onToggle={onToggle}
379-
setSelectedIndex={setSelectedIndex}
380-
/>
381-
</div>
382-
)
383-
}
384-
385-
/** 工作区文件区域 */
386-
function WorkspaceSection({
387-
tree,
388-
selectedIndex,
389-
baseIndex,
390-
onSelect,
391-
onToggle,
392-
setSelectedIndex,
393-
}: {
394-
tree: FileTreeNode[]
395-
selectedIndex: number
396-
baseIndex: number
397-
onSelect: (node: FileTreeNode) => void
398-
onToggle: (path: string) => void
399-
setSelectedIndex: (index: number) => void
400-
}) {
401-
return (
402-
<div>
403-
<div className="flex items-center gap-1.5 px-2.5 py-1.5 text-[11px] font-medium bg-primary/10 text-primary border-b border-border/50">
404-
<Folder className="size-3" />
405-
<span>工作区文件</span>
372+
<span>{label}</span>
406373
</div>
407374
<TreeNodeList
408375
nodes={tree}
@@ -434,6 +401,33 @@ function TreeNodeList({
434401
}) {
435402
let offset = 0
436403

404+
// 双击检测:单击目录时延迟触发 toggle,等待可能的双击
405+
const clickTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
406+
React.useEffect(() => {
407+
return () => {
408+
if (clickTimerRef.current) clearTimeout(clickTimerRef.current)
409+
}
410+
}, [])
411+
412+
function handleDirClick(node: FileTreeNode) {
413+
if (clickTimerRef.current) {
414+
clearTimeout(clickTimerRef.current)
415+
clickTimerRef.current = null
416+
}
417+
clickTimerRef.current = setTimeout(() => {
418+
onToggle(node.path)
419+
clickTimerRef.current = null
420+
}, 180)
421+
}
422+
423+
function handleDirDoubleClick(node: FileTreeNode) {
424+
if (clickTimerRef.current) {
425+
clearTimeout(clickTimerRef.current)
426+
clickTimerRef.current = null
427+
}
428+
onSelect(node)
429+
}
430+
437431
function renderNode(node: FileTreeNode): React.ReactElement {
438432
const idx = baseIndex + offset
439433
const isSelected = idx === selectedIndex
@@ -457,11 +451,16 @@ function TreeNodeList({
457451
onClick={() => {
458452
setSelectedIndex(idx)
459453
if (node.type === 'dir') {
460-
onToggle(node.path)
454+
handleDirClick(node)
461455
} else {
462456
onSelect(node)
463457
}
464458
}}
459+
onDoubleClick={() => {
460+
if (node.type === 'dir') {
461+
handleDirDoubleClick(node)
462+
}
463+
}}
465464
>
466465
{/* 目录展开/折叠箭头 */}
467466
{node.type === 'dir' && node.children.length > 0 ? (

apps/electron/src/renderer/components/file-browser/file-mention-suggestion.tsx

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import type React from 'react'
1010
import { ReactRenderer } from '@tiptap/react'
11-
import type { SuggestionOptions } from '@tiptap/suggestion'
11+
import type { SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'
1212
import { FileMentionList } from './FileMentionList'
1313
import type { FileMentionRef } from './FileMentionList'
1414
import type { FileIndexEntry, FileSearchResult } from '@proma/shared'
@@ -38,15 +38,13 @@ export function createFileMentionSuggestion(
3838
const additionalPaths = attachedDirsRef?.current ?? []
3939
const sessionPaths = sessionAttachedDirsRef?.current ?? []
4040

41-
console.log('[FileMention] searching files, query:', JSON.stringify(query), 'ws:', wsPath, 'additionalPaths:', additionalPaths, 'sessionPaths:', sessionPaths)
4241
const result = await window.electronAPI.searchWorkspaceFiles(
4342
wsPath,
4443
query ?? '',
4544
20,
4645
additionalPaths.length > 0 ? additionalPaths : undefined,
4746
sessionPaths.length > 0 ? sessionPaths : undefined,
4847
)
49-
console.log('[FileMention] search result:', { total: result.total, sessionCount: result.sessionEntries.length, workspaceCount: result.workspaceEntries.length })
5048
lastResult = result
5149
return result.entries
5250
} catch(e) {
@@ -62,21 +60,13 @@ export function createFileMentionSuggestion(
6260
let resizeObserver: ResizeObserver | null = null
6361

6462
function splitEntries(result: FileSearchResult | null) {
65-
let sessionEntries = result?.sessionEntries ?? []
66-
let workspaceEntries = result?.workspaceEntries ?? []
67-
if (sessionEntries.length === 0 && workspaceEntries.length === 0 && (result?.entries.length ?? 0) > 0) {
68-
const hasSource = result!.entries.some((e) => 'source' in e && e.source)
69-
if (hasSource) {
70-
sessionEntries = result!.entries.filter((e) => e.source === 'session')
71-
workspaceEntries = result!.entries.filter((e) => e.source === 'workspace')
72-
} else {
73-
sessionEntries = result!.entries
74-
}
63+
return {
64+
sessionEntries: result?.sessionEntries ?? [],
65+
workspaceEntries: result?.workspaceEntries ?? [],
7566
}
76-
return { sessionEntries, workspaceEntries }
7767
}
7868

79-
function createRenderer(props: any) {
69+
function createRenderer(props: SuggestionProps<FileIndexEntry>) {
8070
const { sessionEntries, workspaceEntries } = splitEntries(lastResult)
8171
renderer = new ReactRenderer(FileMentionList, {
8272
props: {

0 commit comments

Comments
 (0)