Files
resolutionflow/docs/superpowers/plans/2026-03-15-sidebar-redesign.md
chihlasm 357f8e2d08 feat: sidebar redesign — activity feed, grouped nav, AI split (#107)
* docs: add 5 sidebar icon color concepts for UX review

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(ui): add semantic icon colors and updated icons to sidebar nav

Swap generic icons for more descriptive alternatives (Network, Wrench,
FileOutput, Library, Code2, Lightbulb) and assign each nav item a unique
semantic color for instant visual landmarks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ui): default Sessions page to Active tab, reorder tabs

Active sessions are what engineers care about most. Tab order is now
Active, Prepared, Completed, All.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add sidebar grouping and AI naming concept mockups

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add sidebar redesign context and decision summary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add sidebar redesign spec and implementation plan

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>

* feat: add sidebar stats Pydantic schemas

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add failing tests for sidebar stats endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add sidebar stats endpoint with daily stats and activity feed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add sidebar API client, stats bar, activity feed components

New components: SidebarStatsBar, SidebarActivityFeed, ActivityItem.
New API client for sidebar stats endpoint. Pulse-dot CSS animation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: restructure sidebar with stats bar, activity feed, and grouped nav

Dashboard-first layout with Resolve/Build/Insights groups.
AI split: FlowPilot (Resolve) + Flow Assist (Build).
Stats bar: Resolved/Active/In Session daily counters.
Activity feed: active sessions with CW ticket #, recent completions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: remove pinned flows frontend (PinnedFlowsSection, store, API, pin buttons)

Removed: PinnedFlowsSection component, pinnedFlowsStore, pinnedFlows API client.
Cleaned: pin buttons from TreeGridView, TreeListView, TreeTableView.
Cleaned: favorites section from QuickStartPage, pin props from TreeLibraryPage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add FlowAssistPage placeholder and /flow-assist route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: real-time sidebar stats via session-changed events

Sidebar now refreshes stats when sessions are created or completed,
not just on page navigation. Uses window event bus pattern (same as
folder-changed events in codebase).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: live-ticking In Session timer using active session start times

SidebarStatsBar now computes active session elapsed time client-side
from started_at timestamps, ticking every 60s. Backend only returns
completed session minutes to avoid double-counting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: sidebar In Session timer ticks every second and shows seconds

Timer now uses 1s interval (not 60s) and displays seconds when under
a minute so it matches the session timer in the flow UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: trigger PR environment redeploy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* debug: add console.log to SidebarStatsBar for timer investigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: parse sidebar timestamps as UTC (append Z suffix)

Backend returns naive UTC timestamps without timezone indicator.
JS Date() treats bare ISO strings as local time, causing the timer
to compute negative elapsed time (future timestamps). Appending 'Z'
forces UTC parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rename 'In Session' to 'Total Time' for clarity

Makes it clear the timer is an aggregate of all sessions today,
not just the current one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 01:35:16 -04:00

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 response
  • backend/app/api/endpoints/sidebar.pyGET /sessions/sidebar-stats endpoint

Backend (modify)

  • backend/app/api/router.py — register new sidebar router

Frontend (new)

  • frontend/src/components/sidebar/SidebarStatsBar.tsx — three-stat row component
  • frontend/src/components/sidebar/SidebarActivityFeed.tsx — activity feed (active + recents)
  • frontend/src/components/sidebar/ActivityItem.tsx — single activity row
  • frontend/src/api/sidebar.ts — API client for sidebar stats
  • frontend/src/pages/FlowAssistPage.tsx — standalone flow builder AI page

Frontend (modify)

  • frontend/src/components/layout/Sidebar.tsx — major restructure
  • frontend/src/router.tsx — add /flow-assist route
  • frontend/src/api/index.ts — export new sidebar API
  • frontend/src/pages/TreeLibraryPage.tsx — remove pin integration
  • frontend/src/components/library/TreeGridView.tsx — remove pin button
  • frontend/src/components/library/TreeListView.tsx — remove pin button
  • frontend/src/components/library/TreeTableView.tsx — remove pin button

Frontend (delete)

  • frontend/src/components/sidebar/PinnedFlowsSection.tsx
  • frontend/src/store/pinnedFlowsStore.ts
  • frontend/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:

  1. Remove PinnedFlowsSection import and usePinnedFlowsStore usage
  2. Remove sessionsApi and treesApi data fetches (replaced by sidebarApi.getStats)
  3. Add SidebarStatsBar and SidebarActivityFeed in expanded mode
  4. Add Brain and WandSparkles icon imports
  5. Reorganize nav items into Dashboard → Resolve → Build → Insights groups
  6. Update collapsed view with all 13 items
  7. Add flowPilot and flowAssist to NAV_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:

  • usePinnedFlowsStore import and all related hooks (pinnedItems, isMutatingByTreeId, pinnedTreeIds, pinLoadingTreeIds, togglePin, loadPinned)

  • The useEffect that calls loadPinned()

  • The pinnedTreeIds, onTogglePin, pinLoadingTreeIds props passed to TreeGridView, TreeListView, and TreeTableView

  • Step 26: Remove pin button from TreeGridView.tsx

Remove:

  • Star icon import from lucide-react

  • pinnedTreeIds, onTogglePin, pinLoadingTreeIds from 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:

  1. Sidebar loads with stats bar showing 0/0/0m on fresh state
  2. Activity feed shows "No activity today" when empty
  3. Nav is grouped: Dashboard → Resolve (Sessions, All Flows, FlowPilot, Script Library) → Build (Flow Editor, Flow Assist, Step Library, KB Accelerator) → Insights (Exports, Analytics)
  4. FlowPilot link goes to /assistant
  5. Flow Assist link goes to /flow-assist (placeholder page)
  6. Collapsed sidebar shows all 13 icon-only items
  7. Pin buttons are gone from tree library grid/list views
  8. 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