From ff4db1f138238ea34bb162a594c52c4d5b1987fa Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Mon, 21 Sep 2026 16:44:44 +0800 Subject: [PATCH 1/3] fix(web-ui): unify Team chat search into one panel & stop hijacking Ctrl+F on macOS - Keyboard: Team chat find only triggers on Cmd+F on macOS (Ctrl+F falls through to the native cursor-forward default); Win/Linux keep Ctrl+F. Matches the isMac pattern used in App.tsx. - Merge the two search UIs: find-in-conversation now ALSO queries the server FTS5 index (debounced) and lists full-history results below the instant local matches. Removes the separate ChatSearchPanel entry point, its header button, and the duplicate affordance. - ChatHistorySearch gains optional serverResults/serverLoading/ onServerResultClick props + 'All history' section (en/zh-CN/es). --- .../src/components/ChatHistorySearch.tsx | 40 ++++++++ packages/web-ui/src/locales/en/team.json | 1 + packages/web-ui/src/locales/es/team.json | 1 + packages/web-ui/src/locales/zh-CN/team.json | 1 + packages/web-ui/src/pages/Team.tsx | 98 ++++++++----------- 5 files changed, 86 insertions(+), 55 deletions(-) diff --git a/packages/web-ui/src/components/ChatHistorySearch.tsx b/packages/web-ui/src/components/ChatHistorySearch.tsx index 80f0297f..05bf36e9 100644 --- a/packages/web-ui/src/components/ChatHistorySearch.tsx +++ b/packages/web-ui/src/components/ChatHistorySearch.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import type { ChatSearchMatch } from '../lib/chatSearch.ts'; +import type { SearchResult } from '../api.ts'; /** * Find-in-conversation bar. @@ -27,6 +28,10 @@ export interface ChatHistorySearchProps { onClose: () => void; /** Optional per-message label (sender / time) shown next to each hit. */ labelFor?: (match: ChatSearchMatch) => string; + /** Full-history results from the server FTS5 index (the merged second search). */ + serverResults?: readonly SearchResult[]; + serverLoading?: boolean; + onServerResultClick?: (result: SearchResult) => void; } const FIELD_KEY: Record = { @@ -58,6 +63,7 @@ function Snippet({ match }: { match: ChatSearchMatch }) { export function ChatHistorySearch({ query, onQueryChange, matches, cursor, scanned, hasMore, loadingMore, truncated, onLoadEarlier, onJump, onClose, labelFor, + serverResults, serverLoading, onServerResultClick, }: ChatHistorySearchProps) { const { t } = useTranslation(['team', 'common']); const inputRef = useRef(null); @@ -174,6 +180,40 @@ export function ChatHistorySearch({ ))} )} + + {/* Full-history server results (merged with find-in-conversation): when the + local transcript scan is exhausted or the user wants older messages, + the same panel also lists matches from the server FTS5 index. */} + {hasQuery && (serverLoading || (serverResults && serverResults.length > 0)) && ( +
+
{t('page.findServerResultsTitle')}
+ {serverLoading && ( +
{t('page.searching')}
+ )} + {serverResults && serverResults.length > 0 && ( +
+ {serverResults.map(r => ( + + ))} +
+ )} +
+ )} ); } diff --git a/packages/web-ui/src/locales/en/team.json b/packages/web-ui/src/locales/en/team.json index ab792ff3..3e6d965d 100644 --- a/packages/web-ui/src/locales/en/team.json +++ b/packages/web-ui/src/locales/en/team.json @@ -445,6 +445,7 @@ "findScopeHasMore": "Earlier messages are not loaded yet", "findLoadEarlier": "Load earlier messages", "findLoadingEarlier": "Loading…", + "findServerResultsTitle": "All history", "findFieldText": "Message", "findFieldThinking": "Thinking", "findFieldTool": "Tool", diff --git a/packages/web-ui/src/locales/es/team.json b/packages/web-ui/src/locales/es/team.json index c3c043a8..a31dd6b6 100644 --- a/packages/web-ui/src/locales/es/team.json +++ b/packages/web-ui/src/locales/es/team.json @@ -437,6 +437,7 @@ "findScopeHasMore": "Los mensajes anteriores aún no están cargados", "findLoadEarlier": "Cargar mensajes anteriores", "findLoadingEarlier": "Cargando…", + "findServerResultsTitle": "Todo el historial", "findFieldText": "Mensaje", "findFieldThinking": "Pensando", "findFieldTool": "Herramienta", diff --git a/packages/web-ui/src/locales/zh-CN/team.json b/packages/web-ui/src/locales/zh-CN/team.json index ce5721b4..2c104635 100644 --- a/packages/web-ui/src/locales/zh-CN/team.json +++ b/packages/web-ui/src/locales/zh-CN/team.json @@ -445,6 +445,7 @@ "findScopeHasMore": "更早的消息还未加载", "findLoadEarlier": "加载更早的消息", "findLoadingEarlier": "正在加载…", + "findServerResultsTitle": "全部历史", "findFieldText": "正文", "findFieldThinking": "思考", "findFieldTool": "工具", diff --git a/packages/web-ui/src/pages/Team.tsx b/packages/web-ui/src/pages/Team.tsx index 190a3f4d..f3e8d8ef 100644 --- a/packages/web-ui/src/pages/Team.tsx +++ b/packages/web-ui/src/pages/Team.tsx @@ -54,7 +54,7 @@ import { renderMentionText } from '../components/CommentInput.tsx'; import { ChatTeamSidebar } from '../components/ChatTeamSidebar.tsx'; import { TeamDetailPanel } from '../components/TeamDetailPanel.tsx'; import { RightPanel } from '../components/RightPanel.tsx'; -import { ChatSearchPanel, GroupMemberPanel, type PanelCandidate } from './teamPanels.tsx'; +import { GroupMemberPanel, type PanelCandidate } from './teamPanels.tsx'; import { ChatHistorySearch } from '../components/ChatHistorySearch.tsx'; import { searchChatHistory } from '../lib/chatSearch.ts'; import { useLayout } from '../contexts/LayoutContext.tsx'; @@ -866,19 +866,16 @@ export function TeamPage({ initialAgentId, authUser, previewMode, previewData }: const pendingSelectTeamRef = useRef(null); const [showMemberPanel, setShowMemberPanel] = useState(false); - // Message search - const [searchOpen, setSearchOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [searchLoading, setSearchLoading] = useState(false); - const searchDebounceRef = useRef>(undefined); - - // Find-in-conversation (this session's transcript only). Separate from the - // header's global search: that one queries the server across all conversations, - // this one is instant, local, and jumps the virtualized list to the hit. + // Find + full-history search share ONE panel: instant local matches over the + // loaded transcript PLUS server FTS5 results across the whole chat history. + // (The two search UIs were merged to avoid two overlapping search entry + // points with the same keyboard shortcut.) const [findOpen, setFindOpen] = useState(false); const [findQuery, setFindQuery] = useState(''); const [findCursor, setFindCursor] = useState(-1); + const [findServerResults, setFindServerResults] = useState([]); + const [findServerLoading, setFindServerLoading] = useState(false); + const findServerDebounceRef = useRef>(undefined); // Teams const [teams, setTeams] = useState(previewData?.teams ?? []); @@ -1948,20 +1945,25 @@ export function TeamPage({ initialAgentId, authUser, previewMode, previewData }: requestAnimationFrame(flash); }, [findOutcome.matches, chatVirtualizer, pinChatScrollAway]); - // Ctrl/Cmd+F opens find-in-conversation while the chat tab is showing — the - // convention in every chat client, and the only discoverable entry point on - // mobile. Native browser find is not useful inside the Electron shell here. + // Cmd+F (Mac) / Ctrl+F (Win/Linux) opens find-in-conversation while the chat + // tab is showing — the convention in every chat client, and the only + // discoverable entry point on mobile. Native browser find is not useful + // inside the Electron shell here. + // NOTE: on macOS, Ctrl+F is the native "cursor forward" shortcut, so only + // ⌘F is hijacked there; Ctrl+F falls through to the platform default. useEffect(() => { if (mainTab !== 'chat' || previewMode) return; const onKey = (e: KeyboardEvent) => { if (e.key.toLowerCase() !== 'f') return; - if (!(e.metaKey || e.ctrlKey) || e.altKey) return; + if (e.altKey || e.shiftKey) return; + const isFindShortcut = isMac ? (e.metaKey && !e.ctrlKey) : (e.ctrlKey && !e.metaKey); + if (!isFindShortcut) return; e.preventDefault(); setFindOpen(true); }; document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); - }, [mainTab, previewMode]); + }, [mainTab, previewMode, isMac]); // ── Preserve scroll position across page-level navigation ── // PageSlot now uses visibility:hidden + position:absolute instead of @@ -2959,28 +2961,32 @@ export function TeamPage({ initialAgentId, authUser, previewMode, previewData }: } }; - const executeSearch = useCallback(async (q: string) => { - if (q.length < 2) { setSearchResults([]); return; } - setSearchLoading(true); + // ── Full-history search (merged into the find panel) ────────────────────── + // The find-in-conversation bar now ALSO queries the server FTS5 index so the + // same panel covers the loaded transcript (instant, local) AND the whole + // history (server). This replaces the old separate ChatSearchPanel. + const runFindServerSearch = useCallback(async (q: string) => { + if (q.trim().length < 2) { setFindServerResults([]); return; } + setFindServerLoading(true); try { const scope = chatMode === 'channel' ? 'channel' : chatMode === 'direct' ? 'direct' : 'all'; const channel = chatMode === 'channel' ? activeChannel : undefined; const { results } = await api.messages.search(q, { scope, channel, limit: 30 }); - setSearchResults(results); - } catch { setSearchResults([]); } - setSearchLoading(false); + setFindServerResults(results); + } catch { setFindServerResults([]); } + setFindServerLoading(false); }, [chatMode, activeChannel]); - const handleSearchInput = useCallback((q: string) => { - setSearchQuery(q); - if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); - searchDebounceRef.current = setTimeout(() => executeSearch(q), 300); - }, [executeSearch]); + const handleFindInput = useCallback((q: string) => { + setFindQuery(q); + if (findServerDebounceRef.current) clearTimeout(findServerDebounceRef.current); + findServerDebounceRef.current = setTimeout(() => void runFindServerSearch(q), 300); + }, [runFindServerSearch]); - const handleSearchResultClick = useCallback((result: import('../api.ts').SearchResult) => { - setSearchOpen(false); - setSearchQuery(''); - setSearchResults([]); + const handleFindServerResultClick = useCallback((result: import('../api.ts').SearchResult) => { + setFindOpen(false); + setFindQuery(''); + setFindServerResults([]); if (result.source === 'channel' && result.channel) { setChatMode('channel'); setActiveChannel(result.channel); @@ -3867,8 +3873,8 @@ export function TeamPage({ initialAgentId, authUser, previewMode, previewData }: >{chatMode === 'channel' ? t('page.teamTab') : t('page.profileTab')}
@@ -4071,15 +4077,6 @@ export function TeamPage({ initialAgentId, authUser, previewMode, previewData }: - {chatMode === 'channel' && activeGroupChat?.type === 'custom' && (