refactor(app): unified panel state machine for mobile sidebars

Replace separate sidebar stores with a unified panel store that uses a
discriminated union state machine on mobile. This makes it impossible
for both the agent list and file explorer sidebars to be open at the
same time on mobile.

Mobile state machine (mobileView):
- 'agent': Main agent view (no overlay panel)
- 'agent-list': Agent list sidebar (left overlay)
- 'file-explorer': File explorer sidebar (right overlay)

Desktop retains independent boolean toggles since sidebars sit
alongside content rather than overlaying it.

Key changes:
- Created stores/panel-store.ts with unified state
- Deleted stores/sidebar-store.ts and explorer-sidebar-store.ts
- Updated all components to use new panel store
- Animation contexts derive isOpen from unified state
This commit is contained in:
Mohamed Boudra
2026-01-16 16:19:49 +07:00
parent b8f056060f
commit ed50c168ad
58 changed files with 1579 additions and 893 deletions
@@ -4,13 +4,22 @@ import {
View,
Text,
Pressable,
ScrollView,
Modal,
TextInput,
ActivityIndicator,
useWindowDimensions,
ScrollView,
Platform,
} from "react-native";
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
import {
BottomSheetModal,
BottomSheetScrollView,
BottomSheetBackdrop,
BottomSheetTextInput,
BottomSheetBackgroundProps,
} from "@gorhom/bottom-sheet";
import Animated from "react-native-reanimated";
import { ChevronDown, ChevronRight, Pencil, Check, X } from "lucide-react-native";
import { theme as defaultTheme } from "@/styles/theme";
import type {
@@ -156,34 +165,78 @@ interface DropdownSheetProps {
children: ReactNode;
}
function DropdownSheetBackground({ style }: BottomSheetBackgroundProps) {
return (
<Animated.View
pointerEvents="none"
style={[style, styles.bottomSheetBackground]}
/>
);
}
export function DropdownSheet({
title,
visible,
onClose,
children,
}: DropdownSheetProps): ReactElement {
const bottomSheetRef = useRef<BottomSheetModal>(null);
const snapPoints = useMemo(() => ["60%", "90%"], []);
useEffect(() => {
if (visible) {
bottomSheetRef.current?.present();
} else {
bottomSheetRef.current?.dismiss();
}
}, [visible]);
const handleSheetChange = useCallback(
(index: number) => {
if (index === -1) {
onClose();
}
},
[onClose]
);
const renderBackdrop = useCallback(
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
opacity={0.45}
/>
),
[]
);
return (
<Modal
transparent
animationType="fade"
visible={visible}
onRequestClose={onClose}
<BottomSheetModal
ref={bottomSheetRef}
snapPoints={snapPoints}
index={0}
enableDynamicSizing={false}
onChange={handleSheetChange}
backdropComponent={renderBackdrop}
enablePanDownToClose
backgroundComponent={DropdownSheetBackground}
handleIndicatorStyle={styles.bottomSheetHandle}
keyboardBehavior="extend"
keyboardBlurBehavior="restore"
>
<View style={styles.dropdownSheetOverlay}>
<Pressable style={styles.dropdownSheetBackdrop} onPress={onClose} />
<View style={styles.dropdownSheetContainer}>
<View style={styles.dropdownSheetHandle} />
<Text style={styles.dropdownSheetTitle}>{title}</Text>
<ScrollView
contentContainerStyle={styles.dropdownSheetScrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{children}
</ScrollView>
</View>
<View style={styles.bottomSheetHeader}>
<Text style={styles.dropdownSheetTitle}>{title}</Text>
</View>
</Modal>
<BottomSheetScrollView
contentContainerStyle={styles.dropdownSheetScrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{children}
</BottomSheetScrollView>
</BottomSheetModal>
);
}
@@ -205,6 +258,8 @@ export function AdaptiveSelect({
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
const bottomSheetRef = useRef<BottomSheetModal>(null);
const snapPoints = useMemo(() => ["60%", "90%"], []);
const [dropdownPosition, setDropdownPosition] = useState({
top: 0,
left: 0,
@@ -240,29 +295,62 @@ export function AdaptiveSelect({
});
}, [visible, isMobile, anchorRef, windowWidth, windowHeight]);
useEffect(() => {
if (!isMobile) return;
if (visible) {
bottomSheetRef.current?.present();
} else {
bottomSheetRef.current?.dismiss();
}
}, [visible, isMobile]);
const handleSheetChange = useCallback(
(index: number) => {
if (index === -1) {
onClose();
}
},
[onClose]
);
const renderBackdrop = useCallback(
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
opacity={0.45}
/>
),
[]
);
if (isMobile) {
return (
<Modal
transparent
animationType="fade"
visible={visible}
onRequestClose={onClose}
<BottomSheetModal
ref={bottomSheetRef}
snapPoints={snapPoints}
index={0}
enableDynamicSizing={false}
onChange={handleSheetChange}
backdropComponent={renderBackdrop}
enablePanDownToClose
backgroundComponent={DropdownSheetBackground}
handleIndicatorStyle={styles.bottomSheetHandle}
keyboardBehavior="extend"
keyboardBlurBehavior="restore"
>
<View style={styles.dropdownSheetOverlay}>
<Pressable style={styles.dropdownSheetBackdrop} onPress={onClose} />
<View style={styles.dropdownSheetContainer}>
<View style={styles.dropdownSheetHandle} />
<Text style={styles.dropdownSheetTitle}>{title}</Text>
<ScrollView
contentContainerStyle={styles.dropdownSheetScrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{children}
</ScrollView>
</View>
<View style={styles.bottomSheetHeader}>
<Text style={styles.dropdownSheetTitle}>{title}</Text>
</View>
</Modal>
<BottomSheetScrollView
contentContainerStyle={styles.dropdownSheetScrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{children}
</BottomSheetScrollView>
</BottomSheetModal>
);
}
@@ -332,7 +420,6 @@ export function ComboSelect({
}: ComboSelectProps): ReactElement {
const [isOpen, setIsOpen] = useState(false);
const anchorRef = useRef<View>(null);
const inputRef = useRef<TextInput | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const selectedOption = options.find((opt) => opt.id === value);
@@ -347,7 +434,6 @@ export function ComboSelect({
useEffect(() => {
if (isOpen) {
setSearchQuery("");
inputRef.current?.focus();
}
}, [isOpen]);
@@ -400,16 +486,29 @@ export function ComboSelect({
onClose={handleClose}
anchorRef={anchorRef}
>
<TextInput
ref={inputRef}
style={styles.dropdownSearchInput}
placeholder={`Search ${label.toLowerCase()}...`}
placeholderTextColor={defaultTheme.colors.foregroundMuted}
value={searchQuery}
onChangeText={setSearchQuery}
autoCapitalize="none"
autoCorrect={false}
/>
{Platform.OS === "web" ? (
<TextInput
style={styles.dropdownSearchInput}
placeholder={`Search ${label.toLowerCase()}...`}
placeholderTextColor={defaultTheme.colors.foregroundMuted}
value={searchQuery}
onChangeText={setSearchQuery}
autoCapitalize="none"
autoCorrect={false}
autoFocus
/>
) : (
<BottomSheetTextInput
style={styles.dropdownSearchInput}
placeholder={`Search ${label.toLowerCase()}...`}
placeholderTextColor={defaultTheme.colors.foregroundMuted}
value={searchQuery}
onChangeText={setSearchQuery}
autoCapitalize="none"
autoCorrect={false}
autoFocus
/>
)}
{showCustomOption ? (
<View style={styles.dropdownSheetList}>
<Pressable
@@ -876,7 +975,6 @@ export function WorkingDirectoryDropdown({
}: WorkingDirectoryDropdownProps): ReactElement {
const [isOpen, setIsOpen] = useState(false);
const anchorRef = useRef<View>(null);
const inputRef = useRef<TextInput | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const handleOpen = useCallback(() => setIsOpen(true), []);
@@ -885,7 +983,6 @@ export function WorkingDirectoryDropdown({
useEffect(() => {
if (isOpen) {
setSearchQuery("");
inputRef.current?.focus();
}
}, [isOpen]);
@@ -929,16 +1026,29 @@ export function WorkingDirectoryDropdown({
onClose={handleClose}
anchorRef={anchorRef}
>
<TextInput
ref={inputRef}
style={styles.dropdownSearchInput}
placeholder="/path/to/project"
placeholderTextColor={defaultTheme.colors.foregroundMuted}
value={searchQuery}
onChangeText={setSearchQuery}
autoCapitalize="none"
autoCorrect={false}
/>
{Platform.OS === "web" ? (
<TextInput
style={styles.dropdownSearchInput}
placeholder="/path/to/project"
placeholderTextColor={defaultTheme.colors.foregroundMuted}
value={searchQuery}
onChangeText={setSearchQuery}
autoCapitalize="none"
autoCorrect={false}
autoFocus
/>
) : (
<BottomSheetTextInput
style={styles.dropdownSearchInput}
placeholder="/path/to/project"
placeholderTextColor={defaultTheme.colors.foregroundMuted}
value={searchQuery}
onChangeText={setSearchQuery}
autoCapitalize="none"
autoCorrect={false}
autoFocus
/>
)}
{!hasSuggestedPaths && !showCustomOption ? (
<Text style={styles.helperText}>
We'll suggest directories from agents on this host once they exist.
@@ -1208,6 +1318,18 @@ const styles = StyleSheet.create((theme) => ({
paddingVertical: theme.spacing[2],
color: theme.colors.foreground,
},
bottomSheetBackground: {
backgroundColor: theme.colors.surface2,
borderTopLeftRadius: theme.borderRadius["2xl"],
borderTopRightRadius: theme.borderRadius["2xl"],
},
bottomSheetHandle: {
backgroundColor: theme.colors.palette.zinc[600],
},
bottomSheetHeader: {
paddingHorizontal: theme.spacing[6],
paddingBottom: theme.spacing[2],
},
dropdownSheetOverlay: {
flex: 1,
justifyContent: "flex-end",
@@ -1244,7 +1366,6 @@ const styles = StyleSheet.create((theme) => ({
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
textAlign: "center",
marginBottom: theme.spacing[4],
},
dropdownSheetScrollContent: {
paddingBottom: theme.spacing[8],
@@ -231,14 +231,19 @@ export function AgentInputArea({
return;
}
const isControlled = value !== undefined;
const isControlledLocal = value !== undefined;
setSelectedImages([]);
setIsProcessing(true);
try {
await submitMessage(trimmedMessage, imageAttachments);
// Clear input only after successful submission
if (isControlled) {
// For controlled inputs with onSubmitMessage, the parent handles clearing
// because agent creation is async (WebSocket) and errors come back later
if (onSubmitMessageRef.current) {
// Parent manages input state - don't clear here
// Parent will clear on success via onChangeText
} else if (isControlledLocal) {
onChangeText?.("");
} else {
setUserInput("");
@@ -284,8 +289,13 @@ export function AgentInputArea({
}
}, [isAgentRunning, isConnected]);
// Hydrate draft only when switching agents
// Hydrate draft only when switching agents (uncontrolled mode only)
const isControlled = value !== undefined;
useEffect(() => {
// Skip draft hydration for controlled inputs - parent manages state
if (isControlled) {
return;
}
const draft = getDraftInput(agentId);
if (!draft) {
setUserInput("");
@@ -295,7 +305,7 @@ export function AgentInputArea({
setUserInput(draft.text);
setSelectedImages(draft.images as ImageAttachment[]);
}, [agentId, getDraftInput]);
}, [agentId, getDraftInput, isControlled]);
// Persist drafts into the shared session store with change detection to avoid redundant work
useEffect(() => {
@@ -29,7 +29,7 @@ import Animated, {
withTiming,
} from "react-native-reanimated";
import { ChevronDown } from "lucide-react-native";
import { useExplorerSidebarStore } from "@/stores/explorer-sidebar-store";
import { usePanelStore } from "@/stores/panel-store";
import {
AssistantMessage,
UserMessage,
@@ -87,8 +87,8 @@ export function AgentStreamView({
const hasAutoScrolledOnce = useRef(false);
const isNearBottomRef = useRef(true);
const streamItemCountRef = useRef(0);
const { open: openExplorer, setActiveTab: setExplorerTab } =
useExplorerSidebarStore();
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
const setExplorerTab = usePanelStore((state) => state.setExplorerTab);
// Get serverId (fallback to agent's serverId if not provided)
const resolvedServerId = serverId ?? agent.serverId ?? "";
@@ -142,7 +142,7 @@ export function AgentStreamView({
}
setExplorerTab("files");
openExplorer();
openFileExplorer();
},
[
agent.cwd,
@@ -150,7 +150,7 @@ export function AgentStreamView({
requestDirectoryListingOrInert,
requestFilePreviewOrInert,
setExplorerTab,
openExplorer,
openFileExplorer,
]
);
@@ -10,11 +10,11 @@ import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { X, GitBranch, Folder, LayoutGrid, List as ListIcon } from "lucide-react-native";
import {
useExplorerSidebarStore,
usePanelStore,
MIN_EXPLORER_SIDEBAR_WIDTH,
MAX_EXPLORER_SIDEBAR_WIDTH,
type ViewMode,
} from "@/stores/explorer-sidebar-store";
} from "@/stores/panel-store";
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
import { GitDiffPane } from "./git-diff-pane";
@@ -30,8 +30,21 @@ interface ExplorerSidebarProps {
export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const { isOpen, activeTab, width, viewMode, close, setActiveTab, setWidth, setViewMode } =
useExplorerSidebarStore();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const closeToAgent = usePanelStore((state) => state.closeToAgent);
const explorerTab = usePanelStore((state) => state.explorerTab);
const explorerWidth = usePanelStore((state) => state.explorerWidth);
const explorerViewMode = usePanelStore((state) => state.explorerViewMode);
const setExplorerTab = usePanelStore((state) => state.setExplorerTab);
const setExplorerWidth = usePanelStore((state) => state.setExplorerWidth);
const setExplorerViewMode = usePanelStore((state) => state.setExplorerViewMode);
// Derive isOpen from the unified panel state
const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
const {
translateX,
backdropOpacity,
@@ -42,22 +55,19 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) {
closeGestureRef,
} = useExplorerSidebarAnimation();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
// For resize drag, track the starting width
const startWidthRef = useRef(width);
const resizeWidth = useSharedValue(width);
const startWidthRef = useRef(explorerWidth);
const resizeWidth = useSharedValue(explorerWidth);
const handleClose = useCallback(() => {
close();
}, [close]);
closeToAgent();
}, [closeToAgent]);
const handleTabPress = useCallback(
(tab: ExplorerTab) => {
setActiveTab(tab);
setExplorerTab(tab);
},
[setActiveTab]
[setExplorerTab]
);
// Swipe gesture to close (swipe right on mobile)
@@ -116,8 +126,8 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) {
.enabled(!isMobile)
.hitSlop({ left: 8, right: 8, top: 0, bottom: 0 })
.onStart(() => {
startWidthRef.current = width;
resizeWidth.value = width;
startWidthRef.current = explorerWidth;
resizeWidth.value = explorerWidth;
})
.onUpdate((event) => {
// Dragging left (negative translationX) increases width
@@ -129,9 +139,9 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) {
resizeWidth.value = clampedWidth;
})
.onEnd(() => {
runOnJS(setWidth)(resizeWidth.value);
runOnJS(setExplorerWidth)(resizeWidth.value);
}),
[isMobile, width, resizeWidth, setWidth]
[isMobile, explorerWidth, resizeWidth, setExplorerWidth]
);
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
@@ -168,13 +178,13 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) {
pointerEvents="auto"
>
<SidebarContent
activeTab={activeTab}
activeTab={explorerTab}
onTabPress={handleTabPress}
onClose={handleClose}
serverId={serverId}
agentId={agentId}
fileViewMode={viewMode}
onFileViewModeChange={setViewMode}
fileViewMode={explorerViewMode}
onFileViewModeChange={setExplorerViewMode}
isMobile={isMobile}
/>
</Animated.View>
@@ -201,13 +211,13 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) {
</GestureDetector>
<SidebarContent
activeTab={activeTab}
activeTab={explorerTab}
onTabPress={handleTabPress}
onClose={handleClose}
serverId={serverId}
agentId={agentId}
fileViewMode={viewMode}
onFileViewModeChange={setViewMode}
fileViewMode={explorerViewMode}
onFileViewModeChange={setExplorerViewMode}
isMobile={false}
/>
</Animated.View>
@@ -46,9 +46,9 @@ import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import type { DaemonProfile } from "@/contexts/daemon-registry-context";
import { useSessionStore } from "@/stores/session-store";
import {
useExplorerSidebarStore,
usePanelStore,
type SortOption,
} from "@/stores/explorer-sidebar-store";
} from "@/stores/panel-store";
import { formatTimeAgo } from "@/utils/time";
const MAX_CONCURRENT_THUMBNAILS = 2;
@@ -90,7 +90,9 @@ export function FileExplorerPane({
const requestFilePreview = methods?.requestFilePreview;
const requestFileDownloadToken = methods?.requestFileDownloadToken;
const navigateExplorerBack = methods?.navigateExplorerBack;
const { viewMode, sortOption, setSortOption } = useExplorerSidebarStore();
const viewMode = usePanelStore((state) => state.explorerViewMode);
const sortOption = usePanelStore((state) => state.explorerSortOption);
const setSortOption = usePanelStore((state) => state.setExplorerSortOption);
const [selectedEntryPath, setSelectedEntryPath] = useState<string | null>(null);
const listScrollRef = useRef<FlatList<ExplorerEntry> | null>(null);
const listScrollOffsetRef = useRef(0);
@@ -3,7 +3,7 @@ import { Pressable, Text } from "react-native";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { Menu, PanelLeft } from "lucide-react-native";
import { ScreenHeader } from "./screen-header";
import { useSidebarStore } from "@/stores/sidebar-store";
import { usePanelStore } from "@/stores/panel-store";
interface MenuHeaderProps {
title?: string;
@@ -12,10 +12,13 @@ interface MenuHeaderProps {
export function MenuHeader({ title, rightContent }: MenuHeaderProps) {
const { theme } = useUnistyles();
const { isOpen, toggle } = useSidebarStore();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
const toggleAgentList = usePanelStore((state) => state.toggleAgentList);
const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
const MenuIcon = isMobile ? Menu : PanelLeft;
const menuIconColor = !isMobile && isOpen
? theme.colors.foreground
@@ -25,7 +28,7 @@ export function MenuHeader({ title, rightContent }: MenuHeaderProps) {
<ScreenHeader
left={
<>
<Pressable onPress={toggle} style={styles.menuButton}>
<Pressable onPress={toggleAgentList} style={styles.menuButton}>
<MenuIcon size={16} color={menuIconColor} />
</Pressable>
{title && (
@@ -28,7 +28,7 @@ import { useDictation } from "@/hooks/use-dictation";
import { DictationOverlay } from "./dictation-controls";
import type { DaemonClientV2 } from "@server/client/daemon-client-v2";
import type { SessionContextValue } from "@/contexts/session-context";
import { useSidebarStore } from "@/stores/sidebar-store";
import { usePanelStore } from "@/stores/panel-store";
export interface ImageAttachment {
uri: string;
@@ -117,7 +117,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
ref
) {
const { theme } = useUnistyles();
const toggleSidebar = useSidebarStore((state) => state.toggle);
const toggleAgentList = usePanelStore((state) => state.toggleAgentList);
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
const textInputRef = useRef<
TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
@@ -397,7 +397,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
// Cmd+B or Ctrl+B: toggle sidebar
if ((metaKey || ctrlKey) && event.nativeEvent.key === "b") {
event.preventDefault();
toggleSidebar();
toggleAgentList();
return;
}
+1
View File
@@ -649,6 +649,7 @@ export const AssistantMessage = memo(function AssistantMessage({
return (
<View
testID="assistant-message"
style={[
assistantMessageStylesheet.container,
!resolvedDisableOuterSpacing &&
+20 -15
View File
@@ -11,7 +11,7 @@ import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { Plus, Settings } from "lucide-react-native";
import { router } from "expo-router";
import { useSidebarStore } from "@/stores/sidebar-store";
import { usePanelStore } from "@/stores/panel-store";
import { AgentList } from "./agent-list";
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
@@ -26,7 +26,15 @@ interface SlidingSidebarProps {
export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const { isOpen, close } = useSidebarStore();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
const closeToAgent = usePanelStore((state) => state.closeToAgent);
// Derive isOpen from the unified panel state
const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
const { agents, isRevalidating, refreshAll } = useAggregatedAgents();
const {
translateX,
@@ -52,9 +60,6 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
}
}, [isRevalidating, isManualRefresh]);
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const sortedAgents = useMemo(() => {
return [...agents].sort((a, b) => {
if (a.requiresAttention && !b.requiresAttention) return -1;
@@ -70,15 +75,15 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
const hasMore = agents.length > SIDEBAR_AGENT_LIMIT;
const handleClose = useCallback(() => {
close();
}, [close]);
closeToAgent();
}, [closeToAgent]);
// Mobile: close sidebar and navigate
const handleCreateAgentMobile = useCallback(() => {
close();
closeToAgent();
router.push("/");
}, [close]);
}, [closeToAgent]);
// Desktop: just navigate, don't close
const handleCreateAgentDesktop = useCallback(() => {
@@ -87,9 +92,9 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
// Mobile: close sidebar and navigate
const handleSettingsMobile = useCallback(() => {
close();
closeToAgent();
router.push("/settings");
}, [close]);
}, [closeToAgent]);
// Desktop: just navigate, don't close
const handleSettingsDesktop = useCallback(() => {
@@ -101,17 +106,17 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
const handleAgentSelectMobile = useCallback(() => {
translateX.value = -windowWidth;
backdropOpacity.value = 0;
close();
}, [close, translateX, backdropOpacity, windowWidth]);
closeToAgent();
}, [closeToAgent, translateX, backdropOpacity, windowWidth]);
const handleViewMore = useCallback(() => {
if (isMobile) {
translateX.value = -windowWidth;
backdropOpacity.value = 0;
}
close();
closeToAgent();
router.push("/agents");
}, [backdropOpacity, close, isMobile, translateX, windowWidth]);
}, [backdropOpacity, closeToAgent, isMobile, translateX, windowWidth]);
// Close gesture (swipe left to close when sidebar is open)
const closeGesture = Gesture.Pan()