From 2f17c08ff67ea1b25a169ae47e0ec152bed4ef9c Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Mon, 2 Mar 2026 12:48:32 -0500 Subject: [PATCH 1/7] refactor: replace DirectoryPicker card grid with vertical list layout (OPE-117) Card grid was hard to scan with 25+ packages -- cards wrapped unpredictably, proportional sizing added visual noise. New layout: clean vertical list, one row per package. - Checkbox on left, package name, file count, function estimate - Selected rows highlighted with bg-primary/5 + bold name - Dividers between rows, hover highlight - Staggered fade-in animation preserved (30ms per row) - Removed maxFiles/proportional sizing logic (not needed) Same props, same behavior, much easier to scan. --- frontend/src/components/DirectoryPicker.tsx | 57 ++++++++++----------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/frontend/src/components/DirectoryPicker.tsx b/frontend/src/components/DirectoryPicker.tsx index 3ea2ff7..83ed204 100644 --- a/frontend/src/components/DirectoryPicker.tsx +++ b/frontend/src/components/DirectoryPicker.tsx @@ -1,9 +1,9 @@ /** * DirectoryPicker -- monorepo package selection before indexing. * - * Shows an interactive card grid where each package is a clickable card - * sized proportionally to its file count. Users select which packages - * to index instead of the entire repo. + * Shows a clean vertical list where each package is a row with + * checkbox, name, file count, and function estimate. Users select + * which packages to index instead of the entire repo. */ import { useState, useMemo } from 'react' @@ -34,11 +34,6 @@ export function DirectoryPicker({ }: DirectoryPickerProps) { const [selected, setSelected] = useState>(new Set()) - const maxFiles = useMemo( - () => Math.max(...repoInfo.directories.map((d) => d.file_count), 1), - [repoInfo.directories], - ) - const stats = useMemo(() => { const dirs = repoInfo.directories.filter((d) => selected.has(d.path)) return { @@ -106,28 +101,27 @@ export function DirectoryPicker({ - + {repoInfo.directories.map((dir) => ( - toggleDir(dir.path)} /> @@ -198,37 +192,38 @@ function PickerHeader({ } -function PackageCard({ +function PackageRow({ dir, isSelected, - maxFiles, onToggle, }: { dir: DirectoryEntry isSelected: boolean - maxFiles: number onToggle: () => void }) { - // Scale card width: smallest = 120px, largest = 240px - const scale = dir.file_count / maxFiles - const minWidth = Math.round(120 + scale * 120) - return ( ) } From 257acb0d6f9fcdc9ab27bf29e946a1827048ec19 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Mon, 2 Mar 2026 13:00:08 -0500 Subject: [PATCH 2/7] feat: add column headers + sortable columns to DirectoryPicker - Header row: Package, Files, Functions labels above the list - Click any column header to sort by that field - Default: sorted by file count descending (largest first) - Click again to toggle ascending/descending - Active sort column highlighted with ArrowUpDown icon - SortButton component handles toggle logic Makes it easy to find specific packages in repos with 30+ dirs. --- frontend/src/components/DirectoryPicker.tsx | 66 ++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/DirectoryPicker.tsx b/frontend/src/components/DirectoryPicker.tsx index 83ed204..a34eb3a 100644 --- a/frontend/src/components/DirectoryPicker.tsx +++ b/frontend/src/components/DirectoryPicker.tsx @@ -8,13 +8,15 @@ import { useState, useMemo } from 'react' import { motion, AnimatePresence } from 'framer-motion' -import { FolderGit2, X, Files, FunctionSquare } from 'lucide-react' +import { FolderGit2, X, Files, FunctionSquare, ArrowUpDown } from 'lucide-react' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { ScrollArea } from '@/components/ui/scroll-area' import { cn } from '@/lib/utils' import type { AnalyzeResult, DirectoryEntry } from '@/types' +type SortKey = 'name' | 'files' | 'functions' + interface DirectoryPickerProps { isOpen: boolean onClose: () => void @@ -33,6 +35,25 @@ export function DirectoryPicker({ functionLimit, }: DirectoryPickerProps) { const [selected, setSelected] = useState>(new Set()) + const [sortBy, setSortBy] = useState('files') + const [sortAsc, setSortAsc] = useState(false) + + const sortedDirs = useMemo(() => { + const dirs = [...repoInfo.directories] + dirs.sort((a, b) => { + let cmp = 0 + if (sortBy === 'name') cmp = a.name.localeCompare(b.name) + else if (sortBy === 'files') cmp = a.file_count - b.file_count + else cmp = a.estimated_functions - b.estimated_functions + return sortAsc ? cmp : -cmp + }) + return dirs + }, [repoInfo.directories, sortBy, sortAsc]) + + function toggleSort(key: SortKey) { + if (sortBy === key) setSortAsc((prev) => !prev) + else { setSortBy(key); setSortAsc(key === 'name') } + } const stats = useMemo(() => { const dirs = repoInfo.directories.filter((d) => selected.has(d.path)) @@ -101,6 +122,13 @@ export function DirectoryPicker({ +
+ {/* checkbox spacer */} + + + +
+ - {repoInfo.directories.map((dir) => ( + {sortedDirs.map((dir) => ( void + className?: string +}) { + const active = current === sortKey + return ( + + ) +} + + function PackageRow({ dir, isSelected, From eabf178527a9c1445216c4d7693288b954fefe5f Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Mon, 2 Mar 2026 13:47:44 -0500 Subject: [PATCH 3/7] fix: compact DirectoryPicker layout + fix scroll - Removed redundant description text (UI is self-explanatory) - Select all + package count on one line (was taking full section) - Column header row tighter (py-1.5, subtle bg-muted/30 background) - Header padding reduced from p-6 to px-6 py-4 - Less vertical space eaten before ScrollArea = scroll actually works --- frontend/src/components/DirectoryPicker.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/DirectoryPicker.tsx b/frontend/src/components/DirectoryPicker.tsx index a34eb3a..d441778 100644 --- a/frontend/src/components/DirectoryPicker.tsx +++ b/frontend/src/components/DirectoryPicker.tsx @@ -106,11 +106,8 @@ export function DirectoryPicker({ loading={loading} /> -
-

- Select the packages you need for faster indexing and more focused results. -

-
+
+
+ + {repoInfo.directories.length} packages +
-
- {/* checkbox spacer */} +
+ @@ -186,7 +186,7 @@ function PickerHeader({ loading: boolean }) { return ( -
+
From 6e6f3bb3372e0d0205d42518e8b33f0fae765f72 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Mon, 2 Mar 2026 13:55:31 -0500 Subject: [PATCH 4/7] feat: dynamic tier limit from backend instead of hardcoded constant (OPE-118) New useUserUsage hook calls GET /api/v1/users/usage (React Query, 60s stale). Returns actual max_functions_per_repo for the user's tier from backend. DashboardHome now passes usage?.limits?.max_functions_per_repo to DirectoryPicker instead of TIER_FUNCTION_LIMITS[userTier]. Removed: TIER_FUNCTION_LIMITS import, TierName import, rawTier/userTier derivation from session metadata. All gone from DashboardHome. If user is on Pro (20K limit), budget bar shows X/20,000. If admin bumps them to Enterprise (500K), it updates on next page load. No frontend deploy needed when tier limits change on backend. --- .../components/dashboard/DashboardHome.tsx | 11 +++------- frontend/src/hooks/useCachedQuery.ts | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/dashboard/DashboardHome.tsx b/frontend/src/components/dashboard/DashboardHome.tsx index 4fdf346..3483284 100644 --- a/frontend/src/components/dashboard/DashboardHome.tsx +++ b/frontend/src/components/dashboard/DashboardHome.tsx @@ -6,7 +6,7 @@ import { useSearchParams } from 'react-router-dom' import { AnimatePresence } from 'framer-motion' import { toast } from 'sonner' import { useAuth } from '../../contexts/AuthContext' -import { useRepos } from '../../hooks/useCachedQuery' +import { useRepos, useUserUsage } from '../../hooks/useCachedQuery' import { API_URL, MAX_FREE_REPOS } from '../../config/api' import { extractErrorMessage, isUpgradeError } from '../../lib/api-errors' import { RepoListView } from './RepoListView' @@ -16,7 +16,6 @@ import { DirectoryPicker } from '../DirectoryPicker' import { GitHubRepoSelector } from '../GitHubRepoSelector' import { IndexingProgressModal } from '../IndexingProgressModal' import { UpgradeLimitModal } from '../UpgradeLimitModal' -import { TIER_FUNCTION_LIMITS, type TierName } from '../../config/api' import type { GitHubRepo } from '../../hooks/useGitHubRepos' import type { AnalyzeResult, RepoTab } from '../../types' @@ -24,10 +23,7 @@ export function DashboardHome() { const { session } = useAuth() const [searchParams, setSearchParams] = useSearchParams() const { data: repos = [], isLoading: reposLoading, invalidate: refreshRepos } = useRepos(session?.access_token) - - // User tier -- validate against known tiers, fall back to free for unknown values - const rawTier = session?.user?.user_metadata?.tier as string - const userTier: TierName = rawTier && rawTier in TIER_FUNCTION_LIMITS ? (rawTier as TierName) : 'free' + const { data: usage } = useUserUsage(session?.access_token) const [selectedRepo, setSelectedRepo] = useState(null) const [activeTab, setActiveTab] = useState('overview') @@ -260,8 +256,7 @@ export function DashboardHome() { repoInfo={analyzeResult} onConfirm={handleDirectoryConfirm} loading={loading} - // TODO: replace with actual user tier once GET /users/me returns tier - functionLimit={TIER_FUNCTION_LIMITS[userTier]} + functionLimit={usage?.limits?.max_functions_per_repo} /> )} diff --git a/frontend/src/hooks/useCachedQuery.ts b/frontend/src/hooks/useCachedQuery.ts index a694ed1..3175168 100644 --- a/frontend/src/hooks/useCachedQuery.ts +++ b/frontend/src/hooks/useCachedQuery.ts @@ -189,3 +189,24 @@ export function useRepos(apiKey: string | undefined) { return { ...query, invalidate } } + + +/** User usage and tier limits from backend -- single source of truth */ +export function useUserUsage(apiKey: string | undefined) { + return useQuery({ + queryKey: ['user', 'usage'], + queryFn: async () => { + const data = await fetchWithAuth(`${API_URL}/users/usage`, apiKey!) + return data as { + tier: string + limits: { + max_files_per_repo: number + max_functions_per_repo: number + playground_searches_per_day: number | null + } + } + }, + enabled: !!apiKey, + staleTime: 60_000, + }) +} From 9492a934f0b9ef1f9c4bbd9bf7ce7e7fe7223f07 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Mon, 2 Mar 2026 14:08:26 -0500 Subject: [PATCH 5/7] fix: replace Radix ScrollArea with native overflow-y-auto for reliable scroll Radix ScrollArea's Viewport uses h-full inside overflow-hidden Root, which fights with flex-1 in a flex column container. The list never scrolled because the Viewport expanded to fit all content. Native div with overflow-y-auto + flex-1 + min-h-0 works correctly. The browser handles scroll natively without Radix's abstraction layer. --- frontend/src/components/DirectoryPicker.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/DirectoryPicker.tsx b/frontend/src/components/DirectoryPicker.tsx index d441778..86e0618 100644 --- a/frontend/src/components/DirectoryPicker.tsx +++ b/frontend/src/components/DirectoryPicker.tsx @@ -11,7 +11,6 @@ import { motion, AnimatePresence } from 'framer-motion' import { FolderGit2, X, Files, FunctionSquare, ArrowUpDown } from 'lucide-react' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' -import { ScrollArea } from '@/components/ui/scroll-area' import { cn } from '@/lib/utils' import type { AnalyzeResult, DirectoryEntry } from '@/types' @@ -129,7 +128,7 @@ export function DirectoryPicker({
- +
))} - +
{functionLimit && ( From 79990403ee6b7bace679e116beff78546c6a7f50 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Mon, 2 Mar 2026 14:12:00 -0500 Subject: [PATCH 6/7] fix: use explicit max-height for list scroll instead of flex-1 flex-1 + min-h-0 + overflow-y-auto was not constraining the list. Replaced with explicit maxHeight: min(400px, 50vh). Simple, works. --- frontend/src/components/DirectoryPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/DirectoryPicker.tsx b/frontend/src/components/DirectoryPicker.tsx index 86e0618..dc40404 100644 --- a/frontend/src/components/DirectoryPicker.tsx +++ b/frontend/src/components/DirectoryPicker.tsx @@ -128,7 +128,7 @@ export function DirectoryPicker({
-
+
Date: Mon, 2 Mar 2026 14:20:20 -0500 Subject: [PATCH 7/7] fix: a11y nested button, file_count locale formatting, scoped query key 1. PackageRow: replaced nested button>checkbox with div[role=checkbox] and visual-only checkbox indicator. Keyboard Enter/Space handled. 2. file_count now uses toLocaleString() matching estimated_functions 3. useUserUsage query key includes userId to prevent cache bleed between users on shared devices --- frontend/src/components/DirectoryPicker.tsx | 17 ++++++++++++----- .../src/components/dashboard/DashboardHome.tsx | 2 +- frontend/src/hooks/useCachedQuery.ts | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/DirectoryPicker.tsx b/frontend/src/components/DirectoryPicker.tsx index dc40404..2b37f98 100644 --- a/frontend/src/components/DirectoryPicker.tsx +++ b/frontend/src/components/DirectoryPicker.tsx @@ -263,16 +263,23 @@ function PackageRow({ onToggle: () => void }) { return ( - +
) } diff --git a/frontend/src/components/dashboard/DashboardHome.tsx b/frontend/src/components/dashboard/DashboardHome.tsx index 3483284..1f20356 100644 --- a/frontend/src/components/dashboard/DashboardHome.tsx +++ b/frontend/src/components/dashboard/DashboardHome.tsx @@ -23,7 +23,7 @@ export function DashboardHome() { const { session } = useAuth() const [searchParams, setSearchParams] = useSearchParams() const { data: repos = [], isLoading: reposLoading, invalidate: refreshRepos } = useRepos(session?.access_token) - const { data: usage } = useUserUsage(session?.access_token) + const { data: usage } = useUserUsage(session?.access_token, session?.user?.id) const [selectedRepo, setSelectedRepo] = useState(null) const [activeTab, setActiveTab] = useState('overview') diff --git a/frontend/src/hooks/useCachedQuery.ts b/frontend/src/hooks/useCachedQuery.ts index 3175168..22555be 100644 --- a/frontend/src/hooks/useCachedQuery.ts +++ b/frontend/src/hooks/useCachedQuery.ts @@ -192,9 +192,9 @@ export function useRepos(apiKey: string | undefined) { /** User usage and tier limits from backend -- single source of truth */ -export function useUserUsage(apiKey: string | undefined) { +export function useUserUsage(apiKey: string | undefined, userId?: string) { return useQuery({ - queryKey: ['user', 'usage'], + queryKey: ['user', 'usage', userId], queryFn: async () => { const data = await fetchWithAuth(`${API_URL}/users/usage`, apiKey!) return data as {