diff --git a/frontend/src/components/layout/AppShell.tsx b/frontend/src/components/layout/AppShell.tsx
index cd648f0..04d8df8 100644
--- a/frontend/src/components/layout/AppShell.tsx
+++ b/frontend/src/components/layout/AppShell.tsx
@@ -5,19 +5,33 @@ import { AppHeader } from '@/components/layout/AppHeader';
import { ActivityPanel } from '@/components/layout/ActivityPanel';
import { StatusBar } from '@/components/layout/StatusBar';
import { CommandPalette } from '@/components/layout/CommandPalette';
-import { useState, useCallback } from 'react';
+import { useProjectSprintsCache } from '@/hooks/useProjectsCache';
+import { useState, useCallback, useMemo, useEffect } from 'react';
export function AppShell() {
- const { sprintId } = useParams<{ sprintId: string }>();
+ const { sprintId, projectId } = useParams<{ sprintId: string; projectId: string }>();
const inSprint = !!sprintId;
+ const onProjectPage = !!projectId && !inSprint;
- const [activityPanelOpen, setActivityPanelOpen] = useState(true);
+ const { sprints: projectSprints } = useProjectSprintsCache(onProjectPage ? projectId : null);
+ const latestActiveSprintId = useMemo(() => {
+ if (inSprint) return sprintId;
+ const active = projectSprints.find(
+ (s) => s.currentAgentStatus === 'running' || s.currentAgentStatus === 'waiting',
+ );
+ return active?.id ?? projectSprints[0]?.id ?? null;
+ }, [inSprint, sprintId, projectSprints]);
+
+ const [activityPanelOpen, setActivityPanelOpen] = useState(inSprint);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [commandOpen, setCommandOpen] = useState(false);
- // Only show panels when inside a sprint
- const showSidebar = inSprint && !sidebarCollapsed;
- const showActivity = inSprint && activityPanelOpen;
+ useEffect(() => {
+ setActivityPanelOpen(inSprint);
+ }, [inSprint]);
+
+ const showSidebar = !sidebarCollapsed;
+ const showActivity = (inSprint || onProjectPage) && activityPanelOpen;
const toggleSidebar = useCallback(() => setSidebarCollapsed((prev) => !prev), []);
const toggleActivity = useCallback(() => setActivityPanelOpen((prev) => !prev), []);
@@ -25,7 +39,6 @@ export function AppShell() {
return (
- {/* Header */}
- {/* Main content area */}
- {/* Sidebar - only in sprint views */}
{showSidebar && (
)}
- {/* Main content */}
- {/* Activity panel - only in sprint views */}
{showActivity && (
)}
- {/* Status bar */}
-
- {/* Command palette */}
diff --git a/frontend/src/components/layout/AppSidebar.tsx b/frontend/src/components/layout/AppSidebar.tsx
index 9a8cc42..c7cf8fd 100644
--- a/frontend/src/components/layout/AppSidebar.tsx
+++ b/frontend/src/components/layout/AppSidebar.tsx
@@ -1,112 +1,140 @@
-import { useNavigate, useParams, useLocation } from 'react-router-dom';
+import { useNavigate, useLocation, useParams } from 'react-router-dom';
+import {
+ Activity,
+ LayoutDashboard,
+ Loader2,
+ MessageCircleQuestion,
+ Settings,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/ui/scroll-area';
-import { Button } from '@/components/ui/button';
-import { Settings, ArrowLeft } from 'lucide-react';
-import { PipelineView } from '@/components/layout/PipelineView';
-import { useState, useEffect, useCallback } from 'react';
-import { sprintsService, type Sprint } from '@/services/sprints';
-import { realtimeService } from '@/services/realtime';
+import { useProjectsCache } from '@/hooks/useProjectsCache';
-const PHASE_URL_SUFFIX: Record
= {
- INCEPTION: '',
- CONSTRUCTION: '/construction',
- REVIEW: '/review',
+const STATUS_DOT: Record = {
+ running: 'bg-agent-running',
+ waiting: 'bg-agent-waiting',
+ completed: 'bg-agent-success',
+ failed: 'bg-agent-error',
};
export function AppSidebar() {
const navigate = useNavigate();
- const params = useParams();
const location = useLocation();
- const [sprint, setSprint] = useState(null);
+ const params = useParams<{ projectId?: string }>();
+ const { projects } = useProjectsCache();
- const projectId = params.projectId || '';
- const sprintId = params.sprintId || '';
+ const runningCount = projects.filter((p) => {
+ const s = p.latestSprint?.currentAgentStatus;
+ return s === 'running' || s === 'waiting';
+ }).length;
- const loadSprint = useCallback(async () => {
- if (!projectId || !sprintId) return;
- try {
- const data = await sprintsService.get(projectId, sprintId);
- setSprint(data);
- } catch (err) {
- console.error('Failed to load sprint:', err);
- }
- }, [projectId, sprintId]);
-
- useEffect(() => {
- loadSprint();
- }, [loadSprint]);
-
- // Re-fetch sprint on agent/phase events and auto-navigate on phase change
- useEffect(() => {
- if (!sprintId) return;
- const unsubs = [
- realtimeService.on('agent.started', () => loadSprint()),
- realtimeService.on('agent.completed', () => loadSprint()),
- realtimeService.on('agent.error', () => loadSprint()),
- realtimeService.on('sprint.phaseChanged', (data: { phase?: string }) => {
- loadSprint();
- if (data.phase && PHASE_URL_SUFFIX[data.phase] !== undefined) {
- navigate(`/project/${projectId}/sprint/${sprintId}${PHASE_URL_SUFFIX[data.phase]}`);
- }
- }),
- ];
- return () => unsubs.forEach((unsub) => unsub());
- }, [sprintId, projectId, loadSprint, navigate]);
-
- const currentPhase = location.pathname.includes('/construction')
- ? 'CONSTRUCTION'
- : location.pathname.includes('/review')
- ? 'REVIEW'
- : location.pathname.includes('/graph')
- ? 'GRAPH'
- : location.pathname.includes('/agent')
- ? 'AGENT'
- : 'INCEPTION';
+ const isOnDashboard = location.pathname === '/dashboard';
+ const isOnObservability = location.pathname === '/observability';
+ const isOnAdmin = location.pathname === '/admin';
+ const activeProjectId = params.projectId ?? null;
return (
- {/* Brand header */}
-
-

-
AI-DLC
-
+