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
@@ -7,7 +7,8 @@ import {
type SharedValue,
} from "react-native-reanimated";
import { type GestureType } from "react-native-gesture-handler";
import { useExplorerSidebarStore } from "@/stores/explorer-sidebar-store";
import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store";
const ANIMATION_DURATION = 220;
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
@@ -26,7 +27,13 @@ const ExplorerSidebarAnimationContext = createContext<ExplorerSidebarAnimationCo
export function ExplorerSidebarAnimationProvider({ children }: { children: ReactNode }) {
const { width: windowWidth } = useWindowDimensions();
const { isOpen } = useExplorerSidebarStore();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
// Derive isOpen from the unified panel state
const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
// Right sidebar: closed = +windowWidth (off-screen right), open = 0
const translateX = useSharedValue(isOpen ? 0 : windowWidth);
+10 -15
View File
@@ -362,10 +362,11 @@ export interface SessionContextValue {
createAgent: (options: {
config: any;
initialPrompt: string;
images?: Array<{ uri: string; mimeType?: string }>;
git?: any;
worktreeName?: string;
requestId?: string;
}) => void;
}) => Promise<unknown>;
setAgentMode: (agentId: string, modeId: string) => void;
respondToPermission: (
agentId: string,
@@ -2077,20 +2078,14 @@ export function SessionProvider({
error
);
}
void client
.createAgent({
config,
...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}),
...(imagesData && imagesData.length > 0
? { images: imagesData }
: {}),
...(git ? { git } : {}),
...(worktreeName ? { worktreeName } : {}),
...(requestId ? { requestId } : {}),
})
.catch((error) => {
console.error("[Session] Failed to create agent:", error);
});
return client.createAgent({
config,
...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}),
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
...(git ? { git } : {}),
...(worktreeName ? { worktreeName } : {}),
...(requestId ? { requestId } : {}),
});
},
[encodeImages, client]
);
@@ -6,7 +6,8 @@ import {
Easing,
type SharedValue,
} from "react-native-reanimated";
import { useSidebarStore } from "@/stores/sidebar-store";
import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store";
const ANIMATION_DURATION = 220;
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
@@ -24,7 +25,13 @@ const SidebarAnimationContext = createContext<SidebarAnimationContextValue | nul
export function SidebarAnimationProvider({ children }: { children: ReactNode }) {
const { width: windowWidth } = useWindowDimensions();
const { isOpen } = useSidebarStore();
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
// Derive isOpen from the unified panel state
const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
// Initialize based on current state
const translateX = useSharedValue(isOpen ? 0 : -windowWidth);