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.
This commit is contained in:
Mohamed Boudra
2026-03-29 23:10:45 +07:00
parent 487af3c822
commit e5fd61f131
4 changed files with 111 additions and 64 deletions
@@ -520,6 +520,7 @@ function MobileSidebar({
isRefreshing={isManualRefresh && isRevalidating} isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh} onRefresh={handleRefresh}
onWorkspacePress={closeToAgent} onWorkspacePress={closeToAgent}
onAddProject={handleOpenProject}
parentGestureRef={closeGestureRef} parentGestureRef={closeGestureRef}
/> />
)} )}
@@ -701,6 +702,7 @@ function DesktopSidebar({
projects={projects} projects={projects}
isRefreshing={isManualRefresh && isRevalidating} isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh} onRefresh={handleRefresh}
onAddProject={handleOpenProject}
/> />
)} )}
@@ -36,6 +36,7 @@ import {
GitPullRequest, GitPullRequest,
Monitor, Monitor,
MoreVertical, MoreVertical,
Plus,
} from "lucide-react-native"; } from "lucide-react-native";
import { NestableScrollContainer } from "react-native-draggable-flatlist"; import { NestableScrollContainer } from "react-native-draggable-flatlist";
import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list"; 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 { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader"; import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
import { getStatusDotColor } from "@/utils/status-dot-color"; import { getStatusDotColor } from "@/utils/status-dot-color";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Shortcut } from "@/components/ui/shortcut"; import { Shortcut } from "@/components/ui/shortcut";
import type { ShortcutKey } from "@/utils/format-shortcut"; import type { ShortcutKey } from "@/utils/format-shortcut";
@@ -113,6 +115,7 @@ interface SidebarWorkspaceListProps {
isRefreshing?: boolean; isRefreshing?: boolean;
onRefresh?: () => void; onRefresh?: () => void;
onWorkspacePress?: () => void; onWorkspacePress?: () => void;
onAddProject?: () => void;
listFooterComponent?: ReactElement | null; listFooterComponent?: ReactElement | null;
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */ /** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
parentGestureRef?: MutableRefObject<GestureType | undefined>; parentGestureRef?: MutableRefObject<GestureType | undefined>;
@@ -1631,6 +1634,7 @@ export function SidebarWorkspaceList({
isRefreshing = false, isRefreshing = false,
onRefresh, onRefresh,
onWorkspacePress, onWorkspacePress,
onAddProject,
listFooterComponent, listFooterComponent,
parentGestureRef, parentGestureRef,
}: SidebarWorkspaceListProps) { }: SidebarWorkspaceListProps) {
@@ -1900,7 +1904,18 @@ export function SidebarWorkspaceList({
const content = ( const content = (
<> <>
{projects.length === 0 ? ( {projects.length === 0 ? (
<Text style={styles.emptyText}>No projects yet</Text> <View style={styles.emptyContainer}>
<Text style={styles.emptyTitle}>No projects yet</Text>
<Text style={styles.emptyText}>Add a project to get started</Text>
<Button
variant="ghost"
size="sm"
leftIcon={Plus}
onPress={onAddProject}
>
Add project
</Button>
</View>
) : ( ) : (
<DraggableList <DraggableList
testID="sidebar-project-list" testID="sidebar-project-list"
@@ -1963,11 +1978,27 @@ const styles = StyleSheet.create((theme) => ({
marginBottom: theme.spacing[1], marginBottom: theme.spacing[1],
}, },
workspaceListContainer: {}, 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: { emptyText: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
textAlign: "center", textAlign: "center",
marginTop: theme.spacing[8],
marginHorizontal: theme.spacing[2],
}, },
projectRow: { projectRow: {
minHeight: 36, minHeight: 36,
@@ -1,27 +1,22 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { View, Text, Pressable } from "react-native"; import { View, Text } from "react-native";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { FolderOpen } from "lucide-react-native"; import { FolderOpen } from "lucide-react-native";
import { PaseoLogo } from "@/components/icons/paseo-logo"; 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 { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
import { usePanelStore } from "@/stores/panel-store"; 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 }) { 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 openAgentList = usePanelStore((s) => s.openAgentList);
const openProjectPicker = useOpenProjectPicker(serverId); 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 isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const collapsedSidebarInset =
!isMobile && !desktopAgentListOpen && trafficLightPadding.side
? trafficLightPadding
: { left: 0, right: 0 };
const dragHandlers = useDesktopDragHandlers(); const dragHandlers = useDesktopDragHandlers();
useEffect(() => { useEffect(() => {
@@ -32,22 +27,24 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
return ( return (
<View style={styles.container} {...dragHandlers}> <View style={styles.container} {...dragHandlers}>
<View style={[styles.menuToggle, { paddingTop: insets.top, paddingLeft: collapsedSidebarInset.left, paddingRight: collapsedSidebarInset.right }]}> <MenuHeader borderless />
<SidebarMenuToggle />
</View>
<View style={styles.content}> <View style={styles.content}>
<PaseoLogo size={56} /> <View style={styles.logo}>
<Text style={styles.heading}>What shall we build today?</Text> <PaseoLogo size={56} />
<Pressable </View>
style={({ hovered }) => [styles.openButton, hovered && styles.openButtonHovered]} <View style={styles.headingGroup}>
onPress={() => { <Text style={styles.heading}>What shall we build today?</Text>
void openProjectPicker(); {hasHydrated && !hasProjects ? (
}} <Text style={styles.subtitle}>
testID="open-project-submit" Add a project folder to start running agents on your codebase
> </Text>
<FolderOpen size={16} color={theme.colors.foregroundMuted} /> ) : null}
<Text style={styles.openButtonText}>Add a project</Text> </View>
</Pressable> <View style={styles.cta}>
<Button variant="default" leftIcon={FolderOpen} onPress={() => void openProjectPicker()} testID="open-project-submit">
Add a project
</Button>
</View>
</View> </View>
</View> </View>
); );
@@ -59,43 +56,32 @@ const styles = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.surface0, backgroundColor: theme.colors.surface0,
userSelect: "none", userSelect: "none",
}, },
menuToggle: {
position: "absolute",
top: theme.spacing[3],
left: theme.spacing[3],
zIndex: 1,
},
content: { content: {
flexGrow: 1, flexGrow: 1,
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
gap: theme.spacing[6], gap: 0,
padding: theme.spacing[6], padding: theme.spacing[6],
}, },
logo: {
marginBottom: theme.spacing[8],
},
headingGroup: {
alignItems: "center",
gap: theme.spacing[3],
},
cta: {
marginTop: theme.spacing[12],
},
heading: { heading: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize["2xl"], fontSize: theme.fontSize["2xl"],
fontWeight: theme.fontWeight.normal, fontWeight: theme.fontWeight.normal,
textAlign: "center", textAlign: "center",
}, },
openButton: { subtitle: {
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: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal, textAlign: "center",
}, },
})); }));
+36 -8
View File
@@ -1,10 +1,14 @@
import { useMemo, useState, useCallback, useEffect } from "react"; 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 { useIsFocused } from "@react-navigation/native";
import { router } from "expo-router";
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
import { ChevronLeft } from "lucide-react-native";
import { MenuHeader } from "@/components/headers/menu-header"; import { MenuHeader } from "@/components/headers/menu-header";
import { Button } from "@/components/ui/button";
import { AgentList } from "@/components/agent-list"; import { AgentList } from "@/components/agent-list";
import { useAllAgentsList } from "@/hooks/use-all-agents-list"; import { useAllAgentsList } from "@/hooks/use-all-agents-list";
import { buildHostOpenProjectRoute } from "@/utils/host-routes";
export function SessionsScreen({ serverId }: { serverId: string }) { export function SessionsScreen({ serverId }: { serverId: string }) {
const isFocused = useIsFocused(); const isFocused = useIsFocused();
@@ -44,13 +48,26 @@ function SessionsScreenContent({ serverId }: { serverId: string }) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<MenuHeader title="Sessions" /> <MenuHeader title="Sessions" />
<AgentList {sortedAgents.length === 0 ? (
agents={sortedAgents} <View style={styles.emptyContainer}>
showCheckoutInfo={false} <Text style={styles.emptyText}>No sessions yet</Text>
isRefreshing={isManualRefresh && isRevalidating} <Button
onRefresh={handleRefresh} variant="ghost"
showAttentionIndicator={false} leftIcon={ChevronLeft}
/> onPress={() => router.navigate(buildHostOpenProjectRoute(serverId) as any)}
>
Back
</Button>
</View>
) : (
<AgentList
agents={sortedAgents}
showCheckoutInfo={false}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
showAttentionIndicator={false}
/>
)}
</View> </View>
); );
} }
@@ -60,4 +77,15 @@ const styles = StyleSheet.create((theme) => ({
flex: 1, flex: 1,
backgroundColor: theme.colors.surface0, 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,
},
})); }));