mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-17 13:52:07 +08:00
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:
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -10,6 +10,33 @@ import { PaletteOpsProvider, usePaletteOpsRegister } from '../../contexts/Palett
|
||||
import { useDeviceSettings } from '../../hooks/useDeviceSettings';
|
||||
import { useSessionProtection } from '../../hooks/useSessionProtection';
|
||||
import { useProjectsState } from '../../hooks/useProjectsState';
|
||||
import { api } from '../../utils/api';
|
||||
|
||||
type RunningSessionApiItem = {
|
||||
sessionId?: unknown;
|
||||
startedAt?: unknown;
|
||||
statusText?: unknown;
|
||||
canInterrupt?: unknown;
|
||||
};
|
||||
|
||||
type RunningSessionsApiPayload = {
|
||||
data?: {
|
||||
sessions?: RunningSessionApiItem[];
|
||||
};
|
||||
};
|
||||
|
||||
const parseStartedAt = (value: unknown): number | undefined => {
|
||||
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
};
|
||||
|
||||
export default function AppContent() {
|
||||
return (
|
||||
@@ -30,6 +57,7 @@ function AppContentInner() {
|
||||
processingSessions,
|
||||
markSessionProcessing,
|
||||
markSessionIdle,
|
||||
syncProcessingSessions,
|
||||
} = useSessionProtection();
|
||||
|
||||
const {
|
||||
@@ -57,6 +85,54 @@ function AppContentInner() {
|
||||
activeSessions: processingSessions,
|
||||
});
|
||||
|
||||
const refreshRunningSessions = useCallback(async () => {
|
||||
console.log("ASdsad")
|
||||
try {
|
||||
const response = await api.runningSessions();
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as RunningSessionsApiPayload;
|
||||
const sessions = Array.isArray(payload.data?.sessions) ? payload.data.sessions : [];
|
||||
|
||||
syncProcessingSessions(
|
||||
sessions
|
||||
.map((session) => {
|
||||
if (typeof session.sessionId !== 'string' || !session.sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: session.sessionId,
|
||||
startedAt: parseStartedAt(session.startedAt),
|
||||
statusText: typeof session.statusText === 'string' ? session.statusText : undefined,
|
||||
canInterrupt: typeof session.canInterrupt === 'boolean' ? session.canInterrupt : undefined,
|
||||
};
|
||||
})
|
||||
.filter((session): session is NonNullable<typeof session> => Boolean(session)),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[AppContent] Failed to sync running sessions:', error);
|
||||
}
|
||||
}, [syncProcessingSessions]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshRunningSessions();
|
||||
}, [refreshRunningSessions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (processingSessions.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
void refreshRunningSessions();
|
||||
}, 5000);
|
||||
|
||||
return () => window.clearInterval(interval);
|
||||
}, [processingSessions.size, refreshRunningSessions]);
|
||||
|
||||
usePaletteOpsRegister({
|
||||
openSettings,
|
||||
refreshProjects: refreshProjectsSilently,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { TFunction } from 'i18next';
|
||||
import { api } from '../../../utils/api';
|
||||
import { usePaletteOps } from '../../../contexts/PaletteOpsContext';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import type { SessionActivityMap } from '../../../hooks/useSessionProtection';
|
||||
import type {
|
||||
ArchivedProjectListItem,
|
||||
ArchivedSessionListItem,
|
||||
@@ -81,6 +82,7 @@ type UseSidebarControllerArgs = {
|
||||
projects: Project[];
|
||||
selectedProject: Project | null;
|
||||
selectedSession: ProjectSession | null;
|
||||
activeSessions: SessionActivityMap;
|
||||
isLoading: boolean;
|
||||
isMobile: boolean;
|
||||
t: TFunction;
|
||||
@@ -100,6 +102,7 @@ export function useSidebarController({
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedSession: _selectedSession,
|
||||
activeSessions,
|
||||
isLoading,
|
||||
isMobile,
|
||||
t,
|
||||
@@ -146,6 +149,8 @@ export function useSidebarController({
|
||||
const onRefreshRef = useRef(onRefresh);
|
||||
|
||||
const isSidebarCollapsed = !isMobile && !sidebarVisible;
|
||||
const activeSessionIds = useMemo(() => new Set(activeSessions.keys()), [activeSessions]);
|
||||
const runningSessionsCount = activeSessionIds.size;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
@@ -582,9 +587,48 @@ export function useSidebarController({
|
||||
[projectSortOrder, projectsWithResolvedStarState],
|
||||
);
|
||||
|
||||
const runningProjects = useMemo(() => {
|
||||
if (activeSessionIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return sortedProjects.reduce<Project[]>((acc, project) => {
|
||||
const sessions = (project.sessions ?? []).filter((session) => activeSessionIds.has(String(session.id)));
|
||||
const cursorSessions = (project.cursorSessions ?? []).filter((session) => activeSessionIds.has(String(session.id)));
|
||||
const codexSessions = (project.codexSessions ?? []).filter((session) => activeSessionIds.has(String(session.id)));
|
||||
const geminiSessions = (project.geminiSessions ?? []).filter((session) => activeSessionIds.has(String(session.id)));
|
||||
const opencodeSessions = (project.opencodeSessions ?? []).filter((session) => activeSessionIds.has(String(session.id)));
|
||||
const runningCount =
|
||||
sessions.length
|
||||
+ cursorSessions.length
|
||||
+ codexSessions.length
|
||||
+ geminiSessions.length
|
||||
+ opencodeSessions.length;
|
||||
|
||||
if (runningCount === 0) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
...project,
|
||||
sessions,
|
||||
cursorSessions,
|
||||
codexSessions,
|
||||
geminiSessions,
|
||||
opencodeSessions,
|
||||
sessionMeta: {
|
||||
...project.sessionMeta,
|
||||
total: runningCount,
|
||||
hasMore: false,
|
||||
},
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
}, [activeSessionIds, sortedProjects]);
|
||||
|
||||
const filteredProjects = useMemo(
|
||||
() => filterProjects(sortedProjects, debouncedSearchQuery),
|
||||
[debouncedSearchQuery, sortedProjects],
|
||||
() => filterProjects(searchMode === 'running' ? runningProjects : sortedProjects, debouncedSearchQuery),
|
||||
[debouncedSearchQuery, runningProjects, searchMode, sortedProjects],
|
||||
);
|
||||
|
||||
const filteredArchivedSessions = useMemo(() => {
|
||||
@@ -914,6 +958,7 @@ export function useSidebarController({
|
||||
sessionDeleteConfirmation,
|
||||
showVersionModal,
|
||||
filteredProjects,
|
||||
runningSessionsCount,
|
||||
archivedProjects: filteredArchivedProjects,
|
||||
archivedSessions: filteredArchivedSessions,
|
||||
archivedSessionsCount: archivedProjects.length + archivedSessions.length,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { LoadingProgress, Project, ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import type { SessionActivityMap } from '../../../hooks/useSessionProtection';
|
||||
|
||||
export type ProjectSortOrder = 'name' | 'date';
|
||||
export type SidebarSearchMode = 'projects' | 'conversations' | 'archived';
|
||||
export type SidebarSearchMode = 'projects' | 'conversations' | 'running' | 'archived';
|
||||
export type ArchivedProjectListItem = Project & { isArchived: true };
|
||||
|
||||
export type SessionWithProvider = ProjectSession & {
|
||||
@@ -40,6 +41,7 @@ export type SidebarProps = {
|
||||
projects: Project[];
|
||||
selectedProject: Project | null;
|
||||
selectedSession: ProjectSession | null;
|
||||
activeSessions: SessionActivityMap;
|
||||
onProjectSelect: (project: Project) => void;
|
||||
onSessionSelect: (session: ProjectSession) => void;
|
||||
onNewSession: (project: Project) => void;
|
||||
|
||||
@@ -25,6 +25,7 @@ function Sidebar({
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
activeSessions,
|
||||
onProjectSelect,
|
||||
onSessionSelect,
|
||||
onNewSession,
|
||||
@@ -70,6 +71,7 @@ function Sidebar({
|
||||
isSearching,
|
||||
searchProgress,
|
||||
clearConversationResults,
|
||||
runningSessionsCount,
|
||||
deletingProjects,
|
||||
deleteConfirmation,
|
||||
sessionDeleteConfirmation,
|
||||
@@ -113,6 +115,7 @@ function Sidebar({
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedSession,
|
||||
activeSessions,
|
||||
isLoading,
|
||||
isMobile,
|
||||
t,
|
||||
@@ -159,6 +162,8 @@ function Sidebar({
|
||||
mcpServerStatus,
|
||||
getProjectSessions,
|
||||
loadingMoreProjects,
|
||||
activeSessions,
|
||||
forceExpanded: searchMode === 'running',
|
||||
isProjectStarred,
|
||||
onEditingNameChange: setEditingName,
|
||||
onToggleProject: toggleProject,
|
||||
@@ -229,6 +234,7 @@ function Sidebar({
|
||||
isMobile={isMobile}
|
||||
isLoading={isLoading}
|
||||
projects={projects}
|
||||
runningSessionsCount={runningSessionsCount}
|
||||
archivedProjects={archivedProjects}
|
||||
archivedSessions={archivedSessions}
|
||||
archivedSessionsCount={archivedSessionsCount}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Archive, Folder, FolderPlus, MessageSquare, Plus, RefreshCw, Search, X, PanelLeftClose } from 'lucide-react';
|
||||
import { Activity, Archive, Folder, FolderPlus, MessageSquare, Plus, RefreshCw, Search, X, PanelLeftClose } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
import { Button, Input, Tooltip } from '../../../../shared/view/ui';
|
||||
import { IS_PLATFORM } from '../../../../constants/config';
|
||||
import { cn } from '../../../../lib/utils';
|
||||
import type { SidebarSearchMode } from '../../types/types';
|
||||
|
||||
import GitHubStarBadge from './GitHubStarBadge';
|
||||
|
||||
const MOD_KEY =
|
||||
@@ -14,6 +16,7 @@ type SidebarHeaderProps = {
|
||||
isMobile: boolean;
|
||||
isLoading: boolean;
|
||||
projectsCount: number;
|
||||
runningSessionsCount: number;
|
||||
archivedSessionsCount: number;
|
||||
isArchivedSessionsLoading: boolean;
|
||||
searchFilter: string;
|
||||
@@ -33,6 +36,7 @@ export default function SidebarHeader({
|
||||
isMobile,
|
||||
isLoading,
|
||||
projectsCount,
|
||||
runningSessionsCount,
|
||||
archivedSessionsCount,
|
||||
isArchivedSessionsLoading,
|
||||
searchFilter,
|
||||
@@ -46,12 +50,15 @@ export default function SidebarHeader({
|
||||
onCollapseSidebar,
|
||||
t,
|
||||
}: SidebarHeaderProps) {
|
||||
const showSearchTools = (projectsCount > 0 || archivedSessionsCount > 0 || isArchivedSessionsLoading) && !isLoading;
|
||||
const showSearchTools = (projectsCount > 0 || runningSessionsCount > 0 || archivedSessionsCount > 0 || isArchivedSessionsLoading) && !isLoading;
|
||||
const searchPlaceholder = searchMode === 'conversations'
|
||||
? t('search.conversationsPlaceholder')
|
||||
: searchMode === 'archived'
|
||||
? t('search.archivedPlaceholder', 'Search archived sessions...')
|
||||
: t('projects.searchPlaceholder');
|
||||
: searchMode === 'running'
|
||||
? t('search.runningPlaceholder', 'Search running sessions...')
|
||||
: t('projects.searchPlaceholder');
|
||||
const runningBadgeText = runningSessionsCount > 99 ? '99+' : String(runningSessionsCount);
|
||||
|
||||
const LogoBlock = () => (
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
@@ -153,6 +160,29 @@ export default function SidebarHeader({
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
{t('search.modeConversations')}
|
||||
</button>
|
||||
<Tooltip content={t('search.runningTooltip', 'Running sessions')} position="top">
|
||||
<button
|
||||
onClick={() => onSearchModeChange('running')}
|
||||
aria-pressed={searchMode === 'running'}
|
||||
aria-label={t('search.runningTooltip', 'Running sessions')}
|
||||
title={t('search.runningTooltip', 'Running sessions')}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all",
|
||||
searchMode === 'running'
|
||||
? "bg-background shadow-sm text-foreground ring-1 ring-emerald-500/15"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="relative flex h-3 w-3 items-center justify-center">
|
||||
<Activity className={cn("h-3 w-3", runningSessionsCount > 0 && "text-emerald-500")} />
|
||||
{runningSessionsCount > 0 && (
|
||||
<span className="absolute -right-2.5 -top-2 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-emerald-500 px-0.5 text-[8px] font-semibold leading-none text-white shadow-sm ring-1 ring-background">
|
||||
{runningBadgeText}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content={t('search.archiveOnlyTooltip', 'Archive only')} position="top">
|
||||
<button
|
||||
onClick={() => onSearchModeChange('archived')}
|
||||
@@ -270,6 +300,30 @@ export default function SidebarHeader({
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
{t('search.modeConversations')}
|
||||
</button>
|
||||
<Tooltip content={t('search.runningTooltip', 'Running sessions')} position="top">
|
||||
<button
|
||||
onClick={() => onSearchModeChange('running')}
|
||||
aria-pressed={searchMode === 'running'}
|
||||
aria-label={t('search.runningTooltip', 'Running sessions')}
|
||||
title={t('search.runningTooltip', 'Running sessions')}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-medium transition-all",
|
||||
searchMode === 'running'
|
||||
? "bg-background shadow-sm text-foreground ring-1 ring-emerald-500/15"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="relative flex h-3 w-3 items-center justify-center">
|
||||
<Activity className={cn("h-3 w-3", runningSessionsCount > 0 && "text-emerald-500")} />
|
||||
{runningSessionsCount > 0 && (
|
||||
<span className="absolute -right-2.5 -top-2 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-emerald-500 px-0.5 text-[8px] font-semibold leading-none text-white shadow-sm ring-1 ring-background">
|
||||
{runningBadgeText}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="sr-only">{t('search.modeRunning', 'Running')}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content={t('search.archiveOnlyTooltip', 'Archive only')} position="top">
|
||||
<button
|
||||
onClick={() => onSearchModeChange('archived')}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { TFunction } from 'i18next';
|
||||
import { Button } from '../../../../shared/view/ui';
|
||||
import { cn } from '../../../../lib/utils';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import type { SessionActivityMap } from '../../../../hooks/useSessionProtection';
|
||||
import type { MCPServerStatus, SessionWithProvider } from '../../types/types';
|
||||
import { getTaskIndicatorStatus } from '../../utils/utils';
|
||||
|
||||
@@ -43,6 +44,7 @@ type SidebarProjectItemProps = {
|
||||
provider: LLMProvider,
|
||||
) => void;
|
||||
onLoadMoreSessions: (projectId: string) => void;
|
||||
activeSessions: SessionActivityMap;
|
||||
onNewSession: (project: Project) => void;
|
||||
onEditingSessionNameChange: (value: string) => void;
|
||||
onStartEditingSession: (sessionId: string, initialName: string) => void;
|
||||
@@ -84,6 +86,7 @@ export default function SidebarProjectItem({
|
||||
onSessionSelect,
|
||||
onDeleteSession,
|
||||
onLoadMoreSessions,
|
||||
activeSessions,
|
||||
onNewSession,
|
||||
onEditingSessionNameChange,
|
||||
onStartEditingSession,
|
||||
@@ -395,6 +398,7 @@ export default function SidebarProjectItem({
|
||||
initialSessionsLoaded={initialSessionsLoaded}
|
||||
hasMoreSessions={Boolean(project.sessionMeta?.hasMore)}
|
||||
isLoadingMoreSessions={isLoadingMoreSessions}
|
||||
activeSessions={activeSessions}
|
||||
currentTime={currentTime}
|
||||
editingSession={editingSession}
|
||||
editingSessionName={editingSessionName}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
import type { LoadingProgress, Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import type { SessionActivityMap } from '../../../../hooks/useSessionProtection';
|
||||
import type { MCPServerStatus, SessionWithProvider } from '../../types/types';
|
||||
|
||||
import SidebarProjectItem from './SidebarProjectItem';
|
||||
@@ -27,6 +28,8 @@ export type SidebarProjectListProps = {
|
||||
getProjectSessions: (project: Project) => SessionWithProvider[];
|
||||
onLoadMoreSessions: (projectId: string) => void;
|
||||
loadingMoreProjects: Set<string>;
|
||||
activeSessions: SessionActivityMap;
|
||||
forceExpanded?: boolean;
|
||||
isProjectStarred: (projectName: string) => boolean;
|
||||
onEditingNameChange: (value: string) => void;
|
||||
onToggleProject: (projectName: string) => void;
|
||||
@@ -71,6 +74,8 @@ export default function SidebarProjectList({
|
||||
getProjectSessions,
|
||||
onLoadMoreSessions,
|
||||
loadingMoreProjects,
|
||||
activeSessions,
|
||||
forceExpanded = false,
|
||||
isProjectStarred,
|
||||
onEditingNameChange,
|
||||
onToggleProject,
|
||||
@@ -122,7 +127,7 @@ export default function SidebarProjectList({
|
||||
project={project}
|
||||
selectedProject={selectedProject}
|
||||
selectedSession={selectedSession}
|
||||
isExpanded={expandedProjects.has(project.projectId)}
|
||||
isExpanded={forceExpanded || expandedProjects.has(project.projectId)}
|
||||
isDeleting={deletingProjects.has(project.projectId)}
|
||||
isStarred={isProjectStarred(project.projectId)}
|
||||
editingProject={editingProject}
|
||||
@@ -146,6 +151,7 @@ export default function SidebarProjectList({
|
||||
onSessionSelect={onSessionSelect}
|
||||
onDeleteSession={onDeleteSession}
|
||||
onLoadMoreSessions={onLoadMoreSessions}
|
||||
activeSessions={activeSessions}
|
||||
onNewSession={onNewSession}
|
||||
onEditingSessionNameChange={onEditingSessionNameChange}
|
||||
onStartEditingSession={onStartEditingSession}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
|
||||
@@ -15,6 +16,7 @@ type SidebarProjectSessionsProps = {
|
||||
initialSessionsLoaded: boolean;
|
||||
hasMoreSessions: boolean;
|
||||
isLoadingMoreSessions: boolean;
|
||||
activeSessions: SessionActivityMap;
|
||||
currentTime: Date;
|
||||
editingSession: string | null;
|
||||
editingSessionName: string;
|
||||
@@ -61,6 +63,7 @@ export default function SidebarProjectSessions({
|
||||
initialSessionsLoaded,
|
||||
hasMoreSessions,
|
||||
isLoadingMoreSessions,
|
||||
activeSessions,
|
||||
currentTime,
|
||||
editingSession,
|
||||
editingSessionName,
|
||||
@@ -120,6 +123,7 @@ export default function SidebarProjectSessions({
|
||||
project={project}
|
||||
session={session}
|
||||
selectedSession={selectedSession}
|
||||
isProcessing={activeSessions.has(session.id)}
|
||||
currentTime={currentTime}
|
||||
editingSession={editingSession}
|
||||
editingSessionName={editingSessionName}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Check, Edit2, Trash2, X } from 'lucide-react';
|
||||
import { Check, Edit2, Loader2, Trash2, X } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
import { Badge, Button, Tooltip } from '../../../../shared/view/ui';
|
||||
@@ -13,6 +13,7 @@ type SidebarSessionItemProps = {
|
||||
project: Project;
|
||||
session: SessionWithProvider;
|
||||
selectedSession: ProjectSession | null;
|
||||
isProcessing: boolean;
|
||||
currentTime: Date;
|
||||
editingSession: string | null;
|
||||
editingSessionName: string;
|
||||
@@ -63,6 +64,7 @@ export default function SidebarSessionItem({
|
||||
project,
|
||||
session,
|
||||
selectedSession,
|
||||
isProcessing,
|
||||
currentTime,
|
||||
editingSession,
|
||||
editingSessionName,
|
||||
@@ -117,7 +119,7 @@ export default function SidebarSessionItem({
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
{sessionView.isActive && (
|
||||
{!isProcessing && sessionView.isActive && (
|
||||
<div className="absolute left-0 top-1/2 -translate-x-1 -translate-y-1/2 transform">
|
||||
<Tooltip content={t('tooltips.activeSessionIndicator')} position="right">
|
||||
<div
|
||||
@@ -134,7 +136,9 @@ export default function SidebarSessionItem({
|
||||
className={cn(
|
||||
'p-2 mx-3 my-0.5 rounded-md bg-card border active:scale-[0.98] transition-all duration-150 relative',
|
||||
isSelected ? 'bg-primary/5 border-primary/20' : '',
|
||||
!isSelected && sessionView.isActive
|
||||
!isSelected && isProcessing
|
||||
? 'border-border/60 bg-muted/20'
|
||||
: !isSelected && sessionView.isActive
|
||||
? 'border-green-500/30 bg-green-50/5 dark:bg-green-900/5'
|
||||
: 'border-border/30',
|
||||
)}
|
||||
@@ -153,7 +157,13 @@ export default function SidebarSessionItem({
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="truncate text-xs font-medium text-foreground">{sessionView.sessionName}</div>
|
||||
{compactSessionAge && (
|
||||
{isProcessing ? (
|
||||
<Tooltip content={t('tooltips.processingSessionIndicator', 'Processing session')} position="top">
|
||||
<span className="ml-auto flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-md text-muted-foreground transition-opacity duration-200 group-hover:opacity-0">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : compactSessionAge && (
|
||||
<span className="ml-auto flex-shrink-0 text-[11px] text-muted-foreground">{compactSessionAge}</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -187,15 +197,16 @@ export default function SidebarSessionItem({
|
||||
className={cn(
|
||||
'w-full justify-start p-2 h-auto font-normal text-left hover:bg-accent/50 transition-colors duration-200',
|
||||
isSelected && 'bg-accent text-accent-foreground',
|
||||
!isSelected && isProcessing && 'bg-muted/20 hover:bg-accent/50',
|
||||
)}
|
||||
onClick={() => onSessionSelect(session, project.projectId)}
|
||||
>
|
||||
<div className="flex w-full min-w-0 items-start gap-2">
|
||||
<SessionProviderLogo provider={session.__provider} className="mt-0.5 h-3 w-3 flex-shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn('flex items-center gap-2', isProcessing && 'pr-7')}>
|
||||
<div className="truncate text-xs font-medium text-foreground">{sessionView.sessionName}</div>
|
||||
{compactSessionAge && (
|
||||
{!isProcessing && compactSessionAge && (
|
||||
<span
|
||||
className={cn(
|
||||
'ml-auto flex-shrink-0 text-[11px] text-muted-foreground transition-opacity duration-200',
|
||||
@@ -213,6 +224,19 @@ export default function SidebarSessionItem({
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
{isProcessing && (
|
||||
<div
|
||||
role="status"
|
||||
aria-label={t('tooltips.processingSessionIndicator', 'Processing session')}
|
||||
className={cn(
|
||||
'pointer-events-none absolute right-2 top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-opacity duration-200',
|
||||
isEditing ? 'opacity-0' : 'group-hover:opacity-0',
|
||||
)}
|
||||
>
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={editingContainerRef}
|
||||
className={cn(
|
||||
|
||||
Reference in New Issue
Block a user