# Dashboard: My Flows, Favorites/Pin UI, and AI Builder — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Transform the dashboard into a structured personal workspace with a Favorites section (pinned flows), paginated "My Flows", shared pin store, and AI Builder access from the Library page.

**Architecture:** Zustand store (`pinnedFlowsStore`) becomes single source of truth for pin state across Sidebar, Dashboard, and Library. URL-synced pagination hook manages My Flows paging. Cached quota hook avoids redundant AI quota fetches. All three library view components gain optional pin button props.

**Tech Stack:** React 19, Zustand, React Router v7, Tailwind CSS, Lucide icons, Axios

**Design doc:** `docs/plans/2026-02-20-final-dashboard-plan.md`

---

## Task 1: Add `pin()` to `pinnedFlowsApi`

**Files:**
- Modify: `frontend/src/api/pinnedFlows.ts`

**Step 1: Add `pin` method**

In `frontend/src/api/pinnedFlows.ts`, add after the `unpin` method:

```typescript
pin: async (treeId: string): Promise<void> => {
  await apiClient.post(`/trees/${treeId}/pin`)
},
```

**Step 2: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add frontend/src/api/pinnedFlows.ts
git commit -m "feat: add pin() method to pinnedFlowsApi"
```

---

## Task 2: Create `pinnedFlowsStore`

**Files:**
- Create: `frontend/src/store/pinnedFlowsStore.ts`

**Step 1: Create the store**

Create `frontend/src/store/pinnedFlowsStore.ts`:

```typescript
import { create } from 'zustand'
import { pinnedFlowsApi } from '@/api/pinnedFlows'
import type { PinnedFlow } from '@/api/pinnedFlows'
import { toast } from '@/lib/toast'

interface PinnedFlowsState {
  items: PinnedFlow[]
  isLoaded: boolean
  isLoading: boolean
  isMutatingByTreeId: Record<string, boolean>
  error: string | null

  // Actions
  load: (force?: boolean) => Promise<void>
  pin: (treeId: string) => Promise<void>
  unpin: (treeId: string) => Promise<void>
  toggle: (treeId: string) => void

  // Derived helpers
  isPinned: (treeId: string) => boolean
}

export const usePinnedFlowsStore = create<PinnedFlowsState>()((set, get) => ({
  items: [],
  isLoaded: false,
  isLoading: false,
  isMutatingByTreeId: {},
  error: null,

  load: async (force = false) => {
    const state = get()
    if (state.isLoaded && !force) return
    if (state.isLoading) return

    set({ isLoading: true, error: null })
    try {
      const data = await pinnedFlowsApi.list()
      set({ items: data.items, isLoaded: true, isLoading: false })
    } catch {
      set({ error: 'Failed to load pinned flows', isLoading: false })
    }
  },

  pin: async (treeId: string) => {
    const state = get()
    if (state.isMutatingByTreeId[treeId]) return

    set({ isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true } })

    try {
      await pinnedFlowsApi.pin(treeId)
      // Reload to get full PinnedFlow object with tree_name, tree_type, etc.
      const data = await pinnedFlowsApi.list()
      set((s) => ({
        items: data.items,
        isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
      }))
    } catch (err: unknown) {
      const status = (err as { response?: { status?: number } })?.response?.status
      if (status === 409) {
        toast.error('Maximum of 15 favorites reached. Unpin a flow to add a new one.')
      } else {
        toast.error('Failed to pin flow')
      }
      set((s) => ({
        isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
      }))
    }
  },

  unpin: async (treeId: string) => {
    const state = get()
    if (state.isMutatingByTreeId[treeId]) return

    // Optimistic remove
    const prevItems = state.items
    set({
      items: state.items.filter((f) => f.tree_id !== treeId),
      isMutatingByTreeId: { ...state.isMutatingByTreeId, [treeId]: true },
    })

    try {
      await pinnedFlowsApi.unpin(treeId)
      set((s) => ({
        isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
      }))
    } catch {
      // Rollback
      toast.error('Failed to unpin flow')
      set((s) => ({
        items: prevItems,
        isMutatingByTreeId: { ...s.isMutatingByTreeId, [treeId]: false },
      }))
    }
  },

  toggle: (treeId: string) => {
    const state = get()
    if (state.isPinned(treeId)) {
      state.unpin(treeId)
    } else {
      state.pin(treeId)
    }
  },

  isPinned: (treeId: string) => {
    return get().items.some((f) => f.tree_id === treeId)
  },
}))

// Derived selectors (use outside component or in selectors)
export const selectPinnedTreeIds = (state: PinnedFlowsState): Set<string> =>
  new Set(state.items.map((f) => f.tree_id))

export const selectPinLoadingTreeIds = (state: PinnedFlowsState): Set<string> =>
  new Set(
    Object.entries(state.isMutatingByTreeId)
      .filter(([, v]) => v)
      .map(([k]) => k)
  )
```

**Step 2: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`
Expected: No errors

**Step 3: Commit**

```bash
git add frontend/src/store/pinnedFlowsStore.ts
git commit -m "feat: create pinnedFlowsStore — single source of truth for pin state"
```

---

## Task 3: Add `dashboardMyFlowsView` to `userPreferencesStore`

**Files:**
- Modify: `frontend/src/store/userPreferencesStore.ts`

**Step 1: Add preference**

In the `UserPreferencesState` interface, add:
```typescript
dashboardMyFlowsView: 'grid' | 'list' | 'table'
setDashboardMyFlowsView: (view: 'grid' | 'list' | 'table') => void
```

In the store implementation, add alongside existing fields:
```typescript
dashboardMyFlowsView: 'grid',
setDashboardMyFlowsView: (view) => set({ dashboardMyFlowsView: view }),
```

**Step 2: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

**Step 3: Commit**

```bash
git add frontend/src/store/userPreferencesStore.ts
git commit -m "feat: add dashboardMyFlowsView preference (independent from Library)"
```

---

## Task 4: Replace local pin state in Sidebar with store

**Files:**
- Modify: `frontend/src/components/layout/Sidebar.tsx`

**Step 1: Swap to store**

Remove from imports:
- `pinnedFlowsApi` import
- `PinnedFlow` type import

Add import:
```typescript
import { usePinnedFlowsStore } from '@/store/pinnedFlowsStore'
```

Remove from component:
- `const [pinnedFlows, setPinnedFlows] = useState<PinnedFlow[]>([])`
- The `pinnedFlowsApi.list()` call from the `useEffect`
- The entire `handleUnpin` function

Replace with:
```typescript
const pinnedItems = usePinnedFlowsStore((s) => s.items)
const loadPinned = usePinnedFlowsStore((s) => s.load)
const unpinFlow = usePinnedFlowsStore((s) => s.unpin)
```

In the `useEffect` that fetches data, remove the `pinnedFlowsApi.list()` from `Promise.all`. Add a separate call:
```typescript
useEffect(() => { loadPinned() }, [loadPinned])
```

Update `PinnedFlowsSection` props:
```tsx
<PinnedFlowsSection flows={pinnedItems} onUnpin={unpinFlow} />
```

**Step 2: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

**Step 3: Commit**

```bash
git add frontend/src/components/layout/Sidebar.tsx
git commit -m "refactor: sidebar uses pinnedFlowsStore instead of local state"
```

---

## Task 5: Create `usePaginationParams` hook

**Files:**
- Create: `frontend/src/hooks/usePaginationParams.ts`

**Step 1: Create the hook**

```typescript
import { useSearchParams } from 'react-router-dom'
import { useCallback, useMemo } from 'react'

type PageSize = number | 'all'

interface UsePaginationParamsOptions {
  defaultPageSize?: number
  allowedPageSizes?: PageSize[]
}

export function usePaginationParams(options: UsePaginationParamsOptions = {}) {
  const { defaultPageSize = 10, allowedPageSizes = [10, 25, 50, 'all'] } = options
  const [searchParams, setSearchParams] = useSearchParams()

  const page = useMemo(() => {
    const raw = searchParams.get('page')
    const n = raw ? parseInt(raw, 10) : 1
    return Number.isFinite(n) && n >= 1 ? n : 1
  }, [searchParams])

  const pageSize = useMemo((): PageSize => {
    const raw = searchParams.get('size')
    if (raw === 'all' && allowedPageSizes.includes('all')) return 'all'
    const n = raw ? parseInt(raw, 10) : defaultPageSize
    if (Number.isFinite(n) && allowedPageSizes.includes(n)) return n
    return defaultPageSize
  }, [searchParams, defaultPageSize, allowedPageSizes])

  const setPage = useCallback(
    (newPage: number) => {
      setSearchParams((prev) => {
        const next = new URLSearchParams(prev)
        if (newPage <= 1) {
          next.delete('page')
        } else {
          next.set('page', String(newPage))
        }
        return next
      })
    },
    [setSearchParams]
  )

  const setPageSize = useCallback(
    (newSize: PageSize) => {
      setSearchParams((prev) => {
        const next = new URLSearchParams(prev)
        next.set('size', String(newSize))
        next.delete('page') // reset to page 1
        return next
      })
    },
    [setSearchParams]
  )

  return { page, pageSize, setPage, setPageSize }
}
```

**Step 2: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

**Step 3: Commit**

```bash
git add frontend/src/hooks/usePaginationParams.ts
git commit -m "feat: create usePaginationParams hook — URL-synced pagination"
```

---

## Task 6: Create `useCachedQuota` hook

**Files:**
- Create: `frontend/src/hooks/useCachedQuota.ts`

**Step 1: Create the hook**

```typescript
import { useState, useEffect } from 'react'
import { aiBuilderApi } from '@/api'

const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes

let cachedResult: { aiEnabled: boolean; timestamp: number } | null = null

export function useCachedQuota() {
  const [aiEnabled, setAiEnabled] = useState(cachedResult?.aiEnabled ?? false)
  const [isLoading, setIsLoading] = useState(!cachedResult)

  useEffect(() => {
    if (cachedResult && Date.now() - cachedResult.timestamp < CACHE_TTL_MS) {
      setAiEnabled(cachedResult.aiEnabled)
      setIsLoading(false)
      return
    }

    setIsLoading(true)
    aiBuilderApi
      .getQuota()
      .then((q) => {
        cachedResult = { aiEnabled: q.ai_enabled, timestamp: Date.now() }
        setAiEnabled(q.ai_enabled)
      })
      .catch(() => {
        // Leave as false
      })
      .finally(() => setIsLoading(false))
  }, [])

  return { aiEnabled, isLoading }
}
```

**Step 2: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

**Step 3: Commit**

```bash
git add frontend/src/hooks/useCachedQuota.ts
git commit -m "feat: create useCachedQuota hook — 5-min TTL AI quota cache"
```

---

## Task 7: Add pin button props to `TreeGridView`

**Files:**
- Modify: `frontend/src/components/library/TreeGridView.tsx`

**Step 1: Read current file to understand structure**

Read `frontend/src/components/library/TreeGridView.tsx` fully before editing.

**Step 2: Add optional pin props to interface**

Add to `TreeGridViewProps`:
```typescript
pinnedTreeIds?: Set<string>
onTogglePin?: (treeId: string) => void
pinLoadingTreeIds?: Set<string>
```

**Step 3: Add star button to each card**

Import `Star` from `lucide-react`. In each card's top-right area, render (only when `onTogglePin` is provided):

```tsx
{onTogglePin && (
  <button
    onClick={(e) => {
      e.stopPropagation()
      e.preventDefault()
      onTogglePin(tree.id)
    }}
    disabled={pinLoadingTreeIds?.has(tree.id)}
    aria-label={pinnedTreeIds?.has(tree.id) ? 'Remove from favorites' : 'Add to favorites'}
    className={cn(
      'absolute top-3 right-3 rounded-md p-1 transition-colors',
      pinnedTreeIds?.has(tree.id)
        ? 'text-amber-400 hover:text-amber-300'
        : 'text-muted-foreground/40 hover:text-amber-400',
      pinLoadingTreeIds?.has(tree.id) && 'opacity-50 pointer-events-none'
    )}
  >
    <Star size={16} fill={pinnedTreeIds?.has(tree.id) ? 'currentColor' : 'none'} />
  </button>
)}
```

**Step 4: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

**Step 5: Commit**

```bash
git add frontend/src/components/library/TreeGridView.tsx
git commit -m "feat: add optional pin/favorite button to TreeGridView"
```

---

## Task 8: Add pin button props to `TreeListView`

**Files:**
- Modify: `frontend/src/components/library/TreeListView.tsx`

**Step 1: Read the file, then add same optional props and star button pattern as Task 7.**

Place the star button at the end of each row (before the actions area). Use the exact same props interface addition and button pattern from Task 7.

**Step 2: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

**Step 3: Commit**

```bash
git add frontend/src/components/library/TreeListView.tsx
git commit -m "feat: add optional pin/favorite button to TreeListView"
```

---

## Task 9: Add pin button props to `TreeTableView`

**Files:**
- Modify: `frontend/src/components/library/TreeTableView.tsx`

**Step 1: Read the file, then add same optional props.**

Add a narrow "Favorite" column as the leftmost column. Header: star icon. Cell: same button pattern from Task 7. Only render the column when `onTogglePin` is provided.

**Step 2: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

**Step 3: Commit**

```bash
git add frontend/src/components/library/TreeTableView.tsx
git commit -m "feat: add optional pin/favorite column to TreeTableView"
```

---

## Task 10: TreeLibraryPage — Create dropdown + AI Builder + pin wiring

**Files:**
- Modify: `frontend/src/pages/TreeLibraryPage.tsx`

**Step 1: Add imports**

```typescript
import { ChevronDown, Sparkles, FolderTree, ListOrdered, Wrench } from 'lucide-react'
import { usePinnedFlowsStore, selectPinnedTreeIds, selectPinLoadingTreeIds } from '@/store/pinnedFlowsStore'
import { useCachedQuota } from '@/hooks/useCachedQuota'
import { AIFlowBuilderModal } from '@/components/ai-builder/AIFlowBuilderModal'
```

**Step 2: Add state and store selectors**

Inside the component, add:
```typescript
const [showCreateMenu, setShowCreateMenu] = useState(false)
const [showAIBuilder, setShowAIBuilder] = useState(false)
const { aiEnabled } = useCachedQuota()

const pinnedTreeIds = usePinnedFlowsStore(selectPinnedTreeIds)
const pinLoadingTreeIds = usePinnedFlowsStore(selectPinLoadingTreeIds)
const togglePin = usePinnedFlowsStore((s) => s.toggle)
const loadPinned = usePinnedFlowsStore((s) => s.load)

useEffect(() => { loadPinned() }, [loadPinned])
```

**Step 3: Replace the `<Link>` create button**

Replace the single `<Link to={...}>` create button with the dropdown menu pattern from `MyTreesPage.tsx` (lines 134-197). Copy that exact pattern — it already has Troubleshooting Tree, Procedural Flow, Maintenance Flow, divider, and Build with AI (conditional on `aiEnabled`).

**Step 4: Pass pin props to view components**

Add to each `TreeGridView`, `TreeListView`, `TreeTableView` render:
```tsx
pinnedTreeIds={pinnedTreeIds}
onTogglePin={togglePin}
pinLoadingTreeIds={pinLoadingTreeIds}
```

**Step 5: Add AI Builder modal**

Before the closing `</div>` of the component, add:
```tsx
{showAIBuilder && (
  <AIFlowBuilderModal
    isOpen={showAIBuilder}
    onClose={() => setShowAIBuilder(false)}
  />
)}
```

**Step 6: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

**Step 7: Commit**

```bash
git add frontend/src/pages/TreeLibraryPage.tsx
git commit -m "feat: Library page — create dropdown with AI Builder + pin controls on all views"
```

---

## Task 11: QuickStartPage — Favorites section + paginated My Flows

**Files:**
- Modify: `frontend/src/pages/QuickStartPage.tsx`

This is the largest task. The page gets a major refactor.

**Step 1: Read the current file fully (already done in exploration)**

**Step 2: Rewrite QuickStartPage**

The page structure becomes:

1. **Page header** with "Create" dropdown (same pattern as Task 10)
2. **QuickStats** (keep existing)
3. **Search** (keep existing inline search)
4. **Recent Sessions** (keep existing `SessionsPanel`)
5. **Favorites section** (NEW — from `pinnedFlowsStore.items`)
6. **My Flows section** (NEW — paginated, `author_id` filter, view toggle)

Key implementation details:

**Favorites section:**
- Source: `usePinnedFlowsStore` items
- Layout: wrapping grid, max 2 rows (~8 cards). "View all favorites" expands if > 8.
- Each card: flow name + type emoji + unpin star button
- Skeleton: 4 pulse placeholder cards while `isLoading`
- Empty: "Star a flow to pin it here for quick access."

**My Flows section:**
- Source: `treesApi.list({ author_id: user.id, sort_by: 'updated_at', limit: pageSize + 1, skip: (page - 1) * pageSize })`
- Use `usePaginationParams({ defaultPageSize: 10, allowedPageSizes: [10, 25, 50, 'all'] })`
- `hasNextPage = response.length > pageSize; displayItems = response.slice(0, pageSize)`
- For "All": fetch in chunks of 100 up to 500 max
- View toggle: `ViewToggle` bound to `dashboardMyFlowsView`
- Render: `TreeGridView` / `TreeListView` / `TreeTableView` with pin props
- Pagination controls: `Prev` / `Next` / page label / size dropdown
- Skeleton: 6 placeholder cards/rows
- Empty: "You haven't created any flows yet." + "Create your first flow" CTA

**Remove:**
- The `FiltersBar` (All, Recently Used, My Flows, Team Flows) — replaced by the Favorites + My Flows structure
- The `SectionGroup` "All Flows" wrapper
- The hard-cap of 20 items

**Step 3: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npm run build`
Expected: Build succeeds with no errors

**Step 4: Commit**

```bash
git add frontend/src/pages/QuickStartPage.tsx
git commit -m "feat: dashboard — Favorites grid + paginated My Flows + skeletons + empty states"
```

---

## Task 12: PinnedFlowsSection — Dual collapse + smooth transitions

**Files:**
- Modify: `frontend/src/components/sidebar/PinnedFlowsSection.tsx`

**Step 1: Read the file (already done)**

**Step 2: Implement dual collapse**

Add state:
```typescript
const [showAll, setShowAll] = useState(false)
const TRUNCATE_COUNT = 5
```

Modify header collapse to reset truncation:
```typescript
const handleToggleCollapse = () => {
  if (collapsed) {
    setShowAll(false) // Reset to truncated on re-expand
  }
  setCollapsed(!collapsed)
}
```

Replace the flow list rendering:
```typescript
const visibleFlows = showAll ? flows : flows.slice(0, TRUNCATE_COUNT)
const hasMore = flows.length > TRUNCATE_COUNT
```

Add click handler on flow buttons that auto-collapses:
```typescript
onClick={() => {
  setShowAll(false) // Collapse back to 5
  navigate(getTreeNavigatePath(flow.tree_id, flow.tree_type))
}}
```

Add "Show more" / "Show less" link after the flow list:
```tsx
{hasMore && !collapsed && (
  <button
    onClick={() => setShowAll(!showAll)}
    className="px-3 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
  >
    {showAll ? 'Show less' : `Show more (${flows.length})`}
  </button>
)}
```

Add CSS transition on the list container:
```tsx
<div
  className="space-y-0.5 overflow-hidden transition-[max-height] duration-250 ease-out"
  style={{ maxHeight: collapsed ? 0 : showAll ? `${flows.length * 40 + 40}px` : `${TRUNCATE_COUNT * 40 + 40}px` }}
>
```

**Step 3: Verify build**

Run: `cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit`

**Step 4: Commit**

```bash
git add frontend/src/components/sidebar/PinnedFlowsSection.tsx
git commit -m "feat: sidebar pinned section — dual collapse + show more/less + smooth transitions"
```

---

## Task 13: Final build validation

**Step 1: Full build check**

Run: `cd /c/Dev/Projects/patherly/frontend && npm run build`
Expected: Build succeeds with zero errors

**Step 2: Fix any type errors or import issues**

If build fails, fix issues and amend the relevant commit.

**Step 3: Commit any final fixes**

```bash
git add -A
git commit -m "fix: resolve build errors from dashboard implementation"
```

---

## Summary of files changed

| # | File | Action |
|---|------|--------|
| 1 | `frontend/src/api/pinnedFlows.ts` | Add `pin()` method |
| 2 | `frontend/src/store/pinnedFlowsStore.ts` | **New** — Zustand pin store |
| 3 | `frontend/src/store/userPreferencesStore.ts` | Add `dashboardMyFlowsView` |
| 4 | `frontend/src/components/layout/Sidebar.tsx` | Use store instead of local state |
| 5 | `frontend/src/hooks/usePaginationParams.ts` | **New** — URL-synced pagination |
| 6 | `frontend/src/hooks/useCachedQuota.ts` | **New** — AI quota cache |
| 7 | `frontend/src/components/library/TreeGridView.tsx` | Optional pin button |
| 8 | `frontend/src/components/library/TreeListView.tsx` | Optional pin button |
| 9 | `frontend/src/components/library/TreeTableView.tsx` | Optional pin column |
| 10 | `frontend/src/pages/TreeLibraryPage.tsx` | Create dropdown + AI Builder + pins |
| 11 | `frontend/src/pages/QuickStartPage.tsx` | Major refactor: Favorites + My Flows |
| 12 | `frontend/src/components/sidebar/PinnedFlowsSection.tsx` | Dual collapse |
