Files
claudecodeui/src/components/sidebar/view/subcomponents/SidebarProjectSessions.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

157 lines
5.1 KiB
TypeScript

import { Plus } from 'lucide-react';
import type { TFunction } from 'i18next';
import { Button } from '../../../../shared/view/ui';
import type { SessionActivityMap } from '../../../../hooks/useSessionProtection';
import type { Project, ProjectSession, LLMProvider } from '../../../../types/app';
import type { SessionWithProvider } from '../../types/types';
import SidebarSessionItem from './SidebarSessionItem';
type SidebarProjectSessionsProps = {
project: Project;
isExpanded: boolean;
sessions: SessionWithProvider[];
selectedSession: ProjectSession | null;
initialSessionsLoaded: boolean;
hasMoreSessions: boolean;
isLoadingMoreSessions: boolean;
activeSessions: SessionActivityMap;
currentTime: Date;
editingSession: string | null;
editingSessionName: string;
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
onCancelEditingSession: () => void;
onSaveEditingSession: (projectName: string, sessionId: string, summary: string, provider: LLMProvider) => void;
onProjectSelect: (project: Project) => void;
onSessionSelect: (session: SessionWithProvider, projectName: string) => void;
onDeleteSession: (
projectName: string,
sessionId: string,
sessionTitle: string,
provider: LLMProvider,
) => void;
onLoadMoreSessions: (projectId: string) => void;
onNewSession: (project: Project) => void;
t: TFunction;
};
function SessionListSkeleton() {
return (
<>
{Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="rounded-md p-2">
<div className="flex items-start gap-2">
<div className="mt-0.5 h-3 w-3 animate-pulse rounded-full bg-muted" />
<div className="flex-1 space-y-1">
<div className="h-3 animate-pulse rounded bg-muted" style={{ width: `${60 + index * 15}%` }} />
<div className="h-2 w-1/2 animate-pulse rounded bg-muted" />
</div>
</div>
</div>
))}
</>
);
}
export default function SidebarProjectSessions({
project,
isExpanded,
sessions,
selectedSession,
initialSessionsLoaded,
hasMoreSessions,
isLoadingMoreSessions,
activeSessions,
currentTime,
editingSession,
editingSessionName,
onEditingSessionNameChange,
onStartEditingSession,
onCancelEditingSession,
onSaveEditingSession,
onProjectSelect,
onSessionSelect,
onDeleteSession,
onLoadMoreSessions,
onNewSession,
t,
}: SidebarProjectSessionsProps) {
if (!isExpanded) {
return null;
}
const hasSessions = sessions.length > 0;
return (
<div className="ml-3 space-y-1 border-l border-border pl-3">
<div className="px-3 pb-1 pt-1 md:hidden">
<button
className="flex h-8 w-full items-center justify-center gap-2 rounded-md bg-primary text-xs font-medium text-primary-foreground transition-all duration-150 hover:bg-primary/90 active:scale-[0.98]"
onClick={() => {
onProjectSelect(project);
onNewSession(project);
}}
>
<Plus className="h-3 w-3" />
{t('sessions.newSession')}
</button>
</div>
<Button
variant="default"
size="sm"
className="hidden h-8 w-full justify-start gap-2 bg-primary text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 md:flex"
onClick={() => onNewSession(project)}
>
<Plus className="h-3 w-3" />
{t('sessions.newSession')}
</Button>
{!initialSessionsLoaded ? (
<SessionListSkeleton />
) : !hasSessions ? (
<div className="px-3 py-2 text-left">
<p className="text-xs text-muted-foreground">{t('sessions.noSessions')}</p>
</div>
) : (
<>
{sessions.map((session) => (
<SidebarSessionItem
key={session.id}
project={project}
session={session}
selectedSession={selectedSession}
isProcessing={activeSessions.has(session.id)}
currentTime={currentTime}
editingSession={editingSession}
editingSessionName={editingSessionName}
onEditingSessionNameChange={onEditingSessionNameChange}
onStartEditingSession={onStartEditingSession}
onCancelEditingSession={onCancelEditingSession}
onSaveEditingSession={onSaveEditingSession}
onProjectSelect={onProjectSelect}
onSessionSelect={onSessionSelect}
onDeleteSession={onDeleteSession}
t={t}
/>
))}
{hasMoreSessions && (
<Button
variant="ghost"
size="sm"
className="h-8 w-full justify-center text-xs text-muted-foreground hover:text-foreground"
onClick={() => onLoadMoreSessions(project.projectId)}
disabled={isLoadingMoreSessions}
>
{isLoadingMoreSessions ? t('sessions.loadingSessions') : 'Load more sessions'}
</Button>
)}
</>
)}
</div>
);
}