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
@@ -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>