diff --git a/packages/app/src/components/grouped-agent-list.tsx b/packages/app/src/components/grouped-agent-list.tsx index 0ae915e9e..4d7a0f554 100644 --- a/packages/app/src/components/grouped-agent-list.tsx +++ b/packages/app/src/components/grouped-agent-list.tsx @@ -18,7 +18,7 @@ import { import { router, usePathname } from "expo-router"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; 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 { groupAgents, @@ -51,6 +51,8 @@ interface SectionData { firstAgentServerId?: string; /** For project sections, the first agent's id (to lookup checkout status) */ firstAgentId?: string; + /** Working directory for the project (from first agent) */ + workingDir?: string; } interface GroupedAgentListProps { @@ -66,14 +68,17 @@ interface SectionHeaderProps { section: SectionData; isCollapsed: boolean; onToggle: () => void; + onCreateAgent: (workingDir: string) => void; } function SectionHeader({ section, isCollapsed, onToggle, + onCreateAgent, }: SectionHeaderProps) { const { theme } = useUnistyles(); + const [isHovered, setIsHovered] = useState(false); // For project sections, try to get repo name from checkout status 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 ( [ + style={[ styles.sectionHeader, - hovered && styles.sectionHeaderHovered, + isHovered && styles.sectionHeaderHovered, + !isCollapsed && styles.sectionHeaderExpanded, ]} onPress={onToggle} + onHoverIn={() => setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} > @@ -112,6 +130,24 @@ function SectionHeader({ + {section.workingDir && ( + setIsHovered(true)} + onHoverOut={() => setIsHovered(true)} + > + {({ hovered }) => ( + + )} + + )} {isCollapsed ? ( ) : ( @@ -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 // to remote URL updates (e.g. git worktrees in different directories). const checkoutCacheQueries = useQueries({ @@ -260,6 +303,7 @@ export function GroupedAgentList({ data: isCollapsed ? [] : group.agents, firstAgentServerId: firstAgent?.serverId, firstAgentId: firstAgent?.id, + workingDir: firstAgent?.cwd, }); } @@ -386,10 +430,11 @@ export function GroupedAgentList({ section={section} isCollapsed={isCollapsed} onToggle={() => toggleSection(section.key)} + onCreateAgent={handleCreateAgentInProject} /> ); }, - [collapsedSections, toggleSection] + [collapsedSections, toggleSection, handleCreateAgentInProject] ); const keyExtractor = useCallback( @@ -495,14 +540,17 @@ const styles = StyleSheet.create((theme) => ({ alignItems: "center", justifyContent: "space-between", paddingVertical: theme.spacing[2], - paddingHorizontal: theme.spacing[4] + theme.spacing[2], - marginHorizontal: -theme.spacing[4], + paddingHorizontal: theme.spacing[3], + marginHorizontal: -theme.spacing[2], marginTop: theme.spacing[2], borderRadius: theme.borderRadius.md, }, sectionHeaderHovered: { backgroundColor: theme.colors.surface1, }, + sectionHeaderExpanded: { + marginBottom: theme.spacing[1], + }, sectionHeaderLeft: { flexDirection: "row", alignItems: "center", @@ -511,13 +559,21 @@ const styles = StyleSheet.create((theme) => ({ minWidth: 0, }, sectionHeaderRight: { + flexDirection: "row", alignItems: "center", justifyContent: "center", + gap: theme.spacing[2], marginLeft: theme.spacing[2], }, + createAgentButton: { + width: 14, + height: 14, + alignItems: "center", + justifyContent: "center", + }, sectionTitle: { fontSize: theme.fontSize.sm, - fontWeight: "500", + fontWeight: "normal", color: theme.colors.foregroundMuted, flex: 1, textAlign: "left", @@ -525,6 +581,7 @@ const styles = StyleSheet.create((theme) => ({ agentItem: { paddingVertical: theme.spacing[2], paddingHorizontal: theme.spacing[3], + marginHorizontal: -theme.spacing[2], borderRadius: theme.borderRadius.lg, marginBottom: theme.spacing[1], }, diff --git a/packages/app/src/components/sliding-sidebar.tsx b/packages/app/src/components/sliding-sidebar.tsx index e5bd873e5..350966e92 100644 --- a/packages/app/src/components/sliding-sidebar.tsx +++ b/packages/app/src/components/sliding-sidebar.tsx @@ -9,7 +9,7 @@ import Animated, { } from "react-native-reanimated"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; 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 { usePanelStore } from "@/stores/panel-store"; import { GroupedAgentList } from "./grouped-agent-list"; @@ -170,20 +170,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) { pointerEvents: backdropOpacity.value > 0.01 ? "auto" : "none", })); - const viewMoreButton = ( - - [ - styles.viewMoreButton, - (hovered || pressed) && styles.viewMoreButtonHovered, - ]} - onPress={handleViewMore} - > - All agents - - - - ); // Render mobile sidebar // 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} selectedAgentId={selectedAgentId} onAgentSelect={handleAgentSelectMobile} - listFooterComponent={viewMoreButton} /> {/* Footer */} + {({ hovered }) => ( + <> + + + All agents + + + )} + + - - Settings + {({ hovered }) => ( + + )} @@ -279,17 +278,30 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) { isRefreshing={isManualRefresh && isRevalidating} onRefresh={handleRefresh} selectedAgentId={selectedAgentId} - listFooterComponent={viewMoreButton} /> - {/* Footer: Settings button */} + {/* Footer */} + {({ hovered }) => ( + <> + + + All agents + + + )} + + - - Settings + {({ hovered }) => ( + + )} @@ -349,47 +361,32 @@ const styles = StyleSheet.create((theme) => ({ fontWeight: theme.fontWeight.normal, color: theme.colors.foreground, }, - viewMoreContainer: { - paddingTop: theme.spacing[2], - }, - viewMoreButton: { + sidebarFooter: { flexDirection: "row", alignItems: "center", - justifyContent: "flex-start", - 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: { + justifyContent: "space-between", paddingHorizontal: theme.spacing[4], - paddingTop: theme.spacing[2], - paddingBottom: theme.spacing[4], + paddingVertical: theme.spacing[3], borderTopWidth: 1, borderTopColor: theme.colors.border, }, - settingsButton: { + footerButton: { flexDirection: "row", alignItems: "center", - gap: theme.spacing[3], - paddingVertical: theme.spacing[2], - paddingHorizontal: theme.spacing[2], + gap: theme.spacing[2], + paddingVertical: theme.spacing[1], + paddingHorizontal: theme.spacing[1], }, - settingsButtonText: { + footerIconButton: { + paddingVertical: theme.spacing[1], + paddingHorizontal: theme.spacing[1], + }, + footerButtonText: { fontSize: theme.fontSize.base, fontWeight: theme.fontWeight.normal, color: theme.colors.foregroundMuted, }, + footerButtonTextHovered: { + color: theme.colors.foreground, + }, }));