From 1bf380a921f6a05d27ec8b46c2f063608056c0a5 Mon Sep 17 00:00:00 2001 From: Jason Young <44939412+farion1231@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:35:14 +0800 Subject: [PATCH] fix: auto-close panel when no commands match (#7824) (#8784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- .../src/components/QuickPanel/view.tsx | 39 ++++++++++++++++++- .../src/pages/home/Inputbar/InputbarTools.tsx | 1 + .../home/Inputbar/MentionModelsButton.tsx | 38 ++++++++++++++++-- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/QuickPanel/view.tsx b/src/renderer/src/components/QuickPanel/view.tsx index 36957aaf63..c955453903 100644 --- a/src/renderer/src/components/QuickPanel/view.tsx +++ b/src/renderer/src/components/QuickPanel/view.tsx @@ -66,6 +66,9 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { const prevSearchTextRef = useRef('') const prevSymbolRef = useRef('') + // 无匹配项自动关闭的定时器 + const noMatchTimeoutRef = useRef(null) + // 处理搜索,过滤列表 const list = useMemo(() => { if (!ctx.isVisible && !ctx.symbol) return [] @@ -128,12 +131,44 @@ export const QuickPanelView: React.FC = ({ 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 = ({ setInputText }) => { const newSearchText = textBeforeCursor.slice(lastSymbolIndex) setSearchText(newSearchText) } else { - handleClose('delete-symbol') + ctx.close('delete-symbol') } } diff --git a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx index 9053a4e02e..4f6f264ace 100644 --- a/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx +++ b/src/renderer/src/pages/home/Inputbar/InputbarTools.tsx @@ -397,6 +397,7 @@ const InputbarTools = ({ ToolbarButton={ToolbarButton} couldMentionNotVisionModel={couldMentionNotVisionModel} files={files} + setText={setText} /> ) }, diff --git a/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx b/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx index e76b85877b..2aff40c0de 100644 --- a/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/MentionModelsButton.tsx @@ -27,6 +27,7 @@ interface Props { couldMentionNotVisionModel: boolean files: FileType[] ToolbarButton: any + setText: React.Dispatch> } const MentionModelsButton: FC = ({ @@ -35,13 +36,17 @@ const MentionModelsButton: FC = ({ onMentionModel, couldMentionNotVisionModel, files, - ToolbarButton + ToolbarButton, + setText }) => { const { providers } = useProviders() const { t } = useTranslation() const navigate = useNavigate() const quickPanel = useQuickPanel() + // 记录是否有模型被选择的动作发生 + const hasModelActionRef = useRef(false) + const pinnedModels = useLiveQuery( async () => { const setting = await db.settings.get('pinned:models') @@ -74,7 +79,10 @@ const MentionModelsButton: FC = ({ ), 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 = ({ ), 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 = ({ }, [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 = ({ 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 === '@') {