Files
claudecodeui/src/components/sidebar/view/Sidebar.tsx
Haileyesus 591b18e9e3 feat(sidebar): improve running session state tracking
Add a running-session view to the sidebar, including header controls, running counts, empty states, and row-level processing indicators so active provider work is visible outside the current chat.

Hydrate running state after refresh through a status-only /api/providers/sessions/running endpoint backed by chatRunRegistry.listRunningRuns, then sync and poll the frontend processingSessions map from AppContent without attaching to chat streams or replaying messages.

Preserve fresh local processing entries during sync so newly sent messages are not cleared before the backend registry catches up, and clear completed sessions once the status endpoint no longer reports them.

Thread active session state through sidebar project/session components, show rotating loaders for processing sessions, and keep the running search mode expanded and filterable.

Fix optimistic local user-message dedupe so repeated prompts are only collapsed when a matching server echo appears from the same send window, preventing sent messages from disappearing until assistant completion.

Add registry test coverage for listing currently running app sessions.

Tests: npx eslint on changed files; npx tsc --noEmit -p tsconfig.json; npx tsc --noEmit -p server/tsconfig.json; npx tsx --tsconfig server/tsconfig.json --test server/modules/websocket/tests/chat-run-registry.test.ts.
2026-06-11 20:04:38 +03:00

315 lines
10 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 { usePaletteOps } from '../../../contexts/PaletteOpsContext';
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
import type { Project, LLMProvider } 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,
activeSessions,
onProjectSelect,
onSessionSelect,
onNewSession,
onSessionDelete,
onLoadMoreSessions,
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 paletteOps = usePaletteOps();
const {
isSidebarCollapsed,
expandedProjects,
editingProject,
showNewProject,
editingName,
initialSessionsLoaded,
currentTime,
isRefreshing,
editingSession,
editingSessionName,
searchFilter,
searchMode,
setSearchMode,
conversationResults,
isSearching,
searchProgress,
clearConversationResults,
runningSessionsCount,
deletingProjects,
deleteConfirmation,
sessionDeleteConfirmation,
showVersionModal,
filteredProjects,
archivedProjects,
archivedSessions,
archivedSessionsCount,
isArchivedSessionsLoading,
toggleProject,
handleSessionClick,
toggleStarProject,
isProjectStarred,
getProjectSessions,
loadingMoreProjects,
loadMoreSessionsForProject,
startEditing,
cancelEditing,
saveProjectName,
showDeleteSessionConfirmation,
confirmDeleteSession,
requestProjectDelete,
confirmDeleteProject,
handleProjectSelect,
openArchivedSession,
restoreArchivedProject,
restoreArchivedSession,
refreshProjects,
updateSessionSummary,
collapseSidebar: handleCollapseSidebar,
expandSidebar: handleExpandSidebar,
setShowNewProject,
setEditingName,
setEditingSession,
setEditingSessionName,
setSearchFilter,
setDeleteConfirmation,
setSessionDeleteConfirmation,
setShowVersionModal,
} = useSidebarController({
projects,
selectedProject,
selectedSession,
activeSessions,
isLoading,
isMobile,
t,
onRefresh,
onProjectSelect,
onSessionSelect,
onSessionDelete,
onLoadMoreSessions,
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 = () => {
void paletteOps.refreshProjects();
};
const projectListProps: SidebarProjectListProps = {
projects,
filteredProjects,
selectedProject,
selectedSession,
isLoading,
loadingProgress,
expandedProjects,
editingProject,
editingName,
initialSessionsLoaded,
currentTime,
editingSession,
editingSessionName,
deletingProjects,
tasksEnabled,
mcpServerStatus,
getProjectSessions,
loadingMoreProjects,
activeSessions,
forceExpanded: searchMode === 'running',
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: loadMoreSessionsForProject,
onNewSession,
onEditingSessionNameChange: setEditingSessionName,
onStartEditingSession: (sessionId, initialName) => {
setEditingSession(sessionId);
setEditingSessionName(initialName);
},
onCancelEditingSession: () => {
setEditingSession(null);
setEditingSessionName('');
},
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: LLMProvider) => {
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}
runningSessionsCount={runningSessionsCount}
archivedProjects={archivedProjects}
archivedSessions={archivedSessions}
archivedSessionsCount={archivedSessionsCount}
isArchivedSessionsLoading={isArchivedSessionsLoading}
searchFilter={searchFilter}
onSearchFilterChange={setSearchFilter}
onClearSearchFilter={() => setSearchFilter('')}
searchMode={searchMode}
onSearchModeChange={(mode) => {
setSearchMode(mode);
if (mode === 'projects') clearConversationResults();
}}
conversationResults={conversationResults}
isSearching={isSearching}
searchProgress={searchProgress}
onRestoreArchivedProject={restoreArchivedProject}
onArchivedSessionClick={openArchivedSession}
onRestoreArchivedSession={restoreArchivedSession}
onDeleteArchivedSession={(session) => {
showDeleteSessionConfirmation(
session.projectId,
session.sessionId,
session.sessionTitle,
session.provider,
{ isArchived: true },
);
}}
onConversationResultClick={(projectId: string | null, sessionId: string, provider: string, messageTimestamp?: string | null, messageSnippet?: string | null) => {
// `projectId` (DB key) is the canonical identifier post-migration.
// The server emits null when it can't resolve a project row for
// the search hit; treat that as "no project" and still navigate
// to the session so the user can open it from the URL.
const resolvedProvider = (provider || 'claude') as LLMProvider;
const project = projectId ? projects.find(p => p.projectId === projectId) : null;
const searchTarget = { __searchTargetTimestamp: messageTimestamp || null, __searchTargetSnippet: messageSnippet || null };
const sessionObj = {
id: sessionId,
__provider: resolvedProvider,
__projectId: projectId ?? undefined,
...searchTarget,
};
if (project) {
handleProjectSelect(project);
const sessions = getProjectSessions(project);
const existing = sessions.find(s => s.id === sessionId);
if (existing) {
handleSessionClick({ ...existing, ...searchTarget }, project.projectId);
} else {
handleSessionClick(sessionObj, project.projectId);
}
} else {
handleSessionClick(sessionObj, projectId ?? '');
}
}}
onRefresh={() => {
void refreshProjects();
}}
isRefreshing={isRefreshing}
onCreateProject={() => setShowNewProject(true)}
onCollapseSidebar={handleCollapseSidebar}
updateAvailable={updateAvailable}
releaseInfo={releaseInfo}
latestVersion={latestVersion}
currentVersion={currentVersion}
onShowVersionModal={() => setShowVersionModal(true)}
onShowSettings={onShowSettings}
projectListProps={projectListProps}
t={t}
/>
</>
)}
</>
);
}
export default Sidebar;