Add loading progress indicator

Adds real-time progress feedback when loading projects:
- Server broadcasts progress updates via WebSocket
- Sidebar shows current project being loaded with progress bar
- Fixed progress counter to show correct current/total
- Completion event now fires after all projects (including manual) are processed
- Race condition fix for timeout cleanup using ref
- Added cleanup function to prevent state update on unmounted component
This commit is contained in:
Eric Blanquer​
2026-01-18 04:58:03 +01:00
parent f8d1ec7b9e
commit 9e03acb0db
4 changed files with 138 additions and 19 deletions

View File

@@ -18,7 +18,7 @@
* Handles both existing sessions (with real IDs) and new sessions (with temporary IDs).
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { BrowserRouter as Router, Routes, Route, useNavigate, useParams } from 'react-router-dom';
import { Settings as SettingsIcon, Sparkles } from 'lucide-react';
import Sidebar from './components/Sidebar';
@@ -53,6 +53,7 @@ function AppContent() {
const [isMobile, setIsMobile] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(false);
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
const [loadingProgress, setLoadingProgress] = useState(null); // { phase, current, total, currentProject }
const [isInputFocused, setIsInputFocused] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [settingsInitialTab, setSettingsInitialTab] = useState('agents');
@@ -78,7 +79,10 @@ function AppContent() {
const [externalMessageUpdate, setExternalMessageUpdate] = useState(0);
const { ws, sendMessage, messages } = useWebSocketContext();
// Ref to track loading progress timeout for cleanup
const loadingProgressTimeoutRef = useRef(null);
// Detect if running as PWA
const [isPWA, setIsPWA] = useState(false);
@@ -170,7 +174,23 @@ function AppContent() {
useEffect(() => {
if (messages.length > 0) {
const latestMessage = messages[messages.length - 1];
// Handle loading progress updates
if (latestMessage.type === 'loading_progress') {
if (loadingProgressTimeoutRef.current) {
clearTimeout(loadingProgressTimeoutRef.current);
loadingProgressTimeoutRef.current = null;
}
setLoadingProgress(latestMessage);
if (latestMessage.phase === 'complete') {
loadingProgressTimeoutRef.current = setTimeout(() => {
setLoadingProgress(null);
loadingProgressTimeoutRef.current = null;
}, 500);
}
return;
}
if (latestMessage.type === 'projects_updated') {
// External Session Update Detection: Check if the changed file is the current session's JSONL
@@ -247,6 +267,13 @@ function AppContent() {
}
}
}
return () => {
if (loadingProgressTimeoutRef.current) {
clearTimeout(loadingProgressTimeoutRef.current);
loadingProgressTimeoutRef.current = null;
}
};
}, [messages, selectedProject, selectedSession, activeSessions]);
const fetchProjects = async () => {
@@ -765,6 +792,7 @@ function AppContent() {
onSessionDelete={handleSessionDelete}
onProjectDelete={handleProjectDelete}
isLoading={isLoadingProjects}
loadingProgress={loadingProgress}
onRefresh={handleSidebarRefresh}
onShowSettings={() => setShowSettings(true)}
updateAvailable={updateAvailable}
@@ -859,6 +887,7 @@ function AppContent() {
onSessionDelete={handleSessionDelete}
onProjectDelete={handleProjectDelete}
isLoading={isLoadingProjects}
loadingProgress={loadingProgress}
onRefresh={handleSidebarRefresh}
onShowSettings={() => setShowSettings(true)}
updateAvailable={updateAvailable}

View File

@@ -52,6 +52,7 @@ function Sidebar({
onSessionDelete,
onProjectDelete,
isLoading,
loadingProgress,
onRefresh,
onShowSettings,
updateAvailable,
@@ -663,9 +664,28 @@ function Sidebar({
<div className="w-6 h-6 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
</div>
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">Loading projects...</h3>
<p className="text-sm text-muted-foreground">
Fetching your Claude projects and sessions
</p>
{loadingProgress && loadingProgress.total > 0 ? (
<div className="space-y-2">
<div className="w-full bg-muted rounded-full h-2 overflow-hidden">
<div
className="bg-primary h-full transition-all duration-300 ease-out"
style={{ width: `${(loadingProgress.current / loadingProgress.total) * 100}%` }}
/>
</div>
<p className="text-sm text-muted-foreground">
{loadingProgress.current}/{loadingProgress.total} projects
</p>
{loadingProgress.currentProject && (
<p className="text-xs text-muted-foreground/70 truncate max-w-[200px] mx-auto" title={loadingProgress.currentProject}>
{loadingProgress.currentProject.split('-').slice(-2).join('/')}
</p>
)}
</div>
) : (
<p className="text-sm text-muted-foreground">
Fetching your Claude projects and sessions
</p>
)}
</div>
) : projects.length === 0 ? (
<div className="text-center py-12 md:py-8 px-4">