Update files

This commit is contained in:
Mohamed Boudra
2026-01-26 14:30:41 +07:00
parent fe5ab731e0
commit 6095bf975b
2 changed files with 115 additions and 61 deletions
@@ -18,7 +18,7 @@ import {
import { router, usePathname } from "expo-router"; import { router, usePathname } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useQueries, useQueryClient, type UseQueryOptions } from "@tanstack/react-query"; import { useQueries, useQueryClient, type UseQueryOptions } from "@tanstack/react-query";
import { ChevronDown, ChevronRight } from "lucide-react-native"; import { ChevronDown, ChevronRight, Plus } from "lucide-react-native";
import { formatTimeAgo } from "@/utils/time"; import { formatTimeAgo } from "@/utils/time";
import { import {
groupAgents, groupAgents,
@@ -51,6 +51,8 @@ interface SectionData {
firstAgentServerId?: string; firstAgentServerId?: string;
/** For project sections, the first agent's id (to lookup checkout status) */ /** For project sections, the first agent's id (to lookup checkout status) */
firstAgentId?: string; firstAgentId?: string;
/** Working directory for the project (from first agent) */
workingDir?: string;
} }
interface GroupedAgentListProps { interface GroupedAgentListProps {
@@ -66,14 +68,17 @@ interface SectionHeaderProps {
section: SectionData; section: SectionData;
isCollapsed: boolean; isCollapsed: boolean;
onToggle: () => void; onToggle: () => void;
onCreateAgent: (workingDir: string) => void;
} }
function SectionHeader({ function SectionHeader({
section, section,
isCollapsed, isCollapsed,
onToggle, onToggle,
onCreateAgent,
}: SectionHeaderProps) { }: SectionHeaderProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const [isHovered, setIsHovered] = useState(false);
// For project sections, try to get repo name from checkout status // For project sections, try to get repo name from checkout status
const checkoutQuery = useCheckoutStatusCacheOnly({ const checkoutQuery = useCheckoutStatusCacheOnly({
@@ -98,13 +103,26 @@ function SectionHeader({
} }
} }
const handleCreatePress = useCallback(
(e: { stopPropagation: () => void }) => {
e.stopPropagation();
if (section.workingDir) {
onCreateAgent(section.workingDir);
}
},
[onCreateAgent, section.workingDir]
);
return ( return (
<Pressable <Pressable
style={({ hovered }) => [ style={[
styles.sectionHeader, styles.sectionHeader,
hovered && styles.sectionHeaderHovered, isHovered && styles.sectionHeaderHovered,
!isCollapsed && styles.sectionHeaderExpanded,
]} ]}
onPress={onToggle} onPress={onToggle}
onHoverIn={() => setIsHovered(true)}
onHoverOut={() => setIsHovered(false)}
> >
<View style={styles.sectionHeaderLeft}> <View style={styles.sectionHeaderLeft}>
<Text style={styles.sectionTitle} numberOfLines={1}> <Text style={styles.sectionTitle} numberOfLines={1}>
@@ -112,6 +130,24 @@ function SectionHeader({
</Text> </Text>
</View> </View>
<View style={styles.sectionHeaderRight}> <View style={styles.sectionHeaderRight}>
{section.workingDir && (
<Pressable
style={[
styles.createAgentButton,
{ opacity: isHovered ? 1 : 0 },
]}
onPress={handleCreatePress}
onHoverIn={() => setIsHovered(true)}
onHoverOut={() => setIsHovered(true)}
>
{({ hovered }) => (
<Plus
size={14}
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
/>
)}
</Pressable>
)}
{isCollapsed ? ( {isCollapsed ? (
<ChevronRight size={14} color={theme.colors.foregroundMuted} /> <ChevronRight size={14} color={theme.colors.foregroundMuted} />
) : ( ) : (
@@ -206,6 +242,13 @@ export function GroupedAgentList({
}); });
}, []); }, []);
const handleCreateAgentInProject = useCallback(
(workingDir: string) => {
router.push(`/agent?workingDir=${encodeURIComponent(workingDir)}` as any);
},
[]
);
// Subscribe to checkout status cache entries so project grouping can react // Subscribe to checkout status cache entries so project grouping can react
// to remote URL updates (e.g. git worktrees in different directories). // to remote URL updates (e.g. git worktrees in different directories).
const checkoutCacheQueries = useQueries({ const checkoutCacheQueries = useQueries({
@@ -260,6 +303,7 @@ export function GroupedAgentList({
data: isCollapsed ? [] : group.agents, data: isCollapsed ? [] : group.agents,
firstAgentServerId: firstAgent?.serverId, firstAgentServerId: firstAgent?.serverId,
firstAgentId: firstAgent?.id, firstAgentId: firstAgent?.id,
workingDir: firstAgent?.cwd,
}); });
} }
@@ -386,10 +430,11 @@ export function GroupedAgentList({
section={section} section={section}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
onToggle={() => toggleSection(section.key)} onToggle={() => toggleSection(section.key)}
onCreateAgent={handleCreateAgentInProject}
/> />
); );
}, },
[collapsedSections, toggleSection] [collapsedSections, toggleSection, handleCreateAgentInProject]
); );
const keyExtractor = useCallback( const keyExtractor = useCallback(
@@ -495,14 +540,17 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[4] + theme.spacing[2], paddingHorizontal: theme.spacing[3],
marginHorizontal: -theme.spacing[4], marginHorizontal: -theme.spacing[2],
marginTop: theme.spacing[2], marginTop: theme.spacing[2],
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
}, },
sectionHeaderHovered: { sectionHeaderHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surface1,
}, },
sectionHeaderExpanded: {
marginBottom: theme.spacing[1],
},
sectionHeaderLeft: { sectionHeaderLeft: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
@@ -511,13 +559,21 @@ const styles = StyleSheet.create((theme) => ({
minWidth: 0, minWidth: 0,
}, },
sectionHeaderRight: { sectionHeaderRight: {
flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
gap: theme.spacing[2],
marginLeft: theme.spacing[2], marginLeft: theme.spacing[2],
}, },
createAgentButton: {
width: 14,
height: 14,
alignItems: "center",
justifyContent: "center",
},
sectionTitle: { sectionTitle: {
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
fontWeight: "500", fontWeight: "normal",
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
flex: 1, flex: 1,
textAlign: "left", textAlign: "left",
@@ -525,6 +581,7 @@ const styles = StyleSheet.create((theme) => ({
agentItem: { agentItem: {
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[3],
marginHorizontal: -theme.spacing[2],
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
marginBottom: theme.spacing[1], marginBottom: theme.spacing[1],
}, },
+51 -54
View File
@@ -9,7 +9,7 @@ import Animated, {
} from "react-native-reanimated"; } from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { ChevronRight, Plus, Settings } from "lucide-react-native"; import { Plus, Settings, Users } from "lucide-react-native";
import { router } from "expo-router"; import { router } from "expo-router";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { GroupedAgentList } from "./grouped-agent-list"; import { GroupedAgentList } from "./grouped-agent-list";
@@ -170,20 +170,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
pointerEvents: backdropOpacity.value > 0.01 ? "auto" : "none", pointerEvents: backdropOpacity.value > 0.01 ? "auto" : "none",
})); }));
const viewMoreButton = (
<View style={styles.viewMoreContainer}>
<Pressable
style={({ hovered, pressed }) => [
styles.viewMoreButton,
(hovered || pressed) && styles.viewMoreButtonHovered,
]}
onPress={handleViewMore}
>
<Text style={styles.viewMoreButtonText}>All agents</Text>
<ChevronRight size={16} color={theme.colors.foregroundMuted} />
</Pressable>
</View>
);
// Render mobile sidebar // Render mobile sidebar
// On web, use "auto" instead of "box-none" because web's pointer-events: none blocks scroll // On web, use "auto" instead of "box-none" because web's pointer-events: none blocks scroll
@@ -228,17 +214,30 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
onRefresh={handleRefresh} onRefresh={handleRefresh}
selectedAgentId={selectedAgentId} selectedAgentId={selectedAgentId}
onAgentSelect={handleAgentSelectMobile} onAgentSelect={handleAgentSelectMobile}
listFooterComponent={viewMoreButton}
/> />
{/* Footer */} {/* Footer */}
<View style={styles.sidebarFooter}> <View style={styles.sidebarFooter}>
<Pressable <Pressable
style={styles.settingsButton} style={styles.footerButton}
onPress={handleViewMore}
>
{({ hovered }) => (
<>
<Users size={18} color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted} />
<Text style={[styles.footerButtonText, hovered && styles.footerButtonTextHovered]}>
All agents
</Text>
</>
)}
</Pressable>
<Pressable
style={styles.footerIconButton}
onPress={handleSettingsMobile} onPress={handleSettingsMobile}
> >
<Settings size={20} color={theme.colors.foregroundMuted} /> {({ hovered }) => (
<Text style={styles.settingsButtonText}>Settings</Text> <Settings size={20} color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted} />
)}
</Pressable> </Pressable>
</View> </View>
</View> </View>
@@ -279,17 +278,30 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
isRefreshing={isManualRefresh && isRevalidating} isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh} onRefresh={handleRefresh}
selectedAgentId={selectedAgentId} selectedAgentId={selectedAgentId}
listFooterComponent={viewMoreButton}
/> />
{/* Footer: Settings button */} {/* Footer */}
<View style={styles.sidebarFooter}> <View style={styles.sidebarFooter}>
<Pressable <Pressable
style={styles.settingsButton} style={styles.footerButton}
onPress={handleViewMore}
>
{({ hovered }) => (
<>
<Users size={18} color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted} />
<Text style={[styles.footerButtonText, hovered && styles.footerButtonTextHovered]}>
All agents
</Text>
</>
)}
</Pressable>
<Pressable
style={styles.footerIconButton}
onPress={handleSettingsDesktop} onPress={handleSettingsDesktop}
> >
<Settings size={20} color={theme.colors.foregroundMuted} /> {({ hovered }) => (
<Text style={styles.settingsButtonText}>Settings</Text> <Settings size={20} color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted} />
)}
</Pressable> </Pressable>
</View> </View>
</View> </View>
@@ -349,47 +361,32 @@ const styles = StyleSheet.create((theme) => ({
fontWeight: theme.fontWeight.normal, fontWeight: theme.fontWeight.normal,
color: theme.colors.foreground, color: theme.colors.foreground,
}, },
viewMoreContainer: { sidebarFooter: {
paddingTop: theme.spacing[2],
},
viewMoreButton: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "flex-start", justifyContent: "space-between",
gap: theme.spacing[1],
paddingVertical: theme.spacing[2],
paddingHorizontal: 0,
borderWidth: 0,
backgroundColor: "transparent",
alignSelf: "flex-start",
transitionProperty: "opacity",
transitionDuration: "150ms",
},
viewMoreButtonHovered: {
opacity: 0.8,
},
viewMoreButtonText: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal,
color: theme.colors.foregroundMuted,
},
sidebarFooter: {
paddingHorizontal: theme.spacing[4], paddingHorizontal: theme.spacing[4],
paddingTop: theme.spacing[2], paddingVertical: theme.spacing[3],
paddingBottom: theme.spacing[4],
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: theme.colors.border, borderTopColor: theme.colors.border,
}, },
settingsButton: { footerButton: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: theme.spacing[3], gap: theme.spacing[2],
paddingVertical: theme.spacing[2], paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[2], paddingHorizontal: theme.spacing[1],
}, },
settingsButtonText: { footerIconButton: {
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[1],
},
footerButtonText: {
fontSize: theme.fontSize.base, fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.normal, fontWeight: theme.fontWeight.normal,
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
}, },
footerButtonTextHovered: {
color: theme.colors.foreground,
},
})); }));