Files
claudecodeui/src/components/sidebar/view/Sidebar.tsx
Eric Blanquer 3950c0e47f feat: add full-text search across conversations (#482)
* feat: add full-text search across conversations in sidebar

Add a search mode toggle (Projects/Conversations) to the sidebar search bar.
In Conversations mode, search text content across all JSONL session files
with debounced API calls, highlighted snippets, and click-to-navigate results.

* fix: address PR review feedback - session summary tracking, search sequence invalidation, fallback navigation, SSE streaming

- Track session summaries per-session in a Map instead of file-scoped variable
- Increment searchSeqRef when clearing conversation search to invalidate in-flight requests
- Add fallback session navigation when session not loaded in sidebar paging
- Stream search results via SSE for progressive display with progress indicator

* feat(search): add Codex/Gemini search and scroll-to-message navigation

- Search now includes Codex sessions (JSONL from ~/.codex/sessions/) and
  Gemini sessions (in-memory via sessionManager) in addition to Claude
- Search results include provider info and display a provider badge
- Click handler resolves the correct provider instead of hardcoding claude
- Clicking a search result loads all messages and scrolls to the matched
  message with a highlight flash animation

* fix(search): Codex search path matching and scroll reliability

- Fix Codex search scanning all sessions for every project by checking
  session_meta cwd match BEFORE scanning messages (was inflating match
  count and hitting limit before reaching later projects)
- Fix Codex search missing user messages in response_item entries
  (role=user with input_text content parts)
- Fix scroll-to-message being overridden by initial scrollToBottom
  using searchScrollActiveRef to inhibit competing scroll effects
- Fix snippet matching using contiguous substring instead of
  filtered words (which created non-existent phrases)

* feat(search): add Gemini CLI session support for search and history viewing

Gemini CLI sessions stored in ~/.gemini/tmp/<project>/chats/*.json are now
indexed for conversation search and can be loaded for viewing. Previously
only sessions created through the UI (sessionManager) were searchable.

* fix(search): full-word matching and longer highlight flash

- Search now uses word boundaries (\b) instead of substring matching,
  so "hi" no longer matches "this"
- Highlight flash extended to 4s with thicker outline and subtle
  background tint for better visibility
2026-03-06 16:59:23 +03:00

282 lines
8.8 KiB
TypeScript

import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useDeviceSettings } from '../../../hooks/useDeviceSettings';
import { useVersionCheck } from '../../../hooks/useVersionCheck';
import { useUiPreferences } from '../../../hooks/useUiPreferences';
import { useSidebarController } from '../hooks/useSidebarController';
import { useTaskMaster } from '../../../contexts/TaskMasterContext';
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
import type { Project, SessionProvider } from '../../../types/app';
import type { MCPServerStatus, SidebarProps } from '../types/types';
import SidebarCollapsed from './subcomponents/SidebarCollapsed';
import SidebarContent from './subcomponents/SidebarContent';
import SidebarModals from './subcomponents/SidebarModals';
import type { SidebarProjectListProps } from './subcomponents/SidebarProjectList';
type TaskMasterSidebarContext = {
setCurrentProject: (project: Project) => void;
mcpServerStatus: MCPServerStatus;
};
function Sidebar({
projects,
selectedProject,
selectedSession,
onProjectSelect,
onSessionSelect,
onNewSession,
onSessionDelete,
onProjectDelete,
isLoading,
loadingProgress,
onRefresh,
onShowSettings,
showSettings,
settingsInitialTab,
onCloseSettings,
isMobile,
}: SidebarProps) {
const { t } = useTranslation(['sidebar', 'common']);
const { isPWA } = useDeviceSettings({ trackMobile: false });
const { updateAvailable, latestVersion, currentVersion, releaseInfo, installMode } = useVersionCheck(
'siteboon',
'claudecodeui',
);
const { preferences, setPreference } = useUiPreferences();
const { sidebarVisible } = preferences;
const { setCurrentProject, mcpServerStatus } = useTaskMaster() as TaskMasterSidebarContext;
const { tasksEnabled } = useTasksSettings();
const {
isSidebarCollapsed,
expandedProjects,
editingProject,
showNewProject,
editingName,
loadingSessions,
initialSessionsLoaded,
currentTime,
isRefreshing,
editingSession,
editingSessionName,
searchFilter,
searchMode,
setSearchMode,
conversationResults,
isSearching,
searchProgress,
clearConversationResults,
deletingProjects,
deleteConfirmation,
sessionDeleteConfirmation,
showVersionModal,
filteredProjects,
toggleProject,
handleSessionClick,
toggleStarProject,
isProjectStarred,
getProjectSessions,
startEditing,
cancelEditing,
saveProjectName,
showDeleteSessionConfirmation,
confirmDeleteSession,
requestProjectDelete,
confirmDeleteProject,
loadMoreSessions,
handleProjectSelect,
refreshProjects,
updateSessionSummary,
collapseSidebar: handleCollapseSidebar,
expandSidebar: handleExpandSidebar,
setShowNewProject,
setEditingName,
setEditingSession,
setEditingSessionName,
setSearchFilter,
setDeleteConfirmation,
setSessionDeleteConfirmation,
setShowVersionModal,
} = useSidebarController({
projects,
selectedProject,
selectedSession,
isLoading,
isMobile,
t,
onRefresh,
onProjectSelect,
onSessionSelect,
onSessionDelete,
onProjectDelete,
setCurrentProject,
setSidebarVisible: (visible) => setPreference('sidebarVisible', visible),
sidebarVisible,
});
useEffect(() => {
if (typeof document === 'undefined') {
return;
}
document.documentElement.classList.toggle('pwa-mode', isPWA);
document.body.classList.toggle('pwa-mode', isPWA);
}, [isPWA]);
const handleProjectCreated = () => {
if (window.refreshProjects) {
void window.refreshProjects();
return;
}
window.location.reload();
};
const projectListProps: SidebarProjectListProps = {
projects,
filteredProjects,
selectedProject,
selectedSession,
isLoading,
loadingProgress,
expandedProjects,
editingProject,
editingName,
loadingSessions,
initialSessionsLoaded,
currentTime,
editingSession,
editingSessionName,
deletingProjects,
tasksEnabled,
mcpServerStatus,
getProjectSessions,
isProjectStarred,
onEditingNameChange: setEditingName,
onToggleProject: toggleProject,
onProjectSelect: handleProjectSelect,
onToggleStarProject: toggleStarProject,
onStartEditingProject: startEditing,
onCancelEditingProject: cancelEditing,
onSaveProjectName: (projectName) => {
void saveProjectName(projectName);
},
onDeleteProject: requestProjectDelete,
onSessionSelect: handleSessionClick,
onDeleteSession: showDeleteSessionConfirmation,
onLoadMoreSessions: (project) => {
void loadMoreSessions(project);
},
onNewSession,
onEditingSessionNameChange: setEditingSessionName,
onStartEditingSession: (sessionId, initialName) => {
setEditingSession(sessionId);
setEditingSessionName(initialName);
},
onCancelEditingSession: () => {
setEditingSession(null);
setEditingSessionName('');
},
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: SessionProvider) => {
void updateSessionSummary(projectName, sessionId, summary, provider);
},
t,
};
return (
<>
<SidebarModals
projects={projects}
showSettings={showSettings}
settingsInitialTab={settingsInitialTab}
onCloseSettings={onCloseSettings}
showNewProject={showNewProject}
onCloseNewProject={() => setShowNewProject(false)}
onProjectCreated={handleProjectCreated}
deleteConfirmation={deleteConfirmation}
onCancelDeleteProject={() => setDeleteConfirmation(null)}
onConfirmDeleteProject={confirmDeleteProject}
sessionDeleteConfirmation={sessionDeleteConfirmation}
onCancelDeleteSession={() => setSessionDeleteConfirmation(null)}
onConfirmDeleteSession={confirmDeleteSession}
showVersionModal={showVersionModal}
onCloseVersionModal={() => setShowVersionModal(false)}
releaseInfo={releaseInfo}
currentVersion={currentVersion}
latestVersion={latestVersion}
installMode={installMode}
t={t}
/>
{isSidebarCollapsed ? (
<SidebarCollapsed
onExpand={handleExpandSidebar}
onShowSettings={onShowSettings}
updateAvailable={updateAvailable}
onShowVersionModal={() => setShowVersionModal(true)}
t={t}
/>
) : (
<>
<SidebarContent
isPWA={isPWA}
isMobile={isMobile}
isLoading={isLoading}
projects={projects}
searchFilter={searchFilter}
onSearchFilterChange={setSearchFilter}
onClearSearchFilter={() => setSearchFilter('')}
searchMode={searchMode}
onSearchModeChange={(mode: 'projects' | 'conversations') => {
setSearchMode(mode);
if (mode === 'projects') clearConversationResults();
}}
conversationResults={conversationResults}
isSearching={isSearching}
searchProgress={searchProgress}
onConversationResultClick={(projectName: string, sessionId: string, provider: string, messageTimestamp?: string | null, messageSnippet?: string | null) => {
const resolvedProvider = (provider || 'claude') as SessionProvider;
const project = projects.find(p => p.name === projectName);
const searchTarget = { __searchTargetTimestamp: messageTimestamp || null, __searchTargetSnippet: messageSnippet || null };
const sessionObj = {
id: sessionId,
__provider: resolvedProvider,
__projectName: projectName,
...searchTarget,
};
if (project) {
handleProjectSelect(project);
const sessions = getProjectSessions(project);
const existing = sessions.find(s => s.id === sessionId);
if (existing) {
handleSessionClick({ ...existing, ...searchTarget }, projectName);
} else {
handleSessionClick(sessionObj, projectName);
}
} else {
handleSessionClick(sessionObj, projectName);
}
}}
onRefresh={() => {
void refreshProjects();
}}
isRefreshing={isRefreshing}
onCreateProject={() => setShowNewProject(true)}
onCollapseSidebar={handleCollapseSidebar}
updateAvailable={updateAvailable}
releaseInfo={releaseInfo}
latestVersion={latestVersion}
onShowVersionModal={() => setShowVersionModal(true)}
onShowSettings={onShowSettings}
projectListProps={projectListProps}
t={t}
/>
</>
)}
</>
);
}
export default Sidebar;