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.
This commit is contained in:
Haileyesus
2026-06-11 19:54:51 +03:00
parent 881e72d4a0
commit 591b18e9e3
19 changed files with 465 additions and 24 deletions

View File

@@ -1,16 +1,18 @@
import { type ReactNode } from 'react';
import { Archive, Folder, MessageSquare, RotateCcw, Search, Trash2 } from 'lucide-react';
import { Activity, Archive, Folder, MessageSquare, RotateCcw, Search, Trash2 } from 'lucide-react';
import type { TFunction } from 'i18next';
import { ScrollArea } from '../../../../shared/view/ui';
import type { Project } from '../../../../types/app';
import type { ReleaseInfo } from '../../../../types/sharedTypes';
import type { ConversationSearchResults, SearchProgress } from '../../hooks/useSidebarController';
import type { ArchivedProjectListItem, ArchivedSessionListItem, SidebarSearchMode } from '../../types/types';
import SessionProviderLogo from '../../../llm-logo-provider/SessionProviderLogo';
import { getAllSessions } from '../../utils/utils';
import SidebarFooter from './SidebarFooter';
import SidebarHeader from './SidebarHeader';
import SidebarProjectList, { type SidebarProjectListProps } from './SidebarProjectList';
import { getAllSessions } from '../../utils/utils';
function HighlightedSnippet({ snippet, highlights }: { snippet: string; highlights: { start: number; end: number }[] }) {
const parts: ReactNode[] = [];
@@ -114,6 +116,7 @@ type SidebarContentProps = {
isMobile: boolean;
isLoading: boolean;
projects: Project[];
runningSessionsCount: number;
archivedProjects: ArchivedProjectListItem[];
archivedSessions: ArchivedSessionListItem[];
archivedSessionsCount: number;
@@ -152,6 +155,7 @@ export default function SidebarContent({
isMobile,
isLoading,
projects,
runningSessionsCount,
archivedProjects,
archivedSessions,
archivedSessionsCount,
@@ -196,6 +200,7 @@ export default function SidebarContent({
isMobile={isMobile}
isLoading={isLoading}
projectsCount={projects.length}
runningSessionsCount={runningSessionsCount}
archivedSessionsCount={archivedSessionsCount}
isArchivedSessionsLoading={isArchivedSessionsLoading}
searchFilter={searchFilter}
@@ -307,6 +312,39 @@ export default function SidebarContent({
))}
</div>
) : null
) : searchMode === 'running' ? (
projectListProps.filteredProjects.length === 0 ? (
<div className="px-4 py-12 text-center md:py-8">
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-lg border border-border/70 bg-muted/50 md:mb-3">
<Activity className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="mb-2 text-base font-medium text-foreground md:mb-1">
{t('running.emptyTitle', 'No sessions running')}
</h3>
<p className="text-sm text-muted-foreground">
{runningSessionsCount > 0
? t('running.noMatchingSessions', 'No running sessions match this search.')
: t('running.emptyDescription', 'Active work will appear here while a provider is processing.')}
</p>
</div>
) : (
<div className="space-y-2">
<div className="mx-2 flex items-center justify-between rounded-lg border border-border/60 bg-card/50 px-3 py-2 shadow-sm">
<div className="flex min-w-0 items-center gap-2">
<span className="flex h-6 w-6 items-center justify-center rounded-md bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
<Activity className="h-3.5 w-3.5" />
</span>
<span className="truncate text-xs font-medium text-foreground">
{t('running.title', 'Running now')}
</span>
</div>
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-700 dark:text-emerald-300">
{runningSessionsCount}
</span>
</div>
<SidebarProjectList {...projectListProps} />
</div>
)
) : searchMode === 'archived' ? (
isArchivedSessionsLoading ? (
<div className="px-4 py-12 text-center md:py-8">
@@ -358,7 +396,7 @@ export default function SidebarContent({
<span className="truncate text-sm font-medium text-foreground">
{project.displayName}
</span>
<span className="inline-flex items-center justify-center rounded-full bg-muted px-1 py-px text-[7px] font-medium uppercase leading-none tracking-[0.02em] text-center text-muted-foreground">
<span className="inline-flex items-center justify-center rounded-full bg-muted px-1 py-px text-center text-[7px] font-medium uppercase leading-none tracking-[0.02em] text-muted-foreground">
{t('archived.projectArchived', 'Project archived')}
</span>
</div>
@@ -448,7 +486,7 @@ export default function SidebarContent({
{group.projectDisplayName}
</span>
{group.isProjectArchived && (
<span className="inline-flex items-center justify-center rounded-full bg-muted px-1 py-px text-[7px] font-medium uppercase leading-none tracking-[0.02em] text-center text-muted-foreground">
<span className="inline-flex items-center justify-center rounded-full bg-muted px-1 py-px text-center text-[7px] font-medium uppercase leading-none tracking-[0.02em] text-muted-foreground">
{t('archived.projectArchived', 'Project archived')}
</span>
)}