/** * DashboardView - Main dashboard with "Productivity Luxury" aesthetic. * Inspired by Linear, Vercel, and Raycast design patterns. * Features: * - Subtle spotlight gradient * - Centralized command search with inline project filtering * - Border-first project cards with minimal backgrounds */ import React, { useEffect, useMemo, useState } from 'react'; import { api } from '@renderer/api'; import { useStore } from '@renderer/store'; import { getWorktreeNavigationState } from '@renderer/store/utils/stateResetHelpers'; import { formatProjectPath } from '@renderer/utils/pathDisplay'; import { buildTaskCountsByProject, normalizePath, type TaskStatusCounts, } from '@renderer/utils/pathNormalize'; import { formatShortcut } from '@renderer/utils/stringUtils'; import { createLogger } from '@shared/utils/logger'; import { useShallow } from 'zustand/react/shallow'; const logger = createLogger('Component:DashboardView'); import { formatDistanceToNow } from 'date-fns'; import { Command, FolderGit2, FolderOpen, GitBranch, Search, Settings, Users } from 'lucide-react'; import type { RepositoryGroup } from '@renderer/types/data'; // ============================================================================= // Command Search Input // ============================================================================= interface CommandSearchProps { value: string; onChange: (value: string) => void; } const CommandSearch = ({ value, onChange }: Readonly): React.JSX.Element => { const [isFocused, setIsFocused] = useState(false); const { openCommandPalette, selectedProjectId } = useStore( useShallow((s) => ({ openCommandPalette: s.openCommandPalette, selectedProjectId: s.selectedProjectId, })) ); // Handle Cmd+K to open full command palette useEffect(() => { const handleKeyDown = (e: KeyboardEvent): void => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openCommandPalette(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [openCommandPalette]); return (
{/* Search container with glow effect on focus */}
onChange(e.target.value)} placeholder="Search projects..." className="flex-1 bg-transparent text-sm text-text outline-none placeholder:text-text-muted" onFocus={() => setIsFocused(true)} onBlur={() => setIsFocused(false)} /> {/* Keyboard shortcut badge - opens full command palette */}
); }; // ============================================================================= // Repository Card // ============================================================================= interface RepositoryCardProps { repo: RepositoryGroup; onClick: () => void; isHighlighted?: boolean; taskCounts?: TaskStatusCounts; tasksLoading?: boolean; } const RepositoryCard = ({ repo, onClick, isHighlighted, taskCounts, tasksLoading, }: Readonly): React.JSX.Element => { const lastActivity = repo.mostRecentSession ? formatDistanceToNow(new Date(repo.mostRecentSession), { addSuffix: true }) : 'No recent activity'; const worktreeCount = repo.worktrees.length; const hasMultipleWorktrees = worktreeCount > 1; // Get the path from the first worktree const projectPath = repo.worktrees[0]?.path || ''; const formattedPath = formatProjectPath(projectPath); return ( ); }; // ============================================================================= // Ghost Card (New Project) // ============================================================================= interface WorktreeMatch { repoId: string; worktreeId: string; } function findMatchingWorktree( groups: RepositoryGroup[], selectedPath: string ): WorktreeMatch | null { const norm = normalizePath(selectedPath); for (const repo of groups) { for (const worktree of repo.worktrees) { if (normalizePath(worktree.path) === norm) { return { repoId: repo.id, worktreeId: worktree.id }; } } } return null; } const NewProjectCard = (): React.JSX.Element => { const { repositoryGroups, fetchRepositoryGroups } = useStore( useShallow((s) => ({ repositoryGroups: s.repositoryGroups, fetchRepositoryGroups: s.fetchRepositoryGroups, })) ); const navigateToMatch = (match: WorktreeMatch): void => { useStore.setState(getWorktreeNavigationState(match.repoId, match.worktreeId)); void useStore.getState().fetchSessionsInitial(match.worktreeId); }; const handleClick = async (): Promise => { try { const selectedPaths = await api.config.selectFolders(); if (!selectedPaths || selectedPaths.length === 0) { return; // User cancelled } const selectedPath = selectedPaths[0]; // Match selected path against known repository worktrees (normalized comparison) const match = findMatchingWorktree(repositoryGroups, selectedPath); if (match) { navigateToMatch(match); return; } // No match — refresh repository groups and retry await fetchRepositoryGroups(); const refreshedGroups = useStore.getState().repositoryGroups; const matchAfterRefresh = findMatchingWorktree(refreshedGroups, selectedPath); if (matchAfterRefresh) { navigateToMatch(matchAfterRefresh); return; } // Still no match — create a synthetic group for this new folder and navigate to it. // This allows launching teams in projects that don't have Claude sessions yet. const encodedId = selectedPath.replace(/[/\\]/g, '-'); const folderName = selectedPath.split(/[/\\]/).filter(Boolean).pop() ?? selectedPath; const now = Date.now(); const syntheticGroup: RepositoryGroup = { id: encodedId, identity: null, worktrees: [ { id: encodedId, path: selectedPath, name: folderName, isMainWorktree: true, source: 'unknown', sessions: [], createdAt: now, }, ], name: folderName, mostRecentSession: undefined, totalSessions: 0, }; useStore.setState((state) => ({ repositoryGroups: [syntheticGroup, ...state.repositoryGroups], })); navigateToMatch({ repoId: encodedId, worktreeId: encodedId }); } catch (error) { logger.error('Error selecting folder:', error); } }; return ( ); }; // ============================================================================= // Projects Grid // ============================================================================= interface ProjectsGridProps { searchQuery: string; maxProjects?: number; } const ProjectsGrid = ({ searchQuery, maxProjects = 12, }: Readonly): React.JSX.Element => { const { repositoryGroups, repositoryGroupsLoading, fetchRepositoryGroups, selectRepository, globalTasks, globalTasksLoading, fetchAllTasks, openTeamsTab, } = useStore( useShallow((s) => ({ repositoryGroups: s.repositoryGroups, repositoryGroupsLoading: s.repositoryGroupsLoading, fetchRepositoryGroups: s.fetchRepositoryGroups, selectRepository: s.selectRepository, globalTasks: s.globalTasks, globalTasksLoading: s.globalTasksLoading, fetchAllTasks: s.fetchAllTasks, openTeamsTab: s.openTeamsTab, })) ); const hasFetchedTasksRef = React.useRef(false); useEffect(() => { if (repositoryGroups.length === 0) { void fetchRepositoryGroups(); } if (!hasFetchedTasksRef.current) { hasFetchedTasksRef.current = true; void fetchAllTasks(); } }, [repositoryGroups.length, fetchRepositoryGroups, fetchAllTasks]); const taskCountsMap = useMemo(() => buildTaskCountsByProject(globalTasks), [globalTasks]); // Filter projects based on search query const filteredRepos = useMemo(() => { if (!searchQuery.trim()) { return repositoryGroups.slice(0, maxProjects); } const query = searchQuery.toLowerCase().trim(); return repositoryGroups .filter((repo) => { // Match by name if (repo.name.toLowerCase().includes(query)) return true; // Match by path const path = repo.worktrees[0]?.path || ''; if (path.toLowerCase().includes(query)) return true; return false; }) .slice(0, maxProjects); }, [repositoryGroups, searchQuery, maxProjects]); if (repositoryGroupsLoading) { // Organic widths per card — no repeating stamp const titleWidths = [60, 66, 50, 55, 75, 45, 40, 65]; const pathWidths = [80, 75, 85, 66, 70, 80, 60, 72]; return (
{Array.from({ length: 8 }).map((_, i) => (
{/* Icon placeholder */}
{/* Title placeholder */}
{/* Path placeholder */}
{/* Meta row placeholder */}
))}
); } if (filteredRepos.length === 0 && searchQuery.trim()) { return (

No projects found

No matches for "{searchQuery}"

); } if (repositoryGroups.length === 0) { return (

No projects found

~/.claude/projects/

); } return (
{filteredRepos.map((repo) => { const counts = repo.worktrees.reduce( (acc, wt) => { const c = taskCountsMap.get(normalizePath(wt.path)); if (c) { acc.pending += c.pending; acc.inProgress += c.inProgress; acc.completed += c.completed; } return acc; }, { pending: 0, inProgress: 0, completed: 0 } ); return ( { selectRepository(repo.id); openTeamsTab(); }} isHighlighted={!!searchQuery.trim()} taskCounts={globalTasksLoading ? undefined : counts} tasksLoading={globalTasksLoading} /> ); })} {!searchQuery.trim() && }
); }; // ============================================================================= // Dashboard View // ============================================================================= export const DashboardView = (): React.JSX.Element => { const [searchQuery, setSearchQuery] = useState(''); const { openSettingsTab, openTeamsTab } = useStore( useShallow((s) => ({ openSettingsTab: s.openSettingsTab, openTeamsTab: s.openTeamsTab, })) ); return (
{/* Spotlight gradient background */} ); };