Design spec covers: activity zone with daily stats + session feed, nav grouping (Resolve/Build/Insights), AI split (FlowPilot + Flow Assist), pinned flows removal. Implementation plan has 5 chunks, 12 tasks, 39 steps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
41 KiB
Sidebar Redesign Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the sidebar's pinned flows section with a daily stats bar + activity feed, reorganize nav into Resolve/Build/Insights groups, and split AI Assistant into FlowPilot + Flow Assist.
Architecture: New lightweight backend endpoint (/sessions/sidebar-stats) returns daily stats and active/recent session data in a single call. Frontend restructures Sidebar.tsx to render stats bar, activity feed, and grouped nav. Pinned flows system removed from frontend only (backend tables/endpoints left for future cleanup).
Tech Stack: Python FastAPI + SQLAlchemy async (backend), React + TypeScript + Zustand + Tailwind (frontend), Lucide React icons.
Spec: docs/superpowers/specs/2026-03-15-sidebar-redesign-design.md
File Map
Backend (new)
backend/app/schemas/sidebar.py— Pydantic schemas for sidebar stats responsebackend/app/api/endpoints/sidebar.py—GET /sessions/sidebar-statsendpoint
Backend (modify)
backend/app/api/router.py— register new sidebar router
Frontend (new)
frontend/src/components/sidebar/SidebarStatsBar.tsx— three-stat row componentfrontend/src/components/sidebar/SidebarActivityFeed.tsx— activity feed (active + recents)frontend/src/components/sidebar/ActivityItem.tsx— single activity rowfrontend/src/api/sidebar.ts— API client for sidebar statsfrontend/src/pages/FlowAssistPage.tsx— standalone flow builder AI page
Frontend (modify)
frontend/src/components/layout/Sidebar.tsx— major restructurefrontend/src/router.tsx— add/flow-assistroutefrontend/src/api/index.ts— export new sidebar APIfrontend/src/pages/TreeLibraryPage.tsx— remove pin integrationfrontend/src/components/library/TreeGridView.tsx— remove pin buttonfrontend/src/components/library/TreeListView.tsx— remove pin buttonfrontend/src/components/library/TreeTableView.tsx— remove pin button
Frontend (delete)
frontend/src/components/sidebar/PinnedFlowsSection.tsxfrontend/src/store/pinnedFlowsStore.tsfrontend/src/api/pinnedFlows.ts
Tests (new)
backend/tests/test_sidebar_stats.py— integration tests for sidebar endpoint
Chunk 1: Backend — Sidebar Stats Endpoint
Task 1: Sidebar Stats Schema
Files:
-
Create:
backend/app/schemas/sidebar.py -
Step 1: Create the Pydantic schemas
"""Schemas for sidebar stats endpoint."""
from datetime import datetime
from typing import Optional
from uuid import UUID
from pydantic import BaseModel
class SidebarActiveSession(BaseModel):
"""An active or paused session for the activity feed."""
session_id: UUID
tree_name: str
tree_id: UUID
tree_type: str
started_at: datetime
ticket_number: Optional[str] = None
psa_ticket_id: Optional[str] = None
class SidebarRecentSession(BaseModel):
"""A recently completed session for the activity feed."""
session_id: UUID
tree_name: str
tree_id: UUID
tree_type: str
completed_at: datetime
class SidebarTreeCounts(BaseModel):
"""Tree counts for All Flows sub-items."""
total: int
troubleshooting: int
procedural: int
maintenance: int
class SidebarStatsResponse(BaseModel):
"""Response for GET /sessions/sidebar-stats."""
resolved_today: int
active_count: int
total_session_minutes_today: int
tree_counts: SidebarTreeCounts
active_sessions: list[SidebarActiveSession]
recent_completions: list[SidebarRecentSession]
- Step 2: Commit
git add backend/app/schemas/sidebar.py
git commit -m "feat: add sidebar stats Pydantic schemas"
Task 2: Sidebar Stats Endpoint — Tests First
Files:
-
Create:
backend/tests/test_sidebar_stats.py -
Step 3: Write failing tests
"""Integration tests for sidebar stats endpoint."""
import pytest
from httpx import AsyncClient
class TestSidebarStats:
"""Tests for GET /sessions/sidebar-stats."""
@pytest.mark.asyncio
async def test_sidebar_stats_no_sessions(
self, client: AsyncClient, auth_headers: dict
):
"""Empty stats when user has no sessions."""
response = await client.get(
"/api/v1/sessions/sidebar-stats?tz_offset=0",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["resolved_today"] == 0
assert data["active_count"] == 0
assert data["total_session_minutes_today"] == 0
assert data["active_sessions"] == []
assert data["recent_completions"] == []
@pytest.mark.asyncio
async def test_sidebar_stats_with_active_session(
self, client: AsyncClient, auth_headers: dict, test_tree: dict
):
"""Active session appears in activity feed."""
# Create a session (creates as active with started_at set)
create_resp = await client.post(
"/api/v1/sessions",
json={"tree_id": test_tree["id"], "ticket_number": "TK-100"},
headers=auth_headers,
)
assert create_resp.status_code == 201
response = await client.get(
"/api/v1/sessions/sidebar-stats?tz_offset=0",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["active_count"] == 1
assert len(data["active_sessions"]) == 1
assert data["active_sessions"][0]["ticket_number"] == "TK-100"
assert data["active_sessions"][0]["tree_id"] == test_tree["id"]
@pytest.mark.asyncio
async def test_sidebar_stats_resolved_today(
self, client: AsyncClient, auth_headers: dict, test_tree: dict
):
"""Resolved session counts in resolved_today."""
# Create and complete a session
create_resp = await client.post(
"/api/v1/sessions",
json={"tree_id": test_tree["id"]},
headers=auth_headers,
)
session_id = create_resp.json()["id"]
# Complete with resolved outcome
await client.post(
f"/api/v1/sessions/{session_id}/complete",
json={"outcome": "resolved", "outcome_notes": "Fixed it"},
headers=auth_headers,
)
response = await client.get(
"/api/v1/sessions/sidebar-stats?tz_offset=0",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["resolved_today"] >= 1
assert data["active_count"] == 0
assert len(data["recent_completions"]) >= 1
@pytest.mark.asyncio
async def test_sidebar_stats_max_active_sessions(
self, client: AsyncClient, auth_headers: dict, test_tree: dict
):
"""Active sessions capped at 5."""
# Create 7 active sessions
for i in range(7):
await client.post(
"/api/v1/sessions",
json={"tree_id": test_tree["id"], "ticket_number": f"TK-{i}"},
headers=auth_headers,
)
response = await client.get(
"/api/v1/sessions/sidebar-stats?tz_offset=0",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert data["active_count"] == 7 # Total count is accurate
assert len(data["active_sessions"]) == 5 # But list capped at 5
@pytest.mark.asyncio
async def test_sidebar_stats_recent_completions_max_3(
self, client: AsyncClient, auth_headers: dict, test_tree: dict
):
"""Recent completions capped at 3."""
# Create and complete 5 sessions
for i in range(5):
create_resp = await client.post(
"/api/v1/sessions",
json={"tree_id": test_tree["id"]},
headers=auth_headers,
)
session_id = create_resp.json()["id"]
await client.post(
f"/api/v1/sessions/{session_id}/complete",
json={"outcome": "resolved", "outcome_notes": "Done"},
headers=auth_headers,
)
response = await client.get(
"/api/v1/sessions/sidebar-stats?tz_offset=0",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert len(data["recent_completions"]) == 3
@pytest.mark.asyncio
async def test_sidebar_stats_requires_auth(self, client: AsyncClient):
"""Endpoint requires authentication."""
response = await client.get("/api/v1/sessions/sidebar-stats?tz_offset=0")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_sidebar_stats_requires_tz_offset(
self, client: AsyncClient, auth_headers: dict
):
"""tz_offset query param is required."""
response = await client.get(
"/api/v1/sessions/sidebar-stats",
headers=auth_headers,
)
assert response.status_code == 422
- Step 4: Run tests to verify they fail
cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts="
Expected: All tests FAIL (404 — endpoint doesn't exist yet).
- Step 5: Commit failing tests
git add backend/tests/test_sidebar_stats.py
git commit -m "test: add failing tests for sidebar stats endpoint"
Task 3: Sidebar Stats Endpoint — Implementation
Files:
-
Create:
backend/app/api/endpoints/sidebar.py -
Modify:
backend/app/api/router.py -
Step 6: Implement the sidebar stats endpoint
Create backend/app/api/endpoints/sidebar.py:
"""Sidebar stats and activity feed endpoint."""
from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, Query
from sqlalchemy import func, select, and_, case
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.api.deps import get_current_active_user
from app.models.session import Session
from app.models.tree import Tree
from app.models.user import User
from app.schemas.sidebar import (
SidebarActiveSession,
SidebarRecentSession,
SidebarStatsResponse,
SidebarTreeCounts,
)
router = APIRouter(prefix="/sessions", tags=["sidebar"])
@router.get("/sidebar-stats", response_model=SidebarStatsResponse)
async def get_sidebar_stats(
tz_offset: int = Query(..., description="Client UTC offset in minutes (e.g. -300 for EST)"),
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
) -> SidebarStatsResponse:
"""Get sidebar stats and activity feed for the current user.
Returns daily stats (resolved count, active count, time in session)
and lists of active + recently completed sessions.
"""
# Compute "today" start in user's timezone, then convert to UTC
now_utc = datetime.now(timezone.utc)
user_tz = timezone(timedelta(minutes=-tz_offset))
now_local = now_utc.astimezone(user_tz)
today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
today_start_utc = today_start_local.astimezone(timezone.utc)
user_filter = Session.user_id == current_user.id
# --- Resolved today ---
resolved_result = await db.execute(
select(func.count()).where(
and_(
user_filter,
Session.completed_at >= today_start_utc,
Session.outcome == "resolved",
)
)
)
resolved_today = resolved_result.scalar() or 0
# --- Active count (all time, not just today) ---
active_result = await db.execute(
select(func.count()).where(
and_(
user_filter,
Session.started_at.isnot(None),
Session.completed_at.is_(None),
)
)
)
active_count = active_result.scalar() or 0
# --- Total session minutes today ---
# Sum of (completed_at or now) - started_at for sessions started today
duration_expr = func.extract(
"epoch",
func.coalesce(Session.completed_at, now_utc) - Session.started_at,
) / 60.0
duration_result = await db.execute(
select(func.coalesce(func.sum(duration_expr), 0)).where(
and_(
user_filter,
Session.started_at.isnot(None),
Session.started_at >= today_start_utc,
)
)
)
total_minutes = int(duration_result.scalar() or 0)
# --- Active sessions (max 5, most recent first) ---
active_sessions_result = await db.execute(
select(
Session.id,
Session.tree_id,
Session.started_at,
Session.ticket_number,
Session.psa_ticket_id,
Session.tree_snapshot["name"].as_string().label("tree_name"),
Session.tree_snapshot["tree_type"].as_string().label("tree_type"),
)
.where(
and_(
user_filter,
Session.started_at.isnot(None),
Session.completed_at.is_(None),
)
)
.order_by(Session.started_at.desc())
.limit(5)
)
active_sessions = [
SidebarActiveSession(
session_id=row.id,
tree_name=row.tree_name or "Unknown Flow",
tree_id=row.tree_id,
tree_type=row.tree_type or "troubleshooting",
started_at=row.started_at,
ticket_number=row.ticket_number,
psa_ticket_id=row.psa_ticket_id,
)
for row in active_sessions_result.all()
]
# --- Recent completions (max 3, completed today, most recent first) ---
recent_result = await db.execute(
select(
Session.id,
Session.tree_id,
Session.completed_at,
Session.tree_snapshot["name"].as_string().label("tree_name"),
Session.tree_snapshot["tree_type"].as_string().label("tree_type"),
)
.where(
and_(
user_filter,
Session.completed_at.isnot(None),
Session.completed_at >= today_start_utc,
)
)
.order_by(Session.completed_at.desc())
.limit(3)
)
recent_completions = [
SidebarRecentSession(
session_id=row.id,
tree_name=row.tree_name or "Unknown Flow",
tree_id=row.tree_id,
tree_type=row.tree_type or "troubleshooting",
completed_at=row.completed_at,
)
for row in recent_result.all()
]
# --- Tree counts (for All Flows sub-items) ---
tree_counts_result = await db.execute(
select(
func.count().label("total"),
func.count().filter(Tree.tree_type == "troubleshooting").label("troubleshooting"),
func.count().filter(Tree.tree_type == "procedural").label("procedural"),
func.count().filter(Tree.tree_type == "maintenance").label("maintenance"),
).where(
and_(
Tree.account_id == current_user.account_id,
Tree.is_active.is_(True),
Tree.deleted_at.is_(None),
)
)
)
tc = tree_counts_result.one()
return SidebarStatsResponse(
resolved_today=resolved_today,
active_count=active_count,
total_session_minutes_today=total_minutes,
tree_counts=SidebarTreeCounts(
total=tc.total,
troubleshooting=tc.troubleshooting,
procedural=tc.procedural,
maintenance=tc.maintenance,
),
active_sessions=active_sessions,
recent_completions=recent_completions,
)
- Step 7: Register the router in
router.py
In backend/app/api/router.py, add the import and registration. The sidebar router must be registered BEFORE the sessions router since both use /sessions prefix — FastAPI matches routes in registration order, and /sessions/sidebar-stats needs to match before /sessions/{session_id}.
from app.api.endpoints import sidebar
# ... existing imports ...
# Add BEFORE sessions router registration:
api_router.include_router(sidebar.router)
- Step 8: Run tests to verify they pass
cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts="
Expected: All 7 tests PASS.
- Step 9: Commit
git add backend/app/api/endpoints/sidebar.py backend/app/api/router.py backend/app/schemas/sidebar.py
git commit -m "feat: add sidebar stats endpoint with daily stats and activity feed"
Chunk 2: Frontend — Sidebar API Client & Activity Components
Task 4: Sidebar API Client
Files:
-
Create:
frontend/src/api/sidebar.ts -
Modify:
frontend/src/api/index.ts -
Step 10: Create the sidebar API client
import { apiClient } from './client'
export interface SidebarActiveSession {
session_id: string
tree_name: string
tree_id: string
tree_type: 'troubleshooting' | 'procedural' | 'maintenance'
started_at: string
ticket_number: string | null
psa_ticket_id: string | null
}
export interface SidebarRecentSession {
session_id: string
tree_name: string
tree_id: string
tree_type: 'troubleshooting' | 'procedural' | 'maintenance'
completed_at: string
}
export interface SidebarTreeCounts {
total: number
troubleshooting: number
procedural: number
maintenance: number
}
export interface SidebarStatsResponse {
resolved_today: number
active_count: number
total_session_minutes_today: number
tree_counts: SidebarTreeCounts
active_sessions: SidebarActiveSession[]
recent_completions: SidebarRecentSession[]
}
export const sidebarApi = {
getStats: async (): Promise<SidebarStatsResponse> => {
const tzOffset = new Date().getTimezoneOffset()
const response = await apiClient.get<SidebarStatsResponse>(
`/sessions/sidebar-stats?tz_offset=${tzOffset}`
)
return response.data
},
}
- Step 11: Export from api/index.ts
Add to frontend/src/api/index.ts:
export { sidebarApi } from './sidebar'
- Step 12: Commit
git add frontend/src/api/sidebar.ts frontend/src/api/index.ts
git commit -m "feat: add sidebar stats API client"
Task 5: ActivityItem Component
Files:
-
Create:
frontend/src/components/sidebar/ActivityItem.tsx -
Step 13: Create the ActivityItem component
import { useNavigate } from 'react-router-dom'
import { getTreeNavigatePath } from '@/lib/routing'
import { cn } from '@/lib/utils'
interface ActivityItemProps {
sessionId: string
treeName: string
treeId: string
treeType: 'troubleshooting' | 'procedural' | 'maintenance'
status: 'active' | 'paused' | 'recent'
ticketNumber?: string | null
timestamp?: string | null
}
function formatRelativeTime(dateString: string): string {
const now = Date.now()
const then = new Date(dateString).getTime()
const diffMinutes = Math.floor((now - then) / 60000)
if (diffMinutes < 1) return 'just now'
if (diffMinutes < 60) return `${diffMinutes}m ago`
const diffHours = Math.floor(diffMinutes / 60)
if (diffHours < 24) return `${diffHours}h ago`
return 'yesterday'
}
export function ActivityItem({
sessionId,
treeName,
treeId,
treeType,
status,
ticketNumber,
timestamp,
}: ActivityItemProps) {
const navigate = useNavigate()
const handleClick = () => {
navigate(getTreeNavigatePath(treeId, treeType), {
state: { sessionId },
})
}
const isRecent = status === 'recent'
return (
<button
onClick={handleClick}
className={cn(
'flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors',
'hover:bg-[rgba(255,255,255,0.03)]',
isRecent ? 'text-[#6b7280] text-[0.72rem]' : 'text-[#e2e8f0] text-[0.8rem]'
)}
title={`${treeName}${ticketNumber ? ` (${ticketNumber})` : ''} — click to resume`}
aria-label={
status === 'active'
? `Active session: ${treeName}`
: status === 'paused'
? `Paused session: ${treeName}`
: `Recent session: ${treeName}`
}
>
{/* Status dot */}
{status === 'active' && (
<span
className="h-[7px] w-[7px] shrink-0 rounded-full"
style={{
background: '#34d399',
boxShadow: '0 0 6px rgba(52,211,153,0.5)',
animation: 'pulse-dot 2s ease-in-out infinite',
}}
aria-label="Active session"
/>
)}
{status === 'paused' && (
<span
className="h-[7px] w-[7px] shrink-0 rounded-full"
style={{
background: '#f59e0b',
boxShadow: '0 0 4px rgba(245,158,11,0.3)',
}}
aria-label="Paused session"
/>
)}
{status === 'recent' && (
<span className="h-1 w-1 shrink-0 rounded-full bg-[#3d4350]" />
)}
{/* Flow name */}
<span className="flex-1 truncate">{treeName}</span>
{/* Ticket number or timestamp */}
{ticketNumber && !isRecent && (
<span className="shrink-0 font-label text-[0.5625rem] text-[#60a5fa]">
{ticketNumber}
</span>
)}
{isRecent && timestamp && (
<span className="shrink-0 font-label text-[0.5625rem] text-[#5a6170]">
{formatRelativeTime(timestamp)}
</span>
)}
</button>
)
}
- Step 14: Commit
git add frontend/src/components/sidebar/ActivityItem.tsx
git commit -m "feat: add ActivityItem component for sidebar activity feed"
Task 6: SidebarStatsBar Component
Files:
-
Create:
frontend/src/components/sidebar/SidebarStatsBar.tsx -
Step 15: Create the stats bar component
interface SidebarStatsBarProps {
resolved: number
active: number
sessionMinutes: number
}
function formatDuration(minutes: number): string {
if (minutes < 60) return `${minutes}m`
const h = Math.floor(minutes / 60)
const m = minutes % 60
return m > 0 ? `${h}h ${m}m` : `${h}h`
}
export function SidebarStatsBar({ resolved, active, sessionMinutes }: SidebarStatsBarProps) {
return (
<div
className="flex gap-0.5 px-3 pt-2 pb-1"
role="group"
aria-label="Today's stats"
>
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
<div
className="font-label text-sm font-semibold leading-none"
style={{ color: '#34d399' }}
aria-label={`${resolved} resolved today`}
>
{resolved}
</div>
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
Resolved
</div>
</div>
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
<div
className="font-label text-sm font-semibold leading-none"
style={{ color: '#22d3ee' }}
aria-label={`${active} active sessions`}
>
{active}
</div>
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
Active
</div>
</div>
<div className="flex-1 rounded-md bg-[rgba(255,255,255,0.02)] px-1 py-1.5 text-center">
<div
className="font-label text-sm font-semibold leading-none text-muted-foreground"
aria-label={`${formatDuration(sessionMinutes)} in session today`}
>
{formatDuration(sessionMinutes)}
</div>
<div className="mt-1 font-label text-[7px] uppercase tracking-[0.1em] text-[#3d4350]">
In Session
</div>
</div>
</div>
)
}
- Step 16: Commit
git add frontend/src/components/sidebar/SidebarStatsBar.tsx
git commit -m "feat: add SidebarStatsBar component"
Task 7: SidebarActivityFeed Component
Files:
-
Create:
frontend/src/components/sidebar/SidebarActivityFeed.tsx -
Step 17: Create the activity feed component
import { Clock } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { ActivityItem } from './ActivityItem'
import type { SidebarActiveSession, SidebarRecentSession } from '@/api/sidebar'
interface SidebarActivityFeedProps {
activeSessions: SidebarActiveSession[]
recentCompletions: SidebarRecentSession[]
totalActive: number
}
export function SidebarActivityFeed({
activeSessions,
recentCompletions,
totalActive,
}: SidebarActivityFeedProps) {
const navigate = useNavigate()
const hasActivity = activeSessions.length > 0 || recentCompletions.length > 0
return (
<div className="px-3 pb-1">
{/* Header */}
<div className="flex items-center gap-1.5 px-2.5 py-1 mb-0.5">
<Clock size={10} style={{ color: '#34d399' }} />
<span className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170]">
Activity
</span>
</div>
{!hasActivity ? (
<p className="px-2.5 py-2 text-xs text-muted-foreground">
No activity today
</p>
) : (
<div className="space-y-0.5">
{/* Active sessions */}
{activeSessions.map((session) => (
<ActivityItem
key={session.session_id}
sessionId={session.session_id}
treeName={session.tree_name}
treeId={session.tree_id}
treeType={session.tree_type}
status="active"
ticketNumber={session.ticket_number || session.psa_ticket_id}
/>
))}
{/* Overflow link */}
{totalActive > 5 && (
<button
onClick={() => navigate('/sessions')}
className="w-full px-2.5 py-1 text-left text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors"
>
View all in Sessions →
</button>
)}
{/* Divider between active and recent */}
{activeSessions.length > 0 && recentCompletions.length > 0 && (
<div className="mx-2.5 my-1" style={{ height: '1px', background: 'rgba(255,255,255,0.03)' }} />
)}
{/* Recent completions */}
{recentCompletions.map((session) => (
<ActivityItem
key={session.session_id}
sessionId={session.session_id}
treeName={session.tree_name}
treeId={session.tree_id}
treeType={session.tree_type}
status="recent"
timestamp={session.completed_at}
/>
))}
</div>
)}
</div>
)
}
- Step 18: Commit
git add frontend/src/components/sidebar/SidebarActivityFeed.tsx
git commit -m "feat: add SidebarActivityFeed component"
Chunk 3: Frontend — Sidebar Restructure
Task 8: Add Pulse Dot CSS Animation
Files:
-
Modify:
frontend/src/index.css -
Step 19: Add the pulse-dot keyframe animation
Add to frontend/src/index.css (in the global styles section, after existing keyframes):
@keyframes pulse-dot {
0%, 100% { box-shadow: 0 0 4px rgba(52,211,153,0.4); }
50% { box-shadow: 0 0 8px rgba(52,211,153,0.7); }
}
- Step 20: Commit
git add frontend/src/index.css
git commit -m "feat: add pulse-dot animation for active session indicator"
Task 9: Restructure Sidebar.tsx
Files:
- Modify:
frontend/src/components/layout/Sidebar.tsx
This is the largest change. The sidebar is rewritten to use the new layout.
- Step 21: Rewrite Sidebar.tsx
Replace the entire file content. Key changes:
- Remove
PinnedFlowsSectionimport andusePinnedFlowsStoreusage - Remove
sessionsApiandtreesApidata fetches (replaced bysidebarApi.getStats) - Add
SidebarStatsBarandSidebarActivityFeedin expanded mode - Add
BrainandWandSparklesicon imports - Reorganize nav items into Dashboard → Resolve → Build → Insights groups
- Update collapsed view with all 13 items
- Add
flowPilotandflowAssisttoNAV_COLORS
The new Sidebar.tsx should:
Imports:
import { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import {
LayoutGrid, Network, Wrench, Clock, FileOutput, BarChart3,
Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText,
BookOpen, Lightbulb, Code2, Library, Brain, WandSparkles,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { sidebarApi } from '@/api'
import type { SidebarStatsResponse } from '@/api/sidebar'
import { SidebarStatsBar } from '@/components/sidebar/SidebarStatsBar'
import { SidebarActivityFeed } from '@/components/sidebar/SidebarActivityFeed'
import { NavItem } from './NavItem'
NAV_COLORS — add two new entries, remove ai:
const NAV_COLORS = {
dashboard: '#22d3ee',
flows: '#a78bfa',
editor: '#f59e0b',
sessions: '#34d399',
exports: '#60a5fa',
flowPilot: '#e879f9',
flowAssist:'#f472b6',
stepLib: '#fb923c',
scripts: '#2dd4bf',
kb: '#fb7185',
analytics: '#38bdf8',
guides: '#a3e635',
feedback: '#818cf8',
} as const
Data fetch — replace the two separate fetches with one sidebarApi.getStats() call. Use location.pathname as a dependency so stats refresh on navigation (the sidebar persists across pages and does NOT re-mount):
const location = useLocation()
const [stats, setStats] = useState<SidebarStatsResponse | null>(null)
useEffect(() => {
sidebarApi.getStats().then(setStats).catch(() => {})
}, [location.pathname])
Expanded layout (inside the else branch):
{/* Stats Bar */}
<SidebarStatsBar
resolved={stats?.resolved_today ?? 0}
active={stats?.active_count ?? 0}
sessionMinutes={stats?.total_session_minutes_today ?? 0}
/>
{/* Activity Feed */}
<SidebarActivityFeed
activeSessions={stats?.active_sessions ?? []}
recentCompletions={stats?.recent_completions ?? []}
totalActive={stats?.active_count ?? 0}
/>
<div style={{ borderBottom: '1px solid var(--glass-border)' }} />
{/* Dashboard (standalone) */}
<div className="px-3 py-2 space-y-0.5">
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} />
{/* Resolve group */}
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
Resolve
</div>
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} />
<NavItem
href="/trees"
icon={Network}
label="All Flows"
badge={stats?.tree_counts.total || undefined}
iconColor={NAV_COLORS.flows}
matchPaths={['/trees', '/flows']}
children={[
{ href: '/trees?type=troubleshooting', label: 'Troubleshooting', count: stats?.tree_counts.troubleshooting || undefined },
{ href: '/trees?type=procedural', label: 'Projects', count: stats?.tree_counts.procedural || undefined },
{ href: '/trees?type=maintenance', label: 'Maintenance', count: stats?.tree_counts.maintenance || undefined },
]}
/>
<NavItem href="/assistant" icon={Brain} label="FlowPilot" iconColor={NAV_COLORS.flowPilot} />
<NavItem href="/scripts" icon={Code2} label="Script Library" iconColor={NAV_COLORS.scripts} />
{/* Build group */}
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
Build
</div>
<NavItem href="/my-trees" icon={Wrench} label="Flow Editor" iconColor={NAV_COLORS.editor} />
<NavItem href="/flow-assist" icon={WandSparkles} label="Flow Assist" iconColor={NAV_COLORS.flowAssist} />
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} />
<NavItem href="/kb-accelerator" icon={Lightbulb} label="KB Accelerator" iconColor={NAV_COLORS.kb} />
{/* Insights group */}
<div className="font-label text-[0.5625rem] uppercase tracking-[0.12em] text-[#5a6170] px-3 pt-3 pb-1">
Insights
</div>
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} />
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} />
</div>
Collapsed layout — update to include all 13 items (add Brain/FlowPilot and WandSparkles/Flow Assist, remove BotMessageSquare/AI Assistant):
<div className="flex flex-col items-center px-1.5 py-3 space-y-1">
<NavItem href="/" icon={LayoutGrid} label="Dashboard" iconColor={NAV_COLORS.dashboard} collapsed />
<NavItem href="/sessions" icon={Clock} label="Sessions" badge={stats?.active_count || undefined} iconColor={NAV_COLORS.sessions} collapsed />
<NavItem href="/trees" icon={Network} label="All Flows" matchPaths={['/trees', '/flows']} iconColor={NAV_COLORS.flows} collapsed />
<NavItem href="/assistant" icon={Brain} label="FlowPilot" iconColor={NAV_COLORS.flowPilot} collapsed />
<NavItem href="/scripts" icon={Code2} label="Script Library" iconColor={NAV_COLORS.scripts} collapsed />
<NavItem href="/my-trees" icon={Wrench} label="Flow Editor" iconColor={NAV_COLORS.editor} collapsed />
<NavItem href="/flow-assist" icon={WandSparkles} label="Flow Assist" iconColor={NAV_COLORS.flowAssist} collapsed />
<NavItem href="/step-library" icon={Library} label="Step Library" iconColor={NAV_COLORS.stepLib} collapsed />
<NavItem href="/kb-accelerator" icon={Lightbulb} label="KB Accelerator" iconColor={NAV_COLORS.kb} collapsed />
<NavItem href="/shares" icon={FileOutput} label="Exports" iconColor={NAV_COLORS.exports} collapsed />
<NavItem href="/analytics" icon={BarChart3} label="Analytics" iconColor={NAV_COLORS.analytics} collapsed />
<NavItem href="/guides" icon={BookOpen} label="User Guides" iconColor={NAV_COLORS.guides} collapsed />
<NavItem href="/feedback" icon={MessageSquareText} label="Feedback" iconColor={NAV_COLORS.feedback} collapsed />
</div>
- Step 22: Verify the app builds
cd frontend && npm run build
Expected: Build succeeds. Fix any TypeScript or import errors.
- Step 23: Commit
git add frontend/src/components/layout/Sidebar.tsx
git commit -m "feat: restructure sidebar with stats bar, activity feed, and grouped nav"
Task 10: Remove Pinned Flows Frontend Code
Files:
-
Delete:
frontend/src/components/sidebar/PinnedFlowsSection.tsx -
Delete:
frontend/src/store/pinnedFlowsStore.ts -
Delete:
frontend/src/api/pinnedFlows.ts -
Modify:
frontend/src/pages/TreeLibraryPage.tsx— remove pin integration -
Modify:
frontend/src/components/library/TreeGridView.tsx— remove pin button -
Modify:
frontend/src/components/library/TreeListView.tsx— remove pin button -
Step 24: Delete pinned flows files
rm frontend/src/components/sidebar/PinnedFlowsSection.tsx
rm frontend/src/store/pinnedFlowsStore.ts
rm frontend/src/api/pinnedFlows.ts
- Step 25: Remove pin imports and props from TreeLibraryPage.tsx
Remove:
-
usePinnedFlowsStoreimport and all related hooks (pinnedItems,isMutatingByTreeId,pinnedTreeIds,pinLoadingTreeIds,togglePin,loadPinned) -
The
useEffectthat callsloadPinned() -
The
pinnedTreeIds,onTogglePin,pinLoadingTreeIdsprops passed toTreeGridView,TreeListView, andTreeTableView -
Step 26: Remove pin button from TreeGridView.tsx
Remove:
-
Staricon import from lucide-react -
pinnedTreeIds,onTogglePin,pinLoadingTreeIdsfrom component props interface -
The star/pin button JSX (lines ~67-86)
-
Step 27: Remove pin button from TreeListView.tsx
Same pattern as TreeGridView — remove pin-related props and button.
- Step 27b: Remove pin button from TreeTableView.tsx
Same pattern as TreeGridView — remove pin-related props and button.
- Step 28: Remove pinnedFlows export from api/index.ts
Remove the export { pinnedFlowsApi } from './pinnedFlows' line (or similar).
- Step 29: Verify the app builds
cd frontend && npm run build
Expected: Build succeeds with no references to removed files.
- Step 30: Commit
git add frontend/src/components/sidebar/PinnedFlowsSection.tsx frontend/src/store/pinnedFlowsStore.ts frontend/src/api/pinnedFlows.ts frontend/src/pages/TreeLibraryPage.tsx frontend/src/components/library/TreeGridView.tsx frontend/src/components/library/TreeListView.tsx frontend/src/components/library/TreeTableView.tsx frontend/src/api/index.ts
git commit -m "refactor: remove pinned flows frontend (PinnedFlowsSection, store, API, pin buttons)"
Note: The deleted files will show as "deleted" in git add — that's expected. Use git add -u as a fallback if the explicit paths don't stage the deletions correctly.
Chunk 4: Frontend — Flow Assist Page & Route
Task 11: FlowAssistPage
Files:
-
Create:
frontend/src/pages/FlowAssistPage.tsx -
Modify:
frontend/src/router.tsx -
Step 31: Create FlowAssistPage
This is a standalone page for the conversational flow builder. For now, create a placeholder page that will be expanded when the AI split is fully implemented. The page should use the same glass-card layout pattern as other pages.
import { WandSparkles } from 'lucide-react'
export default function FlowAssistPage() {
return (
<div className="p-6">
<div className="mb-6">
<h1 className="font-heading text-2xl font-bold tracking-tight text-foreground">
<span className="inline-flex items-center gap-2">
<WandSparkles size={24} style={{ color: '#f472b6' }} />
Flow Assist
</span>
</h1>
<p className="mt-1 text-sm text-muted-foreground">
Build flows from natural language — describe what you need and Flow Assist will generate the decision tree or procedural steps.
</p>
</div>
<div className="glass-card-static p-8 text-center">
<WandSparkles size={40} className="mx-auto mb-4 text-muted-foreground" />
<h2 className="font-heading text-lg font-semibold text-foreground mb-2">
Coming Soon
</h2>
<p className="text-sm text-muted-foreground max-w-md mx-auto">
Flow Assist will be available here as a dedicated conversational flow builder.
In the meantime, use the AI panel in the Flow Editor to generate flows.
</p>
</div>
</div>
)
}
- Step 32: Add the route in router.tsx
Add inside the protected route children (alongside other page routes like /assistant, /scripts, etc.):
{
path: 'flow-assist',
lazy: () => import('./pages/FlowAssistPage').then(m => ({ Component: m.default })),
},
If the router doesn't use lazy loading, use the direct import pattern matching the existing routes.
- Step 33: Verify the app builds
cd frontend && npm run build
- Step 34: Commit
git add frontend/src/pages/FlowAssistPage.tsx frontend/src/router.tsx
git commit -m "feat: add FlowAssistPage placeholder and /flow-assist route"
Chunk 5: Final Verification & Cleanup
Task 12: Full Test Suite & Build Verification
- Step 35: Run backend tests
cd backend && python -m pytest tests/test_sidebar_stats.py -v --override-ini="addopts="
Expected: All sidebar tests pass.
- Step 36: Run full backend test suite
cd backend && python -m pytest --override-ini="addopts=" -x -q
Expected: No regressions. If any pinned-flows tests exist and fail (because we didn't touch backend), that's expected — they should still pass since backend is unchanged.
- Step 37: Run frontend build
cd frontend && npm run build
Expected: Clean build, no errors.
- Step 38: Manual smoke test
Start the dev servers and verify:
- Sidebar loads with stats bar showing 0/0/0m on fresh state
- Activity feed shows "No activity today" when empty
- Nav is grouped: Dashboard → Resolve (Sessions, All Flows, FlowPilot, Script Library) → Build (Flow Editor, Flow Assist, Step Library, KB Accelerator) → Insights (Exports, Analytics)
- FlowPilot link goes to
/assistant - Flow Assist link goes to
/flow-assist(placeholder page) - Collapsed sidebar shows all 13 icon-only items
- Pin buttons are gone from tree library grid/list views
- No console errors
- Step 39: Final commit with any cleanup
If there are any remaining changes from smoke test fixes, stage the specific files and commit:
git add <changed-files>
git commit -m "chore: sidebar redesign cleanup and verification"
Summary
| Chunk | Tasks | What it delivers |
|---|---|---|
| 1 | Tasks 1-3 | Backend endpoint with tests — GET /sessions/sidebar-stats |
| 2 | Tasks 4-7 | Frontend API client + ActivityItem, StatsBar, ActivityFeed components |
| 3 | Tasks 8-10 | Sidebar restructure + pinned flows removal |
| 4 | Task 11 | FlowAssistPage + /flow-assist route |
| 5 | Task 12 | Full verification and cleanup |