From e5fd61f1313eea8819429efc8edf6237971aeefa Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 29 Mar 2026 23:10:45 +0700 Subject: [PATCH] feat(app): add empty states for sidebar, sessions, and open-project Sidebar shows a card with "No projects yet" and an Add project ghost button. Sessions screen shows "No sessions yet" with a ghost Back button. Open-project screen uses MenuHeader borderless, shared Button, and shows introductory text when no projects exist. --- packages/app/src/components/left-sidebar.tsx | 2 + .../src/components/sidebar-workspace-list.tsx | 37 +++++++- .../app/src/screens/open-project-screen.tsx | 92 ++++++++----------- packages/app/src/screens/sessions-screen.tsx | 44 +++++++-- 4 files changed, 111 insertions(+), 64 deletions(-) diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index 52d31a4d5..dfe51a89e 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -520,6 +520,7 @@ function MobileSidebar({ isRefreshing={isManualRefresh && isRevalidating} onRefresh={handleRefresh} onWorkspacePress={closeToAgent} + onAddProject={handleOpenProject} parentGestureRef={closeGestureRef} /> )} @@ -701,6 +702,7 @@ function DesktopSidebar({ projects={projects} isRefreshing={isManualRefresh && isRevalidating} onRefresh={handleRefresh} + onAddProject={handleOpenProject} /> )} diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index cda2b4c09..aced08534 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -36,6 +36,7 @@ import { GitPullRequest, Monitor, MoreVertical, + Plus, } from "lucide-react-native"; import { NestableScrollContainer } from "react-native-draggable-flatlist"; import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list"; @@ -73,6 +74,7 @@ import { confirmDialog } from "@/utils/confirm-dialog"; import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name"; import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader"; import { getStatusDotColor } from "@/utils/status-dot-color"; +import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Shortcut } from "@/components/ui/shortcut"; import type { ShortcutKey } from "@/utils/format-shortcut"; @@ -113,6 +115,7 @@ interface SidebarWorkspaceListProps { isRefreshing?: boolean; onRefresh?: () => void; onWorkspacePress?: () => void; + onAddProject?: () => void; listFooterComponent?: ReactElement | null; /** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */ parentGestureRef?: MutableRefObject; @@ -1631,6 +1634,7 @@ export function SidebarWorkspaceList({ isRefreshing = false, onRefresh, onWorkspacePress, + onAddProject, listFooterComponent, parentGestureRef, }: SidebarWorkspaceListProps) { @@ -1900,7 +1904,18 @@ export function SidebarWorkspaceList({ const content = ( <> {projects.length === 0 ? ( - No projects yet + + No projects yet + Add a project to get started + + ) : ( ({ marginBottom: theme.spacing[1], }, workspaceListContainer: {}, + emptyContainer: { + marginHorizontal: theme.spacing[2], + marginTop: theme.spacing[4], + paddingTop: theme.spacing[6], + paddingBottom: theme.spacing[4], + paddingHorizontal: theme.spacing[4], + borderRadius: theme.borderRadius.lg, + backgroundColor: theme.colors.surface0, + alignItems: "center", + gap: theme.spacing[3], + }, + emptyTitle: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + fontWeight: theme.fontWeight.medium, + textAlign: "center", + }, emptyText: { color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, textAlign: "center", - marginTop: theme.spacing[8], - marginHorizontal: theme.spacing[2], }, projectRow: { minHeight: 36, diff --git a/packages/app/src/screens/open-project-screen.tsx b/packages/app/src/screens/open-project-screen.tsx index e3e54be8e..a62755afa 100644 --- a/packages/app/src/screens/open-project-screen.tsx +++ b/packages/app/src/screens/open-project-screen.tsx @@ -1,27 +1,22 @@ import { useEffect } from "react"; -import { View, Text, Pressable } from "react-native"; -import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { View, Text } from "react-native"; +import { StyleSheet, UnistylesRuntime } from "react-native-unistyles"; import { FolderOpen } from "lucide-react-native"; import { PaseoLogo } from "@/components/icons/paseo-logo"; -import { SidebarMenuToggle } from "@/components/headers/menu-header"; +import { Button } from "@/components/ui/button"; +import { MenuHeader } from "@/components/headers/menu-header"; import { useOpenProjectPicker } from "@/hooks/use-open-project-picker"; import { usePanelStore } from "@/stores/panel-store"; -import { useDesktopDragHandlers, useTrafficLightPadding } from "@/utils/desktop-window"; +import { useSessionStore } from "@/stores/session-store"; +import { useDesktopDragHandlers } from "@/utils/desktop-window"; export function OpenProjectScreen({ serverId }: { serverId: string }) { - const { theme } = useUnistyles(); - const insets = useSafeAreaInsets(); - const trafficLightPadding = useTrafficLightPadding(); - const desktopAgentListOpen = usePanelStore((s) => s.desktop.agentListOpen); const openAgentList = usePanelStore((s) => s.openAgentList); const openProjectPicker = useOpenProjectPicker(serverId); + const hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false); + const hasProjects = useSessionStore((s) => (s.sessions[serverId]?.workspaces?.size ?? 0) > 0); const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; - const collapsedSidebarInset = - !isMobile && !desktopAgentListOpen && trafficLightPadding.side - ? trafficLightPadding - : { left: 0, right: 0 }; const dragHandlers = useDesktopDragHandlers(); useEffect(() => { @@ -32,22 +27,24 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) { return ( - - - + - - What shall we build today? - [styles.openButton, hovered && styles.openButtonHovered]} - onPress={() => { - void openProjectPicker(); - }} - testID="open-project-submit" - > - - Add a project - + + + + + What shall we build today? + {hasHydrated && !hasProjects ? ( + + Add a project folder to start running agents on your codebase + + ) : null} + + + + ); @@ -59,43 +56,32 @@ const styles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.surface0, userSelect: "none", }, - menuToggle: { - position: "absolute", - top: theme.spacing[3], - left: theme.spacing[3], - zIndex: 1, - }, content: { flexGrow: 1, justifyContent: "center", alignItems: "center", - gap: theme.spacing[6], + gap: 0, padding: theme.spacing[6], }, + logo: { + marginBottom: theme.spacing[8], + }, + headingGroup: { + alignItems: "center", + gap: theme.spacing[3], + }, + cta: { + marginTop: theme.spacing[12], + }, heading: { color: theme.colors.foreground, fontSize: theme.fontSize["2xl"], fontWeight: theme.fontWeight.normal, textAlign: "center", }, - openButton: { - flexDirection: "row", - alignItems: "center", - gap: theme.spacing[2], - paddingVertical: theme.spacing[3], - paddingHorizontal: theme.spacing[4], - borderRadius: theme.borderRadius.lg, - borderWidth: 1, - borderColor: theme.colors.border, - backgroundColor: "transparent", - }, - openButtonHovered: { - borderColor: theme.colors.borderAccent, - backgroundColor: theme.colors.surface1, - }, - openButtonText: { + subtitle: { color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.sm, - fontWeight: theme.fontWeight.normal, + fontSize: theme.fontSize.base, + textAlign: "center", }, })); diff --git a/packages/app/src/screens/sessions-screen.tsx b/packages/app/src/screens/sessions-screen.tsx index e775ad6cb..a6a4e3f50 100644 --- a/packages/app/src/screens/sessions-screen.tsx +++ b/packages/app/src/screens/sessions-screen.tsx @@ -1,10 +1,14 @@ import { useMemo, useState, useCallback, useEffect } from "react"; -import { View } from "react-native"; +import { View, Text } from "react-native"; import { useIsFocused } from "@react-navigation/native"; +import { router } from "expo-router"; import { StyleSheet } from "react-native-unistyles"; +import { ChevronLeft } from "lucide-react-native"; import { MenuHeader } from "@/components/headers/menu-header"; +import { Button } from "@/components/ui/button"; import { AgentList } from "@/components/agent-list"; import { useAllAgentsList } from "@/hooks/use-all-agents-list"; +import { buildHostOpenProjectRoute } from "@/utils/host-routes"; export function SessionsScreen({ serverId }: { serverId: string }) { const isFocused = useIsFocused(); @@ -44,13 +48,26 @@ function SessionsScreenContent({ serverId }: { serverId: string }) { return ( - + {sortedAgents.length === 0 ? ( + + No sessions yet + + + ) : ( + + )} ); } @@ -60,4 +77,15 @@ const styles = StyleSheet.create((theme) => ({ flex: 1, backgroundColor: theme.colors.surface0, }, + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + gap: theme.spacing[6], + padding: theme.spacing[6], + }, + emptyText: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.lg, + }, }));