Separate explorer checkout from panel state

This commit is contained in:
Mohamed Boudra
2026-04-20 12:40:39 +07:00
parent 9ea75afd37
commit f8411d121e
11 changed files with 295 additions and 188 deletions
+11 -7
View File
@@ -388,9 +388,11 @@ function AppContainer({
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const daemons = useHosts(); const daemons = useHosts();
const { settings, updateSettings } = useAppSettings(); const { settings, updateSettings } = useAppSettings();
const toggleAgentList = usePanelStore((state) => state.toggleAgentList); const toggleMobileAgentList = usePanelStore((state) => state.toggleMobileAgentList);
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer); const toggleMobileFileExplorer = usePanelStore((state) => state.toggleMobileFileExplorer);
const toggleBothSidebars = usePanelStore((state) => state.toggleBothSidebars); const toggleDesktopAgentList = usePanelStore((state) => state.toggleDesktopAgentList);
const toggleDesktopFileExplorer = usePanelStore((state) => state.toggleDesktopFileExplorer);
const toggleDesktopSidebars = usePanelStore((state) => state.toggleDesktopSidebars);
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode); const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled); const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
const agentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const agentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
@@ -405,6 +407,8 @@ function AppContainer({
const isCompactLayout = useIsCompactFormFactor(); const isCompactLayout = useIsCompactFormFactor();
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0; const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
const pathname = usePathname(); const pathname = usePathname();
const toggleAgentList = isCompactLayout ? toggleMobileAgentList : toggleDesktopAgentList;
const toggleFileExplorer = isCompactLayout ? toggleMobileFileExplorer : toggleDesktopFileExplorer;
// TODO: stop matching pathname here as a branch. `chromeEnabled` should not // TODO: stop matching pathname here as a branch. `chromeEnabled` should not
// conflate workspace/project-specific chrome (sidebar, mobile gesture) with // conflate workspace/project-specific chrome (sidebar, mobile gesture) with
// global concerns like keyboard shortcuts. Split those out so settings (and // global concerns like keyboard shortcuts. Split those out so settings (and
@@ -445,7 +449,7 @@ function AppContainer({
isMobile: isCompactLayout, isMobile: isCompactLayout,
toggleAgentList, toggleAgentList,
toggleFileExplorer, toggleFileExplorer,
toggleBothSidebars, toggleBothSidebars: toggleDesktopSidebars,
toggleFocusMode, toggleFocusMode,
cycleTheme, cycleTheme,
}); });
@@ -490,7 +494,7 @@ function MobileGestureWrapper({
chromeEnabled: boolean; chromeEnabled: boolean;
}) { }) {
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const openAgentList = usePanelStore((state) => state.openAgentList); const showMobileAgentList = usePanelStore((state) => state.showMobileAgentList);
const horizontalScroll = useHorizontalScrollOptional(); const horizontalScroll = useHorizontalScrollOptional();
const { const {
translateX, translateX,
@@ -507,8 +511,8 @@ function MobileGestureWrapper({
const handleGestureOpen = useCallback(() => { const handleGestureOpen = useCallback(() => {
gestureAnimatingRef.current = true; gestureAnimatingRef.current = true;
openAgentList(); showMobileAgentList();
}, [openAgentList, gestureAnimatingRef]); }, [showMobileAgentList, gestureAnimatingRef]);
const openGesture = useMemo( const openGesture = useMemo(
() => () =>
@@ -125,7 +125,8 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
const [expandedInlineToolCallIds, setExpandedInlineToolCallIds] = useState<Set<string>>( const [expandedInlineToolCallIds, setExpandedInlineToolCallIds] = useState<Set<string>>(
new Set(), new Set(),
); );
const openFileExplorer = usePanelStore((state) => state.openFileExplorer); const showMobileFileExplorer = usePanelStore((state) => state.showMobileFileExplorer);
const openDesktopFileExplorer = usePanelStore((state) => state.openDesktopFileExplorer);
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout); const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
// Get serverId (fallback to agent's serverId if not provided) // Get serverId (fallback to agent's serverId if not provided)
@@ -205,15 +206,21 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
isGit: agent.projectPlacement?.checkout?.isGit ?? true, isGit: agent.projectPlacement?.checkout?.isGit ?? true,
tab: "files", tab: "files",
}); });
openFileExplorer(); if (isMobile) {
showMobileFileExplorer();
return;
}
openDesktopFileExplorer();
}, },
[ [
agent.cwd, agent.cwd,
openFileExplorer, isMobile,
openDesktopFileExplorer,
requestDirectoryListing, requestDirectoryListing,
resolvedServerId, resolvedServerId,
router, router,
setExplorerTabForCheckout, setExplorerTabForCheckout,
showMobileFileExplorer,
openWorkspaceFile, openWorkspaceFile,
workspaceId, workspaceId,
], ],
@@ -54,7 +54,8 @@ export function ExplorerSidebar({
const isMobile = useIsCompactFormFactor(); const isMobile = useIsCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const closeToAgent = usePanelStore((state) => state.closeToAgent); const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
const closeDesktopFileExplorer = usePanelStore((state) => state.closeDesktopFileExplorer);
const explorerTab = usePanelStore((state) => state.explorerTab); const explorerTab = usePanelStore((state) => state.explorerTab);
const explorerWidth = usePanelStore((state) => state.explorerWidth); const explorerWidth = usePanelStore((state) => state.explorerWidth);
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout); const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
@@ -107,15 +108,26 @@ export function ExplorerSidebar({
mobileView, mobileView,
desktopFileExplorerOpen, desktopFileExplorerOpen,
}); });
closeToAgent(); if (isMobile) {
showMobileAgent();
return;
}
closeDesktopFileExplorer();
}, },
[closeToAgent, desktopFileExplorerOpen, isOpen, mobileView], [
closeDesktopFileExplorer,
desktopFileExplorerOpen,
isMobile,
isOpen,
mobileView,
showMobileAgent,
],
); );
const handleCloseFromGesture = useCallback(() => { const handleCloseFromGesture = useCallback(() => {
gestureAnimatingRef.current = true; gestureAnimatingRef.current = true;
closeToAgent(); showMobileAgent();
}, [closeToAgent, gestureAnimatingRef]); }, [gestureAnimatingRef, showMobileAgent]);
const enableSidebarCloseGesture = isMobile && isOpen; const enableSidebarCloseGesture = isMobile && isOpen;
@@ -48,7 +48,8 @@ export function SidebarMenuToggle({
const isMobile = useIsCompactFormFactor(); const isMobile = useIsCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
const toggleAgentList = usePanelStore((state) => state.toggleAgentList); const toggleMobileAgentList = usePanelStore((state) => state.toggleMobileAgentList);
const toggleDesktopAgentList = usePanelStore((state) => state.toggleDesktopAgentList);
const toggleShortcutKeys = getShortcutOs() === "mac" ? ["mod", "B"] : ["mod", "."]; const toggleShortcutKeys = getShortcutOs() === "mac" ? ["mod", "B"] : ["mod", "."];
const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen; const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
@@ -57,7 +58,7 @@ export function SidebarMenuToggle({
return ( return (
<HeaderToggleButton <HeaderToggleButton
onPress={toggleAgentList} onPress={isMobile ? toggleMobileAgentList : toggleDesktopAgentList}
tooltipLabel="Toggle sidebar" tooltipLabel="Toggle sidebar"
tooltipKeys={toggleShortcutKeys} tooltipKeys={toggleShortcutKeys}
tooltipSide={tooltipSide} tooltipSide={tooltipSide}
+6 -6
View File
@@ -118,7 +118,7 @@ export const LeftSidebar = memo(function LeftSidebar({
const isCompactLayout = useIsCompactFormFactor(); const isCompactLayout = useIsCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen); const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
const closeToAgent = usePanelStore((state) => state.closeToAgent); const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
const pathname = usePathname(); const pathname = usePathname();
const daemons = useHosts(); const daemons = useHosts();
const activeServerIdFromPath = useMemo(() => parseServerIdFromPathname(pathname), [pathname]); const activeServerIdFromPath = useMemo(() => parseServerIdFromPathname(pathname), [pathname]);
@@ -208,18 +208,18 @@ export const LeftSidebar = memo(function LeftSidebar({
const openProjectPicker = useOpenProjectPicker(activeServerId); const openProjectPicker = useOpenProjectPicker(activeServerId);
const handleOpenProjectMobile = useCallback(() => { const handleOpenProjectMobile = useCallback(() => {
closeToAgent(); showMobileAgent();
void openProjectPicker(); void openProjectPicker();
}, [closeToAgent, openProjectPicker]); }, [showMobileAgent, openProjectPicker]);
const handleOpenProjectDesktop = useCallback(() => { const handleOpenProjectDesktop = useCallback(() => {
void openProjectPicker(); void openProjectPicker();
}, [openProjectPicker]); }, [openProjectPicker]);
const handleSettingsMobile = useCallback(() => { const handleSettingsMobile = useCallback(() => {
closeToAgent(); showMobileAgent();
router.push(buildSettingsRoute()); router.push(buildSettingsRoute());
}, [closeToAgent]); }, [showMobileAgent]);
const handleSettingsDesktop = useCallback(() => { const handleSettingsDesktop = useCallback(() => {
router.push(buildSettingsRoute()); router.push(buildSettingsRoute());
@@ -272,7 +272,7 @@ export const LeftSidebar = memo(function LeftSidebar({
insetsTop={insets.top} insetsTop={insets.top}
insetsBottom={insets.bottom} insetsBottom={insets.bottom}
isOpen={isOpen} isOpen={isOpen}
closeToAgent={closeToAgent} closeToAgent={showMobileAgent}
handleOpenProject={handleOpenProjectMobile} handleOpenProject={handleOpenProjectMobile}
handleSettings={handleSettingsMobile} handleSettings={handleSettingsMobile}
handleViewMoreNavigate={handleViewMoreNavigate} handleViewMoreNavigate={handleViewMoreNavigate}
@@ -89,8 +89,8 @@ export function TerminalPane({ serverId, cwd, terminalId, isPaneFocused }: Termi
const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme]); const xtermTheme = useMemo(() => toXtermTheme(theme.colors.terminal), [theme]);
const isMobile = useIsCompactFormFactor(); const isMobile = useIsCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const openAgentList = usePanelStore((state) => state.openAgentList); const showMobileAgentList = usePanelStore((state) => state.showMobileAgentList);
const openFileExplorer = usePanelStore((state) => state.openFileExplorer); const showMobileFileExplorer = usePanelStore((state) => state.showMobileFileExplorer);
const swipeGesturesEnabled = isMobile && mobileView === "agent"; const swipeGesturesEnabled = isMobile && mobileView === "agent";
const { shift: keyboardShift, style: keyboardPaddingStyle } = useKeyboardShiftStyle({ const { shift: keyboardShift, style: keyboardPaddingStyle } = useKeyboardShiftStyle({
mode: "padding", mode: "padding",
@@ -573,13 +573,13 @@ export function TerminalPane({ serverId, cwd, terminalId, isPaneFocused }: Termi
if (!swipeGesturesEnabled) { if (!swipeGesturesEnabled) {
return; return;
} }
openAgentList(); showMobileAgentList();
}} }}
onSwipeLeft={() => { onSwipeLeft={() => {
if (!swipeGesturesEnabled) { if (!swipeGesturesEnabled) {
return; return;
} }
openFileExplorer(); showMobileFileExplorer();
}} }}
onInput={handleTerminalData} onInput={handleTerminalData}
onResize={handleTerminalResize} onResize={handleTerminalResize}
@@ -36,7 +36,11 @@ import {
useHostRuntimeIsConnected, useHostRuntimeIsConnected,
} from "@/runtime/host-runtime"; } from "@/runtime/host-runtime";
import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context"; import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context";
import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import {
setActiveExplorerCheckout,
type ExplorerCheckoutContext,
} from "@/stores/explorer-checkout-context";
import { MAX_CONTENT_WIDTH, useIsCompactFormFactor } from "@/constants/layout"; import { MAX_CONTENT_WIDTH, useIsCompactFormFactor } from "@/constants/layout";
import { WelcomeScreen } from "@/components/welcome-screen"; import { WelcomeScreen } from "@/components/welcome-screen";
import type { Agent } from "@/contexts/session-context"; import type { Agent } from "@/contexts/session-context";
@@ -237,10 +241,12 @@ function DraftAgentScreenContent({
const isMobile = useIsCompactFormFactor(); const isMobile = useIsCompactFormFactor();
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer); const showMobileFileExplorer = usePanelStore((state) => state.showMobileFileExplorer);
const openFileExplorer = usePanelStore((state) => state.openFileExplorer); const toggleMobileFileExplorer = usePanelStore((state) => state.toggleMobileFileExplorer);
const closeFileExplorer = usePanelStore((state) => state.closeFileExplorer); const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
const setActiveExplorerCheckout = usePanelStore((state) => state.setActiveExplorerCheckout); const openDesktopFileExplorer = usePanelStore((state) => state.openDesktopFileExplorer);
const toggleDesktopFileExplorer = usePanelStore((state) => state.toggleDesktopFileExplorer);
const closeDesktopFileExplorer = usePanelStore((state) => state.closeDesktopFileExplorer);
const activateExplorerTabForCheckout = usePanelStore( const activateExplorerTabForCheckout = usePanelStore(
(state) => state.activateExplorerTabForCheckout, (state) => state.activateExplorerTabForCheckout,
); );
@@ -646,18 +652,39 @@ function DraftAgentScreenContent({
return; return;
} }
activateExplorerTabForCheckout(draftExplorerCheckout); activateExplorerTabForCheckout(draftExplorerCheckout);
openFileExplorer(); if (isMobile) {
}, [activateExplorerTabForCheckout, draftExplorerCheckout, openFileExplorer]); showMobileFileExplorer();
return;
}
openDesktopFileExplorer();
}, [
activateExplorerTabForCheckout,
draftExplorerCheckout,
isMobile,
openDesktopFileExplorer,
showMobileFileExplorer,
]);
const handleToggleExplorer = useCallback(() => { const handleToggleExplorer = useCallback(() => {
if (!canOpenExplorer) { if (!canOpenExplorer) {
return; return;
} }
if (isExplorerOpen) { if (isExplorerOpen) {
toggleFileExplorer(); if (isMobile) {
toggleMobileFileExplorer();
return;
}
toggleDesktopFileExplorer();
return; return;
} }
openExplorerForDraftCheckout(); openExplorerForDraftCheckout();
}, [canOpenExplorer, isExplorerOpen, openExplorerForDraftCheckout, toggleFileExplorer]); }, [
canOpenExplorer,
isExplorerOpen,
isMobile,
openExplorerForDraftCheckout,
toggleDesktopFileExplorer,
toggleMobileFileExplorer,
]);
const explorerOpenGesture = useExplorerOpenGesture({ const explorerOpenGesture = useExplorerOpenGesture({
enabled: isMobile && mobileView === "agent" && canOpenExplorer, enabled: isMobile && mobileView === "agent" && canOpenExplorer,
onOpen: openExplorerForDraftCheckout, onOpen: openExplorerForDraftCheckout,
@@ -956,8 +983,19 @@ function DraftAgentScreenContent({
if (!isFocused || canOpenExplorer || !isExplorerOpen) { if (!isFocused || canOpenExplorer || !isExplorerOpen) {
return; return;
} }
closeFileExplorer(); if (isMobile) {
}, [canOpenExplorer, closeFileExplorer, isExplorerOpen, isFocused]); showMobileAgent();
return;
}
closeDesktopFileExplorer();
}, [
canOpenExplorer,
closeDesktopFileExplorer,
isExplorerOpen,
isFocused,
isMobile,
showMobileAgent,
]);
useEffect(() => { useEffect(() => {
return () => { return () => {
setActiveExplorerCheckout(null); setActiveExplorerCheckout(null);
@@ -20,7 +20,7 @@ import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
import { PairDeviceModal } from "@/desktop/components/pair-device-modal"; import { PairDeviceModal } from "@/desktop/components/pair-device-modal";
export function OpenProjectScreen({ serverId }: { serverId: string }) { export function OpenProjectScreen({ serverId }: { serverId: string }) {
const openAgentList = usePanelStore((s) => s.openAgentList); const openDesktopAgentList = usePanelStore((s) => s.openDesktopAgentList);
const openProjectPicker = useOpenProjectPicker(serverId); const openProjectPicker = useOpenProjectPicker(serverId);
const hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false); const hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false);
const hasProjects = useHasWorkspaces(serverId); const hasProjects = useHasWorkspaces(serverId);
@@ -31,9 +31,9 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
useEffect(() => { useEffect(() => {
if (!isCompactLayout) { if (!isCompactLayout) {
openAgentList(); openDesktopAgentList();
} }
}, [isCompactLayout, openAgentList]); }, [isCompactLayout, openDesktopAgentList]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
@@ -48,7 +48,11 @@ import { WorkspaceScriptsButton } from "@/screens/workspace/workspace-scripts-bu
import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context"; import { ExplorerSidebarAnimationProvider } from "@/contexts/explorer-sidebar-animation-context";
import { useToast } from "@/contexts/toast-context"; import { useToast } from "@/contexts/toast-context";
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture"; import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import {
setActiveExplorerCheckout,
type ExplorerCheckoutContext,
} from "@/stores/explorer-checkout-context";
import { useSessionStore } from "@/stores/session-store"; import { useSessionStore } from "@/stores/session-store";
import { import {
buildWorkspaceTabPersistenceKey, buildWorkspaceTabPersistenceKey,
@@ -843,13 +847,14 @@ function WorkspaceScreenContent({
const mobileView = usePanelStore((state) => state.mobileView); const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer); const showMobileFileExplorer = usePanelStore((state) => state.showMobileFileExplorer);
const openFileExplorer = usePanelStore((state) => state.openFileExplorer); const toggleMobileFileExplorer = usePanelStore((state) => state.toggleMobileFileExplorer);
const openDesktopFileExplorer = usePanelStore((state) => state.openDesktopFileExplorer);
const toggleDesktopFileExplorer = usePanelStore((state) => state.toggleDesktopFileExplorer);
const activateExplorerTabForCheckout = usePanelStore( const activateExplorerTabForCheckout = usePanelStore(
(state) => state.activateExplorerTabForCheckout, (state) => state.activateExplorerTabForCheckout,
); );
const closeToAgent = usePanelStore((state) => state.closeToAgent); const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
const setActiveExplorerCheckout = usePanelStore((state) => state.setActiveExplorerCheckout);
const isExplorerOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen; const isExplorerOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
@@ -876,16 +881,36 @@ function WorkspaceScreenContent({
return; return;
} }
activateExplorerTabForCheckout(activeExplorerCheckout); activateExplorerTabForCheckout(activeExplorerCheckout);
openFileExplorer(); if (isMobile) {
}, [activateExplorerTabForCheckout, activeExplorerCheckout, openFileExplorer]); showMobileFileExplorer();
return;
}
openDesktopFileExplorer();
}, [
activateExplorerTabForCheckout,
activeExplorerCheckout,
isMobile,
openDesktopFileExplorer,
showMobileFileExplorer,
]);
const handleToggleExplorer = useCallback(() => { const handleToggleExplorer = useCallback(() => {
if (isExplorerOpen) { if (isExplorerOpen) {
toggleFileExplorer(); if (isMobile) {
toggleMobileFileExplorer();
return;
}
toggleDesktopFileExplorer();
return; return;
} }
openExplorerForWorkspace(); openExplorerForWorkspace();
}, [isExplorerOpen, openExplorerForWorkspace, toggleFileExplorer]); }, [
isExplorerOpen,
isMobile,
openExplorerForWorkspace,
toggleDesktopFileExplorer,
toggleMobileFileExplorer,
]);
const explorerOpenGesture = useExplorerOpenGesture({ const explorerOpenGesture = useExplorerOpenGesture({
enabled: isMobile && mobileView === "agent", enabled: isMobile && mobileView === "agent",
@@ -899,14 +924,14 @@ function WorkspaceScreenContent({
const handler = BackHandler.addEventListener("hardwareBackPress", () => { const handler = BackHandler.addEventListener("hardwareBackPress", () => {
if (isExplorerOpen) { if (isExplorerOpen) {
closeToAgent(); showMobileAgent();
return true; return true;
} }
return false; return false;
}); });
return () => handler.remove(); return () => handler.remove();
}, [closeToAgent, isExplorerOpen, isRouteFocused]); }, [isExplorerOpen, isRouteFocused, showMobileAgent]);
const persistenceKey = useMemo( const persistenceKey = useMemo(
() => () =>
@@ -1190,7 +1215,7 @@ function WorkspaceScreenContent({
const handleOpenFileFromExplorer = useCallback( const handleOpenFileFromExplorer = useCallback(
function handleOpenFileFromExplorer(filePath: string) { function handleOpenFileFromExplorer(filePath: string) {
if (isMobile) { if (isMobile) {
closeToAgent(); showMobileAgent();
} }
if (!persistenceKey) { if (!persistenceKey) {
return; return;
@@ -1200,7 +1225,7 @@ function WorkspaceScreenContent({
navigateToTabId(tabId); navigateToTabId(tabId);
} }
}, },
[closeToAgent, isMobile, navigateToTabId, openWorkspaceTabFocused, persistenceKey], [isMobile, navigateToTabId, openWorkspaceTabFocused, persistenceKey, showMobileAgent],
); );
const handleOpenFileFromChat = useCallback( const handleOpenFileFromChat = useCallback(
@@ -0,0 +1,23 @@
export interface ExplorerCheckoutContext {
serverId: string;
cwd: string;
isGit: boolean;
}
let activeExplorerCheckout: ExplorerCheckoutContext | null = null;
export function getActiveExplorerCheckout(): ExplorerCheckoutContext | null {
return activeExplorerCheckout;
}
export function setActiveExplorerCheckout(checkout: ExplorerCheckoutContext | null) {
if (
activeExplorerCheckout?.serverId === checkout?.serverId &&
activeExplorerCheckout?.cwd === checkout?.cwd &&
activeExplorerCheckout?.isGit === checkout?.isGit
) {
return;
}
activeExplorerCheckout = checkout;
}
+131 -134
View File
@@ -8,8 +8,13 @@ import {
resolveExplorerTabForCheckout, resolveExplorerTabForCheckout,
type ExplorerTab, type ExplorerTab,
} from "./explorer-tab-memory"; } from "./explorer-tab-memory";
import {
getActiveExplorerCheckout,
type ExplorerCheckoutContext,
} from "./explorer-checkout-context";
import { isWeb } from "@/constants/platform"; import { isWeb } from "@/constants/platform";
export type { ExplorerTab } from "./explorer-tab-memory"; export type { ExplorerTab } from "./explorer-tab-memory";
export type { ExplorerCheckoutContext } from "./explorer-checkout-context";
/** /**
* Mobile panel state machine. * Mobile panel state machine.
@@ -37,12 +42,6 @@ interface DesktopSidebarState {
} }
export type SortOption = "name" | "modified" | "size"; export type SortOption = "name" | "modified" | "size";
export interface ExplorerCheckoutContext {
serverId: string;
cwd: string;
isGit: boolean;
}
export const DEFAULT_SIDEBAR_WIDTH = 320; export const DEFAULT_SIDEBAR_WIDTH = 320;
export const MIN_SIDEBAR_WIDTH = 200; export const MIN_SIDEBAR_WIDTH = 200;
export const MAX_SIDEBAR_WIDTH = 600; export const MAX_SIDEBAR_WIDTH = 600;
@@ -68,21 +67,25 @@ interface PanelState {
explorerTabByCheckout: Record<string, ExplorerTab>; explorerTabByCheckout: Record<string, ExplorerTab>;
expandedPathsByWorkspace: Record<string, string[]>; expandedPathsByWorkspace: Record<string, string[]>;
diffExpandedPathsByWorkspace: Record<string, string[]>; diffExpandedPathsByWorkspace: Record<string, string[]>;
activeExplorerCheckout: ExplorerCheckoutContext | null;
sidebarWidth: number; sidebarWidth: number;
explorerWidth: number; explorerWidth: number;
explorerSortOption: SortOption; explorerSortOption: SortOption;
explorerFilesSplitRatio: number; explorerFilesSplitRatio: number;
// Actions // Actions
openAgentList: () => void;
openFileExplorer: () => void;
closeFileExplorer: () => void;
closeToAgent: () => void;
toggleAgentList: () => void;
toggleFileExplorer: () => void;
toggleBothSidebars: () => void;
toggleFocusMode: () => void; toggleFocusMode: () => void;
showMobileAgent: () => void;
showMobileAgentList: () => void;
showMobileFileExplorer: () => void;
toggleMobileAgentList: () => void;
toggleMobileFileExplorer: () => void;
openDesktopAgentList: () => void;
closeDesktopAgentList: () => void;
toggleDesktopAgentList: () => void;
openDesktopFileExplorer: () => void;
closeDesktopFileExplorer: () => void;
toggleDesktopFileExplorer: () => void;
toggleDesktopSidebars: () => void;
// File explorer settings actions // File explorer settings actions
setExplorerTab: (tab: ExplorerTab) => void; setExplorerTab: (tab: ExplorerTab) => void;
@@ -90,7 +93,6 @@ interface PanelState {
setExpandedPathsForWorkspace: (workspaceKey: string, paths: string[]) => void; setExpandedPathsForWorkspace: (workspaceKey: string, paths: string[]) => void;
setDiffExpandedPathsForWorkspace: (workspaceKey: string, paths: string[]) => void; setDiffExpandedPathsForWorkspace: (workspaceKey: string, paths: string[]) => void;
activateExplorerTabForCheckout: (checkout: ExplorerCheckoutContext) => void; activateExplorerTabForCheckout: (checkout: ExplorerCheckoutContext) => void;
setActiveExplorerCheckout: (checkout: ExplorerCheckoutContext | null) => void;
setSidebarWidth: (width: number) => void; setSidebarWidth: (width: number) => void;
setExplorerWidth: (width: number) => void; setExplorerWidth: (width: number) => void;
setExplorerSortOption: (option: SortOption) => void; setExplorerSortOption: (option: SortOption) => void;
@@ -116,14 +118,17 @@ function clampExplorerFilesSplitRatio(ratio: number): number {
return clampNumber(ratio, MIN_EXPLORER_FILES_SPLIT_RATIO, MAX_EXPLORER_FILES_SPLIT_RATIO); return clampNumber(ratio, MIN_EXPLORER_FILES_SPLIT_RATIO, MAX_EXPLORER_FILES_SPLIT_RATIO);
} }
function resolveExplorerTabFromActiveCheckout(state: PanelState): ExplorerTab | null { function resolveExplorerTabFromCheckout(
if (!state.activeExplorerCheckout) { state: PanelState,
checkout: ExplorerCheckoutContext | null,
): ExplorerTab | null {
if (!checkout) {
return null; return null;
} }
return resolveExplorerTabForCheckout({ return resolveExplorerTabForCheckout({
serverId: state.activeExplorerCheckout.serverId, serverId: checkout.serverId,
cwd: state.activeExplorerCheckout.cwd, cwd: checkout.cwd,
isGit: state.activeExplorerCheckout.isGit, isGit: checkout.isGit,
explorerTabByCheckout: state.explorerTabByCheckout, explorerTabByCheckout: state.explorerTabByCheckout,
}); });
} }
@@ -148,103 +153,118 @@ export const usePanelStore = create<PanelState>()(
explorerTabByCheckout: {}, explorerTabByCheckout: {},
expandedPathsByWorkspace: {}, expandedPathsByWorkspace: {},
diffExpandedPathsByWorkspace: {}, diffExpandedPathsByWorkspace: {},
activeExplorerCheckout: null,
sidebarWidth: DEFAULT_SIDEBAR_WIDTH, sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH, explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
explorerSortOption: "name", explorerSortOption: "name",
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO, explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
openAgentList: () =>
set((state) => ({
mobileView: "agent-list" as const,
desktop: { ...state.desktop, agentListOpen: true },
})),
openFileExplorer: () =>
set((state) => {
const resolvedTab = resolveExplorerTabFromActiveCheckout(state);
return {
mobileView: "file-explorer" as MobilePanelView,
desktop: { ...state.desktop, fileExplorerOpen: true },
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
closeFileExplorer: () =>
set((state) => ({
mobileView: state.mobileView === "file-explorer" ? ("agent" as const) : state.mobileView,
desktop: {
...state.desktop,
fileExplorerOpen: false,
},
})),
closeToAgent: () =>
set((state) => ({
mobileView: "agent" as const,
// 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: {
...state.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
mobileView: (state.mobileView === "agent-list"
? "agent"
: "agent-list") as MobilePanelView,
desktop: {
...state.desktop,
agentListOpen: !state.desktop.agentListOpen,
},
})),
toggleFileExplorer: () =>
set((state) => {
// Mobile: toggle between agent and file-explorer
const willOpenMobile = state.mobileView !== "file-explorer";
const willOpenDesktop = !state.desktop.fileExplorerOpen;
const nextMobileView: MobilePanelView = willOpenMobile ? "file-explorer" : "agent";
const nextDesktop = {
...state.desktop,
fileExplorerOpen: willOpenDesktop,
};
const nextState: Pick<PanelState, "mobileView" | "desktop"> &
Partial<Pick<PanelState, "explorerTab">> = {
mobileView: nextMobileView,
desktop: nextDesktop,
};
if (willOpenMobile || willOpenDesktop) {
const resolvedTab = resolveExplorerTabFromActiveCheckout(state);
if (resolvedTab) {
nextState.explorerTab = resolvedTab;
}
}
return nextState;
}),
toggleFocusMode: () => toggleFocusMode: () =>
set((state) => ({ set((state) => ({
desktop: { ...state.desktop, focusModeEnabled: !state.desktop.focusModeEnabled }, desktop: { ...state.desktop, focusModeEnabled: !state.desktop.focusModeEnabled },
})), })),
toggleBothSidebars: () => showMobileAgent: () =>
set((state) => {
if (state.mobileView === "agent") {
return state;
}
return { mobileView: "agent" as const };
}),
showMobileAgentList: () =>
set((state) => {
if (state.mobileView === "agent-list") {
return state;
}
return { mobileView: "agent-list" as const };
}),
showMobileFileExplorer: () =>
set((state) => {
const resolvedTab = resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout());
return {
mobileView: "file-explorer" as const,
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
toggleMobileAgentList: () =>
set((state) => ({
mobileView: state.mobileView === "agent-list" ? "agent" : "agent-list",
})),
toggleMobileFileExplorer: () =>
set((state) => {
if (state.mobileView === "file-explorer") {
return { mobileView: "agent" as const };
}
const resolvedTab = resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout());
return {
mobileView: "file-explorer" as const,
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
openDesktopAgentList: () =>
set((state) => {
if (state.desktop.agentListOpen) {
return state;
}
return { desktop: { ...state.desktop, agentListOpen: true } };
}),
closeDesktopAgentList: () =>
set((state) => {
if (!state.desktop.agentListOpen) {
return state;
}
return { desktop: { ...state.desktop, agentListOpen: false } };
}),
toggleDesktopAgentList: () =>
set((state) => ({
desktop: { ...state.desktop, agentListOpen: !state.desktop.agentListOpen },
})),
openDesktopFileExplorer: () =>
set((state) => {
const resolvedTab = resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout());
return {
desktop: { ...state.desktop, fileExplorerOpen: true },
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
closeDesktopFileExplorer: () =>
set((state) => {
if (!state.desktop.fileExplorerOpen) {
return state;
}
return { desktop: { ...state.desktop, fileExplorerOpen: false } };
}),
toggleDesktopFileExplorer: () =>
set((state) => {
const willOpen = !state.desktop.fileExplorerOpen;
const resolvedTab = willOpen
? resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout())
: null;
return {
desktop: { ...state.desktop, fileExplorerOpen: willOpen },
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
toggleDesktopSidebars: () =>
set((state) => { set((state) => {
// If either sidebar is open, close both. Otherwise, open both.
const anyOpen = state.desktop.agentListOpen || state.desktop.fileExplorerOpen; const anyOpen = state.desktop.agentListOpen || state.desktop.fileExplorerOpen;
if (anyOpen) { if (anyOpen) {
return { return {
mobileView: "agent" as MobilePanelView,
desktop: { ...state.desktop, agentListOpen: false, fileExplorerOpen: false }, desktop: { ...state.desktop, agentListOpen: false, fileExplorerOpen: false },
}; };
} }
const resolvedTab = resolveExplorerTabFromActiveCheckout(state); const resolvedTab = resolveExplorerTabFromCheckout(state, getActiveExplorerCheckout());
return { return {
mobileView: "agent" as MobilePanelView,
desktop: { ...state.desktop, agentListOpen: true, fileExplorerOpen: true }, desktop: { ...state.desktop, agentListOpen: true, fileExplorerOpen: true },
...(resolvedTab ? { explorerTab: resolvedTab } : {}), ...(resolvedTab ? { explorerTab: resolvedTab } : {}),
}; };
@@ -280,7 +300,6 @@ export const usePanelStore = create<PanelState>()(
})), })),
activateExplorerTabForCheckout: (checkout) => activateExplorerTabForCheckout: (checkout) =>
set((state) => ({ set((state) => ({
activeExplorerCheckout: checkout,
explorerTab: resolveExplorerTabForCheckout({ explorerTab: resolveExplorerTabForCheckout({
serverId: checkout.serverId, serverId: checkout.serverId,
cwd: checkout.cwd, cwd: checkout.cwd,
@@ -288,18 +307,6 @@ export const usePanelStore = create<PanelState>()(
explorerTabByCheckout: state.explorerTabByCheckout, explorerTabByCheckout: state.explorerTabByCheckout,
}), }),
})), })),
setActiveExplorerCheckout: (checkout) =>
set((state) => {
const current = state.activeExplorerCheckout;
if (
current?.serverId === checkout?.serverId &&
current?.cwd === checkout?.cwd &&
current?.isGit === checkout?.isGit
) {
return state;
}
return { activeExplorerCheckout: checkout };
}),
setSidebarWidth: (width) => set({ sidebarWidth: clampSidebarWidth(width) }), setSidebarWidth: (width) => set({ sidebarWidth: clampSidebarWidth(width) }),
setExplorerWidth: (width) => set({ explorerWidth: clampWidth(width) }), setExplorerWidth: (width) => set({ explorerWidth: clampWidth(width) }),
setExplorerSortOption: (option) => set({ explorerSortOption: option }), setExplorerSortOption: (option) => set({ explorerSortOption: option }),
@@ -400,8 +407,6 @@ export const usePanelStore = create<PanelState>()(
state.diffExpandedPathsByWorkspace = {}; state.diffExpandedPathsByWorkspace = {};
} }
state.activeExplorerCheckout = null;
return state as PanelState; return state as PanelState;
}, },
partialize: (state) => ({ partialize: (state) => ({
@@ -435,12 +440,12 @@ export function usePanelState(isMobile: boolean) {
return { return {
isAgentListOpen: store.mobileView === "agent-list", isAgentListOpen: store.mobileView === "agent-list",
isFileExplorerOpen: store.mobileView === "file-explorer", isFileExplorerOpen: store.mobileView === "file-explorer",
openAgentList: store.openAgentList, openAgentList: store.showMobileAgentList,
openFileExplorer: store.openFileExplorer, openFileExplorer: store.showMobileFileExplorer,
closeAgentList: store.closeToAgent, closeAgentList: store.showMobileAgent,
closeFileExplorer: store.closeToAgent, closeFileExplorer: store.showMobileAgent,
toggleAgentList: store.toggleAgentList, toggleAgentList: store.toggleMobileAgentList,
toggleFileExplorer: store.toggleFileExplorer, toggleFileExplorer: store.toggleMobileFileExplorer,
// Explorer settings // Explorer settings
explorerTab: store.explorerTab, explorerTab: store.explorerTab,
explorerTabByCheckout: store.explorerTabByCheckout, explorerTabByCheckout: store.explorerTabByCheckout,
@@ -450,7 +455,6 @@ export function usePanelState(isMobile: boolean) {
setExplorerTab: store.setExplorerTab, setExplorerTab: store.setExplorerTab,
setExplorerTabForCheckout: store.setExplorerTabForCheckout, setExplorerTabForCheckout: store.setExplorerTabForCheckout,
activateExplorerTabForCheckout: store.activateExplorerTabForCheckout, activateExplorerTabForCheckout: store.activateExplorerTabForCheckout,
setActiveExplorerCheckout: store.setActiveExplorerCheckout,
setExplorerWidth: store.setExplorerWidth, setExplorerWidth: store.setExplorerWidth,
setExplorerSortOption: store.setExplorerSortOption, setExplorerSortOption: store.setExplorerSortOption,
setExplorerFilesSplitRatio: store.setExplorerFilesSplitRatio, setExplorerFilesSplitRatio: store.setExplorerFilesSplitRatio,
@@ -461,18 +465,12 @@ export function usePanelState(isMobile: boolean) {
return { return {
isAgentListOpen: store.desktop.agentListOpen, isAgentListOpen: store.desktop.agentListOpen,
isFileExplorerOpen: store.desktop.fileExplorerOpen, isFileExplorerOpen: store.desktop.fileExplorerOpen,
openAgentList: store.openAgentList, openAgentList: store.openDesktopAgentList,
openFileExplorer: store.openFileExplorer, openFileExplorer: store.openDesktopFileExplorer,
closeAgentList: () => closeAgentList: store.closeDesktopAgentList,
usePanelStore.setState((state) => ({ closeFileExplorer: store.closeDesktopFileExplorer,
desktop: { ...state.desktop, agentListOpen: false }, toggleAgentList: store.toggleDesktopAgentList,
})), toggleFileExplorer: store.toggleDesktopFileExplorer,
closeFileExplorer: () =>
usePanelStore.setState((state) => ({
desktop: { ...state.desktop, fileExplorerOpen: false },
})),
toggleAgentList: store.toggleAgentList,
toggleFileExplorer: store.toggleFileExplorer,
// Explorer settings // Explorer settings
explorerTab: store.explorerTab, explorerTab: store.explorerTab,
explorerTabByCheckout: store.explorerTabByCheckout, explorerTabByCheckout: store.explorerTabByCheckout,
@@ -482,7 +480,6 @@ export function usePanelState(isMobile: boolean) {
setExplorerTab: store.setExplorerTab, setExplorerTab: store.setExplorerTab,
setExplorerTabForCheckout: store.setExplorerTabForCheckout, setExplorerTabForCheckout: store.setExplorerTabForCheckout,
activateExplorerTabForCheckout: store.activateExplorerTabForCheckout, activateExplorerTabForCheckout: store.activateExplorerTabForCheckout,
setActiveExplorerCheckout: store.setActiveExplorerCheckout,
setExplorerWidth: store.setExplorerWidth, setExplorerWidth: store.setExplorerWidth,
setExplorerSortOption: store.setExplorerSortOption, setExplorerSortOption: store.setExplorerSortOption,
setExplorerFilesSplitRatio: store.setExplorerFilesSplitRatio, setExplorerFilesSplitRatio: store.setExplorerFilesSplitRatio,