import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { Platform } from "react-native"; /** * Mobile panel state machine. * * On mobile, exactly one panel can be visible at a time: * - 'agent': Main agent view (no overlay panel) * - 'agent-list': Agent list sidebar (left overlay) * - 'file-explorer': File explorer sidebar (right overlay) * * This makes impossible states unrepresentable - you cannot have both * sidebars open at the same time on mobile. */ type MobilePanelView = "agent" | "agent-list" | "file-explorer"; /** * Desktop sidebar state. * * On desktop, sidebars are independent toggleable panels that don't overlay * the main content - they sit alongside it. Both can be open simultaneously. */ interface DesktopSidebarState { agentListOpen: boolean; fileExplorerOpen: boolean; } export type ExplorerTab = "changes" | "files"; export type ViewMode = "list" | "grid"; export type SortOption = "name" | "modified" | "size"; export const DEFAULT_EXPLORER_SIDEBAR_WIDTH = 400; export const MIN_EXPLORER_SIDEBAR_WIDTH = 280; export const MAX_EXPLORER_SIDEBAR_WIDTH = 800; interface PanelState { // Mobile: which panel is currently shown mobileView: MobilePanelView; // Desktop: independent sidebar toggles desktop: DesktopSidebarState; // File explorer settings (shared between mobile/desktop) explorerTab: ExplorerTab; explorerWidth: number; explorerViewMode: ViewMode; explorerSortOption: SortOption; // Actions openAgentList: () => void; openFileExplorer: () => void; closeToAgent: () => void; toggleAgentList: () => void; toggleFileExplorer: () => void; // File explorer settings actions setExplorerTab: (tab: ExplorerTab) => void; setExplorerWidth: (width: number) => void; setExplorerViewMode: (mode: ViewMode) => void; setExplorerSortOption: (option: SortOption) => void; } function clampWidth(width: number): number { return Math.max(MIN_EXPLORER_SIDEBAR_WIDTH, Math.min(MAX_EXPLORER_SIDEBAR_WIDTH, width)); } const DEFAULT_DESKTOP_OPEN = Platform.OS === "web"; export const usePanelStore = create()( persist( (set) => ({ // Mobile always starts at agent view mobileView: "agent", // Desktop defaults based on platform desktop: { agentListOpen: DEFAULT_DESKTOP_OPEN, fileExplorerOpen: DEFAULT_DESKTOP_OPEN, }, // File explorer defaults explorerTab: "changes", explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH, explorerViewMode: "list", explorerSortOption: "name", openAgentList: () => set((state) => ({ mobileView: "agent-list", desktop: { ...state.desktop, agentListOpen: true }, })), openFileExplorer: () => set((state) => ({ mobileView: "file-explorer", desktop: { ...state.desktop, fileExplorerOpen: true }, })), closeToAgent: () => set((state) => ({ mobileView: "agent", // On desktop, closing depends on which panel triggered it // This is called when closing via gesture/backdrop, so we close the currently active mobile panel desktop: { agentListOpen: state.mobileView === "agent-list" ? false : state.desktop.agentListOpen, fileExplorerOpen: state.mobileView === "file-explorer" ? false : state.desktop.fileExplorerOpen, }, })), toggleAgentList: () => set((state) => { // Mobile: toggle between agent and agent-list const newMobileView = state.mobileView === "agent-list" ? "agent" : "agent-list"; return { mobileView: newMobileView, desktop: { ...state.desktop, agentListOpen: !state.desktop.agentListOpen, }, }; }), toggleFileExplorer: () => set((state) => { // Mobile: toggle between agent and file-explorer const newMobileView = state.mobileView === "file-explorer" ? "agent" : "file-explorer"; return { mobileView: newMobileView, desktop: { ...state.desktop, fileExplorerOpen: !state.desktop.fileExplorerOpen, }, }; }), setExplorerTab: (tab) => set({ explorerTab: tab }), setExplorerWidth: (width) => set({ explorerWidth: clampWidth(width) }), setExplorerViewMode: (mode) => set({ explorerViewMode: mode }), setExplorerSortOption: (option) => set({ explorerSortOption: option }), }), { name: "panel-state", storage: createJSONStorage(() => AsyncStorage), partialize: (state) => ({ mobileView: state.mobileView, desktop: state.desktop, explorerTab: state.explorerTab, explorerWidth: state.explorerWidth, explorerViewMode: state.explorerViewMode, explorerSortOption: state.explorerSortOption, }), } ) ); /** * Hook that provides platform-aware panel state. * * On mobile, uses the state machine (mobileView). * On desktop, uses independent booleans (desktop.agentListOpen, desktop.fileExplorerOpen). * * @param isMobile - Whether the current breakpoint is mobile */ export function usePanelState(isMobile: boolean) { const store = usePanelStore(); if (isMobile) { return { isAgentListOpen: store.mobileView === "agent-list", isFileExplorerOpen: store.mobileView === "file-explorer", openAgentList: store.openAgentList, openFileExplorer: store.openFileExplorer, closeAgentList: store.closeToAgent, closeFileExplorer: store.closeToAgent, toggleAgentList: store.toggleAgentList, toggleFileExplorer: store.toggleFileExplorer, // Explorer settings explorerTab: store.explorerTab, explorerWidth: store.explorerWidth, explorerViewMode: store.explorerViewMode, explorerSortOption: store.explorerSortOption, setExplorerTab: store.setExplorerTab, setExplorerWidth: store.setExplorerWidth, setExplorerViewMode: store.setExplorerViewMode, setExplorerSortOption: store.setExplorerSortOption, }; } // Desktop: independent toggles return { isAgentListOpen: store.desktop.agentListOpen, isFileExplorerOpen: store.desktop.fileExplorerOpen, openAgentList: store.openAgentList, openFileExplorer: store.openFileExplorer, closeAgentList: () => usePanelStore.setState((state) => ({ desktop: { ...state.desktop, agentListOpen: false }, })), closeFileExplorer: () => usePanelStore.setState((state) => ({ desktop: { ...state.desktop, fileExplorerOpen: false }, })), toggleAgentList: store.toggleAgentList, toggleFileExplorer: store.toggleFileExplorer, // Explorer settings explorerTab: store.explorerTab, explorerWidth: store.explorerWidth, explorerViewMode: store.explorerViewMode, explorerSortOption: store.explorerSortOption, setExplorerTab: store.setExplorerTab, setExplorerWidth: store.setExplorerWidth, setExplorerViewMode: store.setExplorerViewMode, setExplorerSortOption: store.setExplorerSortOption, }; }