From 3bd347068baacfe9d60eddb0a9382fceb1bfc1b4 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Thu, 5 Mar 2026 16:54:43 -0500 Subject: [PATCH 1/8] feat: redesign repo cards with owner/repo, timestamps, sort tabs (OPE-157) Repo cards now show: - owner/repo slug extracted from git_url (e.g. Effect-TS/effect) - File count + function count side by side - 'Indexed 2h ago' relative timestamp from last_indexed_at - Status dot instead of full badge (cleaner) Sort bar above the grid: - Recent (default): sorted by last_indexed_at descending - Name: alphabetical - Size: largest file count first - Repo count shown on right side Repository type updated with function_count, created_at, last_indexed_at. These fields already exist in the backend (select *), just weren't typed. 230 lines, same glow effect, premium feel. --- frontend/src/components/RepoList.tsx | 179 ++++++++++++++++++++------- frontend/src/types.ts | 3 + 2 files changed, 134 insertions(+), 48 deletions(-) diff --git a/frontend/src/components/RepoList.tsx b/frontend/src/components/RepoList.tsx index c76e77c..92bc4e6 100644 --- a/frontend/src/components/RepoList.tsx +++ b/frontend/src/components/RepoList.tsx @@ -1,6 +1,7 @@ import { useState, useRef, useMemo } from 'react' import { motion } from 'framer-motion' -import { FolderGit2, Plus } from 'lucide-react' +import { FolderGit2, Plus, Files, FunctionSquare, Clock } from 'lucide-react' +import { cn } from '@/lib/utils' import type { Repository } from '../types' import { RepoGridSkeleton } from './ui/Skeleton' @@ -12,38 +13,71 @@ interface RepoListProps { loading?: boolean } -const StatusBadge = ({ status }: { status: string }) => { +type SortMode = 'recent' | 'name' | 'size' + +/** Extract "owner/repo" from a GitHub URL */ +function parseRepoSlug(gitUrl: string): string { + try { + const cleaned = gitUrl.replace(/\.git$/, '') + const match = cleaned.match(/github\.com\/([^/]+\/[^/]+)/) + return match ? match[1] : '' + } catch { + return '' + } +} + +/** Relative time: "2h ago", "3d ago", "just now" */ +function timeAgo(dateStr?: string): string { + if (!dateStr) return '' + const now = Date.now() + const then = new Date(dateStr).getTime() + const diff = now - then + if (diff < 0) return '' + + const mins = Math.floor(diff / 60_000) + if (mins < 1) return 'just now' + if (mins < 60) return `${mins}m ago` + const hrs = Math.floor(mins / 60) + if (hrs < 24) return `${hrs}h ago` + const days = Math.floor(hrs / 24) + if (days < 30) return `${days}d ago` + return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +} + +const StatusDot = ({ status }: { status: string }) => { const isIndexed = status === 'indexed' - return ( - - + + {isIndexed ? 'Indexed' : 'Pending'} ) } -const RepoCard = ({ repo, index, onSelect }: { +const RepoCard = ({ repo, index, onSelect }: { repo: Repository index: number - onSelect: () => void + onSelect: () => void }) => { const cardRef = useRef(null) const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) const [hovering, setHovering] = useState(false) + const slug = parseRepoSlug(repo.git_url) + const indexed = timeAgo(repo.last_indexed_at) return ( { if (!cardRef.current) return @@ -56,7 +90,6 @@ const RepoCard = ({ repo, index, onSelect }: { bg-card border border-border hover:border-primary/40 focus:outline-none focus:ring-2 focus:ring-primary/50 p-5 transition-colors" > - {/* Mouse glow effect */} {hovering && (
)} - +
- {/* Header */} -
-
+ {/* Top row: icon + status */} +
+
- +
- {/* Title */} -

+ {/* Repo name + slug */} +

{repo.name}

-

{repo.branch}

- - {/* Stats */} -
-
- Files - - {(repo.file_count || 0).toLocaleString()} + {slug && ( +

{slug}

+ )} + + {/* Stats row */} +
+ + + {(repo.file_count || 0).toLocaleString()} + + {repo.function_count != null && repo.function_count > 0 && ( + + + {repo.function_count.toLocaleString()} -
+ )} + {indexed && ( + + + {indexed} + + )}
) } +const SortTab = ({ label, active, onClick }: { + label: string + active: boolean + onClick: () => void +}) => ( + +) + export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }: RepoListProps) { - // Hooks must be called before any conditional returns + const [sortMode, setSortMode] = useState('recent') + const sortedRepos = useMemo(() => { - return [...repos].sort((a, b) => { - if (a.status === 'indexed' && b.status !== 'indexed') return -1 - if (b.status === 'indexed' && a.status !== 'indexed') return 1 - return (b.file_count || 0) - (a.file_count || 0) - }) - }, [repos]) + const sorted = [...repos] + if (sortMode === 'recent') { + sorted.sort((a, b) => { + const aTime = a.last_indexed_at || a.created_at || '' + const bTime = b.last_indexed_at || b.created_at || '' + return bTime.localeCompare(aTime) + }) + } else if (sortMode === 'name') { + sorted.sort((a, b) => a.name.localeCompare(b.name)) + } else { + sorted.sort((a, b) => (b.file_count || 0) - (a.file_count || 0)) + } + return sorted + }, [repos, sortMode]) if (loading) return @@ -133,15 +205,26 @@ export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }: } return ( -
- {sortedRepos.map((repo, index) => ( - onSelect(repo.id)} - /> - ))} +
+ {/* Sort bar */} +
+ setSortMode('recent')} /> + setSortMode('name')} /> + setSortMode('size')} /> + {repos.length} repos +
+ + {/* Grid */} +
+ {sortedRepos.map((repo, index) => ( + onSelect(repo.id)} + /> + ))} +
) } diff --git a/frontend/src/types.ts b/frontend/src/types.ts index f954efc..8944d6d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -5,6 +5,9 @@ export interface Repository { branch: string status: string file_count: number + function_count?: number + created_at?: string + last_indexed_at?: string } export interface SearchResult { From 744b002361f715e4a52cfde28f9e8a4dfa22eecd Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Thu, 5 Mar 2026 20:29:46 -0500 Subject: [PATCH 2/8] fix: Settings page shows actual tier and repo limit instead of hardcoded 'Free' (OPE-158) Bug: Settings page had const MAX_REPOS = 3 hardcoded and 'Free tier limit' text for ALL users. Enterprise users saw '5/3 - 0 slots available'. Fix: Uses useUserUsage hook for actual tier and repo limit. - 'Free tier limit' -> 'Enterprise tier limit' (dynamic, capitalized) - '5/3' -> '5/10' for Enterprise (actual limit from backend) - Removed hardcoded MAX_REPOS constant Closes OPE-158 --- frontend/src/pages/SettingsPage.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index c3ef9d2..a7b480f 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -20,17 +20,20 @@ import { import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { toast } from 'sonner' import { API_URL } from '@/config/api' +import { useUserUsage } from '@/hooks/useCachedQuery' interface Repository { id: string name: string } -const MAX_REPOS = 3 const DELETE_CONFIRMATION_TEXT = 'delete all' export function SettingsPage() { const { user, session } = useAuth() + const { data: usage } = useUserUsage(session?.access_token, session?.user?.id) + const maxRepos = usage?.repositories?.limit ?? 1 + const tier = usage?.tier || 'free' const { status, checkStatus, disconnect, loading: githubLoading } = useGitHubRepos() const [repos, setRepos] = useState([]) @@ -135,7 +138,7 @@ export function SettingsPage() { } const isDeleteEnabled = deleteConfirmation === DELETE_CONFIRMATION_TEXT - const availableSlots = Math.max(0, MAX_REPOS - repos.length) + const availableSlots = Math.max(0, maxRepos - repos.length) return (
@@ -239,7 +242,7 @@ export function SettingsPage() {

Repository slots

-

Free tier limit

+

{tier} tier limit

{reposLoading ? ( @@ -248,7 +251,7 @@ export function SettingsPage() { <>

{repos.length} - / {MAX_REPOS} + / {maxRepos}

{availableSlots} slot{availableSlots !== 1 ? 's' : ''} available From 9ca8913368cd69e3b70a18d381d3e228c9b140f6 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Thu, 5 Mar 2026 20:38:43 -0500 Subject: [PATCH 3/8] feat: delete individual repos via three-dot menu + confirmation dialog (OPE-159) Each repo card now has a three-dot menu (visible on hover) with 'Delete repository'. Clicking shows confirmation dialog: 'This will permanently remove [name] and all its indexed data. This action cannot be undone.' Flow: hover card -> click ... -> click Delete -> confirm -> DELETE /repos/{id} - Toast notification on success/failure - Repo list refreshes automatically - If deleted repo was selected, clears selection Props chain: DashboardHome (handler) -> RepoListView -> RepoList -> RepoCard Closes OPE-159 --- frontend/src/components/RepoList.tsx | 78 +++++++++++++++++-- .../components/dashboard/DashboardHome.tsx | 18 +++++ .../src/components/dashboard/RepoListView.tsx | 3 + 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/RepoList.tsx b/frontend/src/components/RepoList.tsx index 92bc4e6..47ed726 100644 --- a/frontend/src/components/RepoList.tsx +++ b/frontend/src/components/RepoList.tsx @@ -1,7 +1,22 @@ import { useState, useRef, useMemo } from 'react' import { motion } from 'framer-motion' -import { FolderGit2, Plus, Files, FunctionSquare, Clock } from 'lucide-react' +import { FolderGit2, Plus, Files, FunctionSquare, Clock, MoreVertical, Trash2 } from 'lucide-react' import { cn } from '@/lib/utils' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from './ui/dropdown-menu' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from './ui/dialog' +import { Button } from './ui/button' import type { Repository } from '../types' import { RepoGridSkeleton } from './ui/Skeleton' @@ -9,6 +24,7 @@ interface RepoListProps { repos: Repository[] selectedRepo: string | null onSelect: (repoId: string) => void + onDelete?: (repoId: string) => void onAddClick?: () => void loading?: boolean } @@ -60,10 +76,11 @@ const StatusDot = ({ status }: { status: string }) => { ) } -const RepoCard = ({ repo, index, onSelect }: { +const RepoCard = ({ repo, index, onSelect, onDeleteClick }: { repo: Repository index: number onSelect: () => void + onDeleteClick?: () => void }) => { const cardRef = useRef(null) const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) @@ -100,12 +117,35 @@ const RepoCard = ({ repo, index, onSelect }: { )}

- {/* Top row: icon + status */} + {/* Top row: icon + status + menu */}
- +
+ + {onDeleteClick && ( + + + + + + { e.stopPropagation(); onDeleteClick() }} + className="text-destructive focus:text-destructive" + > + + Delete repository + + + + )} +
{/* Repo name + slug */} @@ -158,8 +198,9 @@ const SortTab = ({ label, active, onClick }: { ) -export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }: RepoListProps) { +export function RepoList({ repos, selectedRepo, onSelect, onDelete, onAddClick, loading }: RepoListProps) { const [sortMode, setSortMode] = useState('recent') + const [deleteTarget, setDeleteTarget] = useState(null) const sortedRepos = useMemo(() => { const sorted = [...repos] @@ -222,9 +263,36 @@ export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }: repo={repo} index={index} onSelect={() => onSelect(repo.id)} + onDeleteClick={onDelete ? () => setDeleteTarget(repo) : undefined} /> ))}
+ + {/* Delete confirmation dialog */} + !open && setDeleteTarget(null)}> + + + Delete repository + + This will permanently remove {deleteTarget?.name} and all its indexed data. This action cannot be undone. + + + + + + + +
) } diff --git a/frontend/src/components/dashboard/DashboardHome.tsx b/frontend/src/components/dashboard/DashboardHome.tsx index 7cdfd7c..5030928 100644 --- a/frontend/src/components/dashboard/DashboardHome.tsx +++ b/frontend/src/components/dashboard/DashboardHome.tsx @@ -212,6 +212,23 @@ export function DashboardHome() { } const selectedRepoData = repos.find((r) => r.id === selectedRepo) + const handleDeleteRepo = async (repoId: string) => { + try { + const response = await fetch(`${API_URL}/repos/${repoId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${session?.access_token}` }, + }) + if (!response.ok) throw new Error('Failed to delete') + toast.success('Repository deleted') + refreshRepos() + if (selectedRepo === repoId) setSelectedRepo(null) + } catch (error) { + toast.error('Failed to delete repository', { + description: error instanceof Error ? error.message : 'Please try again', + }) + } + } + const isRepoView = selectedRepo && selectedRepoData return ( @@ -226,6 +243,7 @@ export function DashboardHome() { selectedRepo={selectedRepo} maxRepos={maxRepos} onSelectRepo={(id) => { setSelectedRepo(id); setActiveTab('overview') }} + onDeleteRepo={handleDeleteRepo} onAddClick={() => setShowAddForm(true)} onGitHubClick={() => setShowGitHubSelector(true)} /> diff --git a/frontend/src/components/dashboard/RepoListView.tsx b/frontend/src/components/dashboard/RepoListView.tsx index 26b3ae0..9f0d4fe 100644 --- a/frontend/src/components/dashboard/RepoListView.tsx +++ b/frontend/src/components/dashboard/RepoListView.tsx @@ -15,6 +15,7 @@ interface RepoListViewProps { selectedRepo: string | null maxRepos: number onSelectRepo: (id: string) => void + onDeleteRepo: (id: string) => void onAddClick: () => void onGitHubClick: () => void } @@ -26,6 +27,7 @@ export function RepoListView({ selectedRepo, maxRepos, onSelectRepo, + onDeleteRepo, onAddClick, onGitHubClick, }: RepoListViewProps) { @@ -74,6 +76,7 @@ export function RepoListView({ selectedRepo={selectedRepo} loading={reposLoading} onSelect={onSelectRepo} + onDelete={onDeleteRepo} onAddClick={onAddClick} /> From 27936bad5ec4078a931932a348fcdb4b84480f94 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Thu, 5 Mar 2026 20:49:17 -0500 Subject: [PATCH 4/8] feat: add delete option inside repo detail view via three-dot menu Three-dot menu in top-right of repo detail header (next to repo name). Click '...' -> 'Delete repository' -> confirmation dialog -> DELETE API. Delete is now available in TWO places: 1. Repo card grid (hover card -> three dots) 2. Inside repo detail view (top-right header) Both use same confirmation dialog and same handleDeleteRepo handler. After delete, navigates back to repo list automatically. --- .../components/dashboard/DashboardHome.tsx | 1 + .../components/dashboard/RepoDetailView.tsx | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/frontend/src/components/dashboard/DashboardHome.tsx b/frontend/src/components/dashboard/DashboardHome.tsx index 5030928..5a77c64 100644 --- a/frontend/src/components/dashboard/DashboardHome.tsx +++ b/frontend/src/components/dashboard/DashboardHome.tsx @@ -257,6 +257,7 @@ export function DashboardHome() { onTabChange={setActiveTab} onBack={() => { setSelectedRepo(null); setActiveTab('overview') }} onReindex={handleReindex} + onDelete={() => handleDeleteRepo(selectedRepo)} /> )} diff --git a/frontend/src/components/dashboard/RepoDetailView.tsx b/frontend/src/components/dashboard/RepoDetailView.tsx index dcc88d2..7bc6e5d 100644 --- a/frontend/src/components/dashboard/RepoDetailView.tsx +++ b/frontend/src/components/dashboard/RepoDetailView.tsx @@ -1,6 +1,7 @@ // Single repo detail view with tabs (Overview, Search, Dependencies, etc.) // Receives repo data and callbacks from DashboardHome +import { useState } from 'react' import { motion } from 'framer-motion' import { LayoutDashboard, @@ -11,7 +12,24 @@ import { ArrowLeft, FolderGit2, ExternalLink, + MoreVertical, + Trash2, } from 'lucide-react' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' import { SearchPanel } from '../SearchPanel' import { DependencyGraph } from '../DependencyGraph' import { RepoOverview } from '../RepoOverview' @@ -36,6 +54,7 @@ interface RepoDetailViewProps { onTabChange: (tab: RepoTab) => void onBack: () => void onReindex: () => void + onDelete?: () => void } export function RepoDetailView({ @@ -46,7 +65,9 @@ export function RepoDetailView({ onTabChange, onBack, onReindex, + onDelete, }: RepoDetailViewProps) { + const [showDeleteDialog, setShowDeleteDialog] = useState(false) return (
+ {onDelete && ( + + + + + + setShowDeleteDialog(true)} + className="text-destructive focus:text-destructive" + > + + Delete repository + + + + )}
+ + + + + Delete repository + + This will permanently remove {repo.name} and all its indexed data. This action cannot be undone. + + + + + + + +
{/* tab bar */} From a29e14321b2a7db51310679e6cad6b09be43edfd Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Thu, 5 Mar 2026 20:53:21 -0500 Subject: [PATCH 5/8] fix: type repo name to confirm delete -- prevents accidental deletion Both delete dialogs (card grid + detail view) now require typing the exact repo name before the Delete button enables. Shared DeleteConfirmDialog component: - Shows repo name in bold - Input field with placeholder matching repo name - Delete button disabled until text matches exactly - Button text: 'Delete opencodeintel' (includes name) - Input clears on cancel or close Reused in RepoList (card grid) and RepoDetailView (detail header). --- frontend/src/components/RepoList.tsx | 88 ++++++++++++++----- .../components/dashboard/RepoDetailView.tsx | 35 ++------ 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/frontend/src/components/RepoList.tsx b/frontend/src/components/RepoList.tsx index 47ed726..33a02d6 100644 --- a/frontend/src/components/RepoList.tsx +++ b/frontend/src/components/RepoList.tsx @@ -17,6 +17,7 @@ import { DialogTitle, } from './ui/dialog' import { Button } from './ui/button' +import { Input } from './ui/input' import type { Repository } from '../types' import { RepoGridSkeleton } from './ui/Skeleton' @@ -269,30 +270,69 @@ export function RepoList({ repos, selectedRepo, onSelect, onDelete, onAddClick,
{/* Delete confirmation dialog */} - !open && setDeleteTarget(null)}> - - - Delete repository - - This will permanently remove {deleteTarget?.name} and all its indexed data. This action cannot be undone. - - - - - - - - + setDeleteTarget(null)} + onConfirm={() => { + if (deleteTarget && onDelete) { + onDelete(deleteTarget.id) + setDeleteTarget(null) + } + }} + />
) } + + +export function DeleteConfirmDialog({ + repo, + onCancel, + onConfirm, +}: { + repo: Repository | null + onCancel: () => void + onConfirm: () => void +}) { + const [confirmText, setConfirmText] = useState('') + const repoName = repo?.name || '' + const isMatch = confirmText === repoName + + return ( + { if (!open) { setConfirmText(''); onCancel() } }} + > + + + Delete repository + + This will permanently remove {repoName} and all its + indexed data. This action cannot be undone. + + +
+

+ Type {repoName} to confirm +

+ setConfirmText(e.target.value)} + placeholder={repoName} + autoFocus + /> +
+ + + + +
+
+ ) +} diff --git a/frontend/src/components/dashboard/RepoDetailView.tsx b/frontend/src/components/dashboard/RepoDetailView.tsx index 7bc6e5d..edf5d04 100644 --- a/frontend/src/components/dashboard/RepoDetailView.tsx +++ b/frontend/src/components/dashboard/RepoDetailView.tsx @@ -21,15 +21,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' +import { DeleteConfirmDialog } from '../RepoList' import { SearchPanel } from '../SearchPanel' import { DependencyGraph } from '../DependencyGraph' import { RepoOverview } from '../RepoOverview' @@ -68,6 +60,7 @@ export function RepoDetailView({ onDelete, }: RepoDetailViewProps) { const [showDeleteDialog, setShowDeleteDialog] = useState(false) + const deleteRepo = showDeleteDialog ? repo : null return ( - - - - Delete repository - - This will permanently remove {repo.name} and all its indexed data. This action cannot be undone. - - - - - - - - + setShowDeleteDialog(false)} + onConfirm={() => { setShowDeleteDialog(false); onDelete?.() }} + />
{/* tab bar */} From 673de1c871f05f5f65f5d15758a2e43c566d8165 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Fri, 6 Mar 2026 19:48:33 -0500 Subject: [PATCH 6/8] fix: StatusDot shows 'Failed' state in red, parseRepoSlug handles SSH URLs StatusDot: added 'failed' status with text-destructive/bg-destructive. Previously showed failed repos as 'Pending' with pulsing dot. parseRepoSlug: now matches both HTTPS (github.com/owner/repo) and SSH (git@github.com:owner/repo) URL formats. Skipped: function_count > 0 guard (intentional -- 0 before indexing is misleading), SortTab -> shadcn Tabs (custom component is simpler). --- frontend/src/components/RepoList.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/RepoList.tsx b/frontend/src/components/RepoList.tsx index 33a02d6..df8930f 100644 --- a/frontend/src/components/RepoList.tsx +++ b/frontend/src/components/RepoList.tsx @@ -36,8 +36,13 @@ type SortMode = 'recent' | 'name' | 'size' function parseRepoSlug(gitUrl: string): string { try { const cleaned = gitUrl.replace(/\.git$/, '') - const match = cleaned.match(/github\.com\/([^/]+\/[^/]+)/) - return match ? match[1] : '' + // Match HTTPS: github.com/owner/repo + const https = cleaned.match(/github\.com\/([^/]+\/[^/]+)/) + if (https) return https[1] + // Match SSH: git@github.com:owner/repo + const ssh = cleaned.match(/github\.com:([^/]+\/[^/]+)/) + if (ssh) return ssh[1] + return '' } catch { return '' } @@ -63,16 +68,17 @@ function timeAgo(dateStr?: string): string { const StatusDot = ({ status }: { status: string }) => { const isIndexed = status === 'indexed' + const isFailed = status === 'failed' return ( - {isIndexed ? 'Indexed' : 'Pending'} + {isIndexed ? 'Indexed' : isFailed ? 'Failed' : 'Pending'} ) } From 6de079c1bcb2b5f732a71a2fffc85a10d30ccc81 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Fri, 6 Mar 2026 19:55:09 -0500 Subject: [PATCH 7/8] refactor: replace custom SortTab with shadcn Tabs for sort controls Swapped the hand-rolled SortTab pill buttons for shadcn/ui Tabs (Radix TabsPrimitive). Gets us keyboard navigation, ARIA roles, and consistent styling from the design system for free. Removed 18 lines of custom component, added 6 lines of shadcn usage. --- frontend/src/components/RepoList.tsx | 31 ++++++++-------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/RepoList.tsx b/frontend/src/components/RepoList.tsx index df8930f..66ed3b6 100644 --- a/frontend/src/components/RepoList.tsx +++ b/frontend/src/components/RepoList.tsx @@ -18,6 +18,7 @@ import { } from './ui/dialog' import { Button } from './ui/button' import { Input } from './ui/input' +import { Tabs, TabsList, TabsTrigger } from './ui/tabs' import type { Repository } from '../types' import { RepoGridSkeleton } from './ui/Skeleton' @@ -187,24 +188,6 @@ const RepoCard = ({ repo, index, onSelect, onDeleteClick }: { ) } -const SortTab = ({ label, active, onClick }: { - label: string - active: boolean - onClick: () => void -}) => ( - -) - export function RepoList({ repos, selectedRepo, onSelect, onDelete, onAddClick, loading }: RepoListProps) { const [sortMode, setSortMode] = useState('recent') const [deleteTarget, setDeleteTarget] = useState(null) @@ -255,10 +238,14 @@ export function RepoList({ repos, selectedRepo, onSelect, onDelete, onAddClick, return (
{/* Sort bar */} -
- setSortMode('recent')} /> - setSortMode('name')} /> - setSortMode('size')} /> +
+ setSortMode(v as SortMode)}> + + Recent + Name + Size + + {repos.length} repos
From abbf6b17ad056e5043575efeb7500ed59850e2c6 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Fri, 6 Mar 2026 20:13:02 -0500 Subject: [PATCH 8/8] fix: sort by last_indexed_at first, add aria-label to delete confirm input Sort: never-indexed repos no longer jump ahead of recently indexed ones. last_indexed_at is primary sort key, created_at is tiebreaker only. Accessibility: delete confirmation input now has aria-label for screen readers. Skipped: function_count > 0 (intentional), motion.button nesting (large refactor), slug-based confirm (users think in repo names). --- frontend/src/components/RepoList.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/RepoList.tsx b/frontend/src/components/RepoList.tsx index 66ed3b6..a350dd5 100644 --- a/frontend/src/components/RepoList.tsx +++ b/frontend/src/components/RepoList.tsx @@ -196,9 +196,11 @@ export function RepoList({ repos, selectedRepo, onSelect, onDelete, onAddClick, const sorted = [...repos] if (sortMode === 'recent') { sorted.sort((a, b) => { - const aTime = a.last_indexed_at || a.created_at || '' - const bTime = b.last_indexed_at || b.created_at || '' - return bTime.localeCompare(aTime) + // Prefer last_indexed_at; use created_at only as tiebreaker + const aIdx = a.last_indexed_at || '' + const bIdx = b.last_indexed_at || '' + if (aIdx !== bIdx) return bIdx.localeCompare(aIdx) + return (b.created_at || '').localeCompare(a.created_at || '') }) } else if (sortMode === 'name') { sorted.sort((a, b) => a.name.localeCompare(b.name)) @@ -312,6 +314,7 @@ export function DeleteConfirmDialog({ value={confirmText} onChange={(e) => setConfirmText(e.target.value)} placeholder={repoName} + aria-label={`Type ${repoName} to confirm deletion`} autoFocus />