fix: auto-close panel when no commands match (#7824) (#8784)

* fix(QuickPanel): auto-close panel when no commands match (#7824)

Fixes the issue where QuickPanel remains visible when user types
invalid slash commands. Now the panel intelligently closes after
300ms when no matching commands are found.

- Add smart delayed closing mechanism for unmatched searches
- Optimize memory management with proper timer cleanup
- Preserve existing trigger behavior for / and @ symbols

* feat(inputbar): intelligent @ symbol handling on model selection panel close

- Add smart @ character deletion when user selects models and closes panel with ESC/Backspace
- Preserve @ character when user closes panel without selecting any models
- Implement action tracking using useRef to detect user model interactions
- Support both ESC key and Backspace key for consistent behavior
- Use React setState instead of DOM manipulation for proper state management

Resolves user experience issue where @ symbol always remained after closing model selection panel

* perf(QuickPanel): optimize timer management and fix React anti-patterns

- Move side effects from useMemo to useEffect for proper React lifecycle
- Add automatic timer cleanup on component unmount and dependency changes
- Remove unnecessary timer creation/destruction on each search input
- Improve memory management and prevent potential memory leaks
- Maintain existing smart auto-close functionality with better performance

Fixes React anti-pattern where side effects were executed in useMemo,
which should be a pure function. This improves performance especially
when users type quickly in the search input.

* refactor(QuickPanel): remove redundant timer cleanup useEffect

Remove duplicate timer cleanup logic as the existing useEffect at line 141-164
already handles component unmount cleanup properly.

* refactor(QuickPanel): optimize useEffect dependencies and timer cleanup logic

- Replace overly broad `ctx` dependency with precise `[ctx.isVisible, searchText, list.length, ctx.close]`
- Move timer cleanup before visibility check to ensure proper cleanup on panel hide
- Add early return when panel is invisible to prevent unnecessary timer creation
- Improve performance by avoiding redundant effect executions
- Fix edge case where timers might not be cleared when panel becomes invisible

Addresses review feedback about dependency array optimization while maintaining
existing auto-close functionality and improving memory management.

* feat(QuickPanel): implement smart re-opening with dependency optimization

Features:
- Implement smart re-opening during deletion with real-time matching
- Only reopen panel when actual matches exist to avoid unnecessary interactions
- Add intelligent @ symbol handling on model selection panel close
- Optimize search text length limits (≤10 chars) for performance

Fixes:
- Fix useMemo dependency from overly broad [ctx, searchText] to precise [ctx.isVisible, ctx.symbol, ctx.list, searchText]
- Resolve trailing whitespace formatting issues
- Eliminate ESLint exhaustive-deps warnings while maintaining stability
- Prevent unnecessary re-renders when unrelated ctx properties change

Performance improvements ensure optimal QuickPanel responsiveness while maintaining
existing auto-close functionality and improving user experience.

* fix(ci): add eslint-disable comment for exhaustive-deps warning

The useEffect dependency array [ctx.isVisible, searchText, list.length, ctx.close]
is intentionally precise to avoid unnecessary re-renders when unrelated ctx
properties change. Adding ctx object would cause performance degradation.

* refactor(QuickPanel): remove smart re-opening logic during deletion

- Remove 62 lines of complex deletion detection logic from Inputbar component
- Eliminates performance overhead from frequent string matching during typing
- Reduces code complexity and potential edge cases
- Maintains simple and predictable QuickPanel behavior
- Improves maintainability by removing unnecessary "smart" features

The deletion-triggered smart reopening feature added unnecessary complexity
without significant user benefit. Users can simply type / or @ again to
reopen panels when needed.
This commit is contained in:
Jason Young 2025-08-14 16:35:14 +08:00 committed by GitHub
parent a4c61bcd66
commit 1bf380a921
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 72 additions and 6 deletions

View File

@ -66,6 +66,9 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
const prevSearchTextRef = useRef('')
const prevSymbolRef = useRef('')
// 无匹配项自动关闭的定时器
const noMatchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
// 处理搜索,过滤列表
const list = useMemo(() => {
if (!ctx.isVisible && !ctx.symbol) return []
@ -128,12 +131,44 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
prevSymbolRef.current = ctx.symbol
return newList
}, [ctx.isVisible, ctx.list, ctx.symbol, searchText])
}, [ctx.isVisible, ctx.symbol, ctx.list, searchText])
const canForwardAndBackward = useMemo(() => {
return list.some((item) => item.isMenu) || historyPanel.length > 0
}, [list, historyPanel])
// 智能关闭逻辑:当有搜索文本但无匹配项时,延迟关闭面板
useEffect(() => {
const _searchText = searchText.replace(/^[/@]/, '')
// 清除之前的定时器(无论面板是否可见都要清理)
if (noMatchTimeoutRef.current) {
clearTimeout(noMatchTimeoutRef.current)
noMatchTimeoutRef.current = null
}
// 面板不可见时不设置新定时器
if (!ctx.isVisible) {
return
}
// 只有在有搜索文本但无匹配项时才设置延迟关闭
if (_searchText && _searchText.length > 0 && list.length === 0) {
noMatchTimeoutRef.current = setTimeout(() => {
ctx.close('no-matches')
}, 300)
}
// 清理函数
return () => {
if (noMatchTimeoutRef.current) {
clearTimeout(noMatchTimeoutRef.current)
noMatchTimeoutRef.current = null
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- ctx对象引用不稳定使用具体属性避免过度重渲染
}, [ctx.isVisible, searchText, list.length, ctx.close])
const clearSearchText = useCallback(
(includeSymbol = false) => {
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement
@ -275,7 +310,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
const newSearchText = textBeforeCursor.slice(lastSymbolIndex)
setSearchText(newSearchText)
} else {
handleClose('delete-symbol')
ctx.close('delete-symbol')
}
}

View File

@ -397,6 +397,7 @@ const InputbarTools = ({
ToolbarButton={ToolbarButton}
couldMentionNotVisionModel={couldMentionNotVisionModel}
files={files}
setText={setText}
/>
)
},

View File

@ -27,6 +27,7 @@ interface Props {
couldMentionNotVisionModel: boolean
files: FileType[]
ToolbarButton: any
setText: React.Dispatch<React.SetStateAction<string>>
}
const MentionModelsButton: FC<Props> = ({
@ -35,13 +36,17 @@ const MentionModelsButton: FC<Props> = ({
onMentionModel,
couldMentionNotVisionModel,
files,
ToolbarButton
ToolbarButton,
setText
}) => {
const { providers } = useProviders()
const { t } = useTranslation()
const navigate = useNavigate()
const quickPanel = useQuickPanel()
// 记录是否有模型被选择的动作发生
const hasModelActionRef = useRef<boolean>(false)
const pinnedModels = useLiveQuery(
async () => {
const setting = await db.settings.get('pinned:models')
@ -74,7 +79,10 @@ const MentionModelsButton: FC<Props> = ({
</Avatar>
),
filterText: getFancyProviderName(p) + m.name,
action: () => onMentionModel(m),
action: () => {
hasModelActionRef.current = true // 标记有模型动作发生
onMentionModel(m)
},
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
}))
)
@ -107,7 +115,10 @@ const MentionModelsButton: FC<Props> = ({
</Avatar>
),
filterText: getFancyProviderName(p) + m.name,
action: () => onMentionModel(m),
action: () => {
hasModelActionRef.current = true // 标记有模型动作发生
onMentionModel(m)
},
isSelected: mentionedModels.some((selected) => getModelUniqId(selected) === getModelUniqId(m))
}))
@ -127,6 +138,9 @@ const MentionModelsButton: FC<Props> = ({
}, [pinnedModels, providers, t, couldMentionNotVisionModel, mentionedModels, onMentionModel, navigate])
const openQuickPanel = useCallback(() => {
// 重置模型动作标记
hasModelActionRef.current = false
quickPanel.open({
title: t('agents.edit.model.select.title'),
list: modelItems,
@ -134,9 +148,25 @@ const MentionModelsButton: FC<Props> = ({
multiple: true,
afterAction({ item }) {
item.isSelected = !item.isSelected
},
onClose({ action }) {
// ESC或Backspace关闭时的特殊处理
if (action === 'esc' || action === 'delete-symbol') {
// 如果有模型选择动作发生,删除@字符
if (hasModelActionRef.current) {
// 使用React的setText来更新状态而不是直接操作DOM
setText((currentText) => {
const lastAtIndex = currentText.lastIndexOf('@')
if (lastAtIndex !== -1) {
return currentText.slice(0, lastAtIndex) + currentText.slice(lastAtIndex + 1)
}
return currentText
})
}
}
}
})
}, [modelItems, quickPanel, t])
}, [modelItems, quickPanel, t, setText])
const handleOpenQuickPanel = useCallback(() => {
if (quickPanel.isVisible && quickPanel.symbol === '@') {