refactor: extract explorer open gesture and standardize icon sizes
This commit is contained in:
@@ -17,11 +17,8 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import ReanimatedAnimated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
runOnJS,
|
||||
interpolate,
|
||||
Extrapolation,
|
||||
} from "react-native-reanimated";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { GestureDetector } from "react-native-gesture-handler";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import {
|
||||
MoreVertical,
|
||||
@@ -41,7 +38,6 @@ import { FileDropZone } from "@/components/file-drop-zone";
|
||||
import type { ImageAttachment } from "@/components/message-input";
|
||||
import {
|
||||
ExplorerSidebarAnimationProvider,
|
||||
useExplorerSidebarAnimation,
|
||||
} from "@/contexts/explorer-sidebar-animation-context";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
@@ -76,6 +72,7 @@ import {
|
||||
import { mergePendingCreateImages } from "@/utils/pending-create-images";
|
||||
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
|
||||
import type { FetchAgentsEntry } from "@server/client/daemon-client";
|
||||
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -90,6 +87,14 @@ const DROPDOWN_WIDTH = 220;
|
||||
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
||||
const RECONNECT_NOTICE_DELAY_MS = 10_000;
|
||||
const CONNECTED_NOTICE_DURATION_MS = 2_500;
|
||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||
|
||||
function logAgentExplorer(event: string, details: Record<string, unknown>): void {
|
||||
if (!IS_DEV) {
|
||||
return;
|
||||
}
|
||||
console.log(`[AgentExplorer] ${event}`, details);
|
||||
}
|
||||
|
||||
export function AgentReadyScreen({
|
||||
serverId,
|
||||
@@ -324,31 +329,27 @@ function AgentScreenContent({
|
||||
}, [resolveCachedCheckoutIsGit, resolvedAgentId, checkout?.isGit, serverId]);
|
||||
const openExplorerForActiveCheckout = useCallback(() => {
|
||||
const checkoutContext = resolveCurrentExplorerCheckout();
|
||||
logAgentExplorer("openExplorerForActiveCheckout", {
|
||||
hasCheckoutContext: Boolean(checkoutContext),
|
||||
checkoutContext,
|
||||
});
|
||||
if (checkoutContext) {
|
||||
activateExplorerTabForCheckout(checkoutContext);
|
||||
}
|
||||
openFileExplorer();
|
||||
}, [activateExplorerTabForCheckout, openFileExplorer, resolveCurrentExplorerCheckout]);
|
||||
const handleToggleExplorer = useCallback(() => {
|
||||
logAgentExplorer("handleToggleExplorer", {
|
||||
isExplorerOpen,
|
||||
mobileView,
|
||||
isMobile,
|
||||
});
|
||||
if (isExplorerOpen) {
|
||||
toggleFileExplorer();
|
||||
return;
|
||||
}
|
||||
openExplorerForActiveCheckout();
|
||||
}, [isExplorerOpen, openExplorerForActiveCheckout, toggleFileExplorer]);
|
||||
|
||||
const {
|
||||
translateX: explorerTranslateX,
|
||||
backdropOpacity: explorerBackdropOpacity,
|
||||
windowWidth: explorerWindowWidth,
|
||||
animateToOpen: animateExplorerToOpen,
|
||||
animateToClose: animateExplorerToClose,
|
||||
isGesturing: isExplorerGesturing,
|
||||
} = useExplorerSidebarAnimation();
|
||||
const handleOpenExplorerFromGesture = useCallback(() => {
|
||||
openExplorerForActiveCheckout();
|
||||
animateExplorerToOpen();
|
||||
}, [animateExplorerToOpen, openExplorerForActiveCheckout]);
|
||||
}, [isExplorerOpen, isMobile, mobileView, openExplorerForActiveCheckout, toggleFileExplorer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "web") {
|
||||
@@ -360,53 +361,10 @@ function AgentScreenContent({
|
||||
}, [serverId, agentId]);
|
||||
|
||||
// Swipe-left gesture to open explorer sidebar on mobile
|
||||
const explorerOpenGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
.enabled(isMobile && !isExplorerOpen)
|
||||
// Only activate after 15px horizontal movement to the left (negative)
|
||||
.activeOffsetX(-15)
|
||||
// Fail if 10px vertical movement happens first (allow vertical scroll)
|
||||
.failOffsetY([-10, 10])
|
||||
.onStart(() => {
|
||||
isExplorerGesturing.value = true;
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
// Right sidebar: start from closed position (+windowWidth) and move towards 0
|
||||
// Swiping left means negative translationX
|
||||
const newTranslateX = Math.max(0, explorerWindowWidth + event.translationX);
|
||||
explorerTranslateX.value = newTranslateX;
|
||||
explorerBackdropOpacity.value = interpolate(
|
||||
newTranslateX,
|
||||
[explorerWindowWidth, 0],
|
||||
[0, 1],
|
||||
Extrapolation.CLAMP
|
||||
);
|
||||
})
|
||||
.onEnd((event) => {
|
||||
isExplorerGesturing.value = false;
|
||||
// Open if dragged more than 1/3 of window or fast swipe left
|
||||
const shouldOpen = event.translationX < -explorerWindowWidth / 3 || event.velocityX < -500;
|
||||
if (shouldOpen) {
|
||||
runOnJS(handleOpenExplorerFromGesture)();
|
||||
} else {
|
||||
animateExplorerToClose();
|
||||
}
|
||||
})
|
||||
.onFinalize(() => {
|
||||
isExplorerGesturing.value = false;
|
||||
}),
|
||||
[
|
||||
isMobile,
|
||||
isExplorerOpen,
|
||||
explorerWindowWidth,
|
||||
explorerTranslateX,
|
||||
explorerBackdropOpacity,
|
||||
animateExplorerToClose,
|
||||
handleOpenExplorerFromGesture,
|
||||
isExplorerGesturing,
|
||||
]
|
||||
);
|
||||
const explorerOpenGesture = useExplorerOpenGesture({
|
||||
enabled: isMobile && mobileView === "agent",
|
||||
onOpen: openExplorerForActiveCheckout,
|
||||
});
|
||||
|
||||
// Handle hardware back button - close explorer sidebar first, then navigate back
|
||||
useEffect(() => {
|
||||
@@ -947,7 +905,7 @@ function AgentScreenContent({
|
||||
await Clipboard.setStringAsync(value);
|
||||
toast.show(`Copied ${label}`, {
|
||||
variant: "success",
|
||||
icon: <CheckCircle2 size={16} color={theme.colors.primary} />,
|
||||
icon: <CheckCircle2 size={theme.iconSize.md} color={theme.colors.primary} />,
|
||||
});
|
||||
} catch {
|
||||
toast.error("Copy failed");
|
||||
@@ -1015,7 +973,7 @@ function AgentScreenContent({
|
||||
{isMobile ? (
|
||||
checkout?.isGit ? (
|
||||
<GitBranch
|
||||
size={20}
|
||||
size={theme.iconSize.lg}
|
||||
color={
|
||||
isExplorerOpen
|
||||
? theme.colors.foreground
|
||||
@@ -1024,7 +982,7 @@ function AgentScreenContent({
|
||||
/>
|
||||
) : (
|
||||
<Folder
|
||||
size={20}
|
||||
size={theme.iconSize.lg}
|
||||
color={
|
||||
isExplorerOpen
|
||||
? theme.colors.foreground
|
||||
@@ -1034,7 +992,7 @@ function AgentScreenContent({
|
||||
)
|
||||
) : (
|
||||
<PanelRight
|
||||
size={16}
|
||||
size={theme.iconSize.md}
|
||||
color={
|
||||
isExplorerOpen
|
||||
? theme.colors.foreground
|
||||
@@ -1051,7 +1009,10 @@ function AgentScreenContent({
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger testID="agent-overflow-menu" style={styles.menuButton}>
|
||||
<MoreVertical size={isMobile ? 20 : 16} color={theme.colors.foregroundMuted} />
|
||||
<MoreVertical
|
||||
size={isMobile ? theme.iconSize.lg : theme.iconSize.md}
|
||||
color={theme.colors.foregroundMuted}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" width={DROPDOWN_WIDTH} testID="agent-overflow-content">
|
||||
<View style={styles.menuMetaContainer}>
|
||||
@@ -1170,7 +1131,7 @@ function AgentScreenContent({
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
leading={<RotateCcw size={16} color={theme.colors.foreground} />}
|
||||
leading={<RotateCcw size={theme.iconSize.md} color={theme.colors.foreground} />}
|
||||
disabled={isInitializing || shouldBlockForHistorySync}
|
||||
trailing={
|
||||
isInitializing ? (
|
||||
@@ -1202,7 +1163,7 @@ function AgentScreenContent({
|
||||
>
|
||||
{showConnectedNotice ? (
|
||||
<CheckCircle2
|
||||
size={14}
|
||||
size={theme.iconSize.sm}
|
||||
color={theme.colors.palette.green[600]}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -3,11 +3,12 @@ import { createNameId } from 'mnemonic-id'
|
||||
import type { ImageAttachment } from '@/components/message-input'
|
||||
import { View, Text, Pressable, ScrollView, Keyboard, Platform } from 'react-native'
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router'
|
||||
import { useIsFocused } from '@react-navigation/native'
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
|
||||
import { GestureDetector } from 'react-native-gesture-handler'
|
||||
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
|
||||
import { Folder, GitBranch, PanelRight } from 'lucide-react-native'
|
||||
import { SidebarMenuToggle } from '@/components/headers/menu-header'
|
||||
import { HeaderToggleButton } from '@/components/headers/header-toggle-button'
|
||||
@@ -31,6 +32,7 @@ import { buildBranchComboOptions, normalizeBranchOptionName } from '@/utils/bran
|
||||
import { shortenPath } from '@/utils/shorten-path'
|
||||
import { collectAgentWorkingDirectorySuggestions } from '@/utils/agent-working-directory-suggestions'
|
||||
import { buildWorkingDirectorySuggestions } from '@/utils/working-directory-suggestions'
|
||||
import { useExplorerOpenGesture } from '@/hooks/use-explorer-open-gesture'
|
||||
import { useSessionStore } from '@/stores/session-store'
|
||||
import { useCreateFlowStore } from '@/stores/create-flow-store'
|
||||
import { ExplorerSidebarAnimationProvider } from '@/contexts/explorer-sidebar-animation-context'
|
||||
@@ -131,6 +133,7 @@ function DraftAgentScreenContent({
|
||||
onCreateFlowActiveChange,
|
||||
forcedServerId,
|
||||
}: DraftAgentScreenProps = {}) {
|
||||
const isFocused = useIsFocused()
|
||||
const { theme } = useUnistyles()
|
||||
const router = useRouter()
|
||||
const insets = useSafeAreaInsets()
|
||||
@@ -713,26 +716,10 @@ function DraftAgentScreenContent({
|
||||
}
|
||||
openExplorerForDraftCheckout()
|
||||
}, [canOpenExplorer, isExplorerOpen, openExplorerForDraftCheckout, toggleFileExplorer])
|
||||
const handleOpenExplorerFromGesture = useCallback(() => {
|
||||
if (!canOpenExplorer) {
|
||||
return
|
||||
}
|
||||
openExplorerForDraftCheckout()
|
||||
}, [canOpenExplorer, openExplorerForDraftCheckout])
|
||||
const explorerOpenGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
.enabled(isMobile && !isExplorerOpen && canOpenExplorer)
|
||||
.activeOffsetX(-15)
|
||||
.failOffsetY([-10, 10])
|
||||
.onEnd((event) => {
|
||||
const shouldOpen = event.translationX < -80 || event.velocityX < -500
|
||||
if (shouldOpen) {
|
||||
runOnJS(handleOpenExplorerFromGesture)()
|
||||
}
|
||||
}),
|
||||
[canOpenExplorer, handleOpenExplorerFromGesture, isExplorerOpen, isMobile]
|
||||
)
|
||||
const explorerOpenGesture = useExplorerOpenGesture({
|
||||
enabled: isMobile && mobileView === 'agent' && canOpenExplorer,
|
||||
onOpen: openExplorerForDraftCheckout,
|
||||
})
|
||||
const hasWorkingDirectorySearch = debouncedWorkingDirSearchQuery.length > 0
|
||||
const workingDirSearchError =
|
||||
directorySuggestionsQuery.error instanceof Error
|
||||
@@ -926,20 +913,23 @@ function DraftAgentScreenContent({
|
||||
workingDir,
|
||||
])
|
||||
useEffect(() => {
|
||||
if (!isFocused) {
|
||||
return
|
||||
}
|
||||
setActiveExplorerCheckout(draftExplorerCheckout)
|
||||
}, [draftExplorerCheckout, setActiveExplorerCheckout])
|
||||
}, [draftExplorerCheckout, isFocused, setActiveExplorerCheckout])
|
||||
useEffect(() => {
|
||||
if (!draftExplorerCheckout) {
|
||||
if (!isFocused || !draftExplorerCheckout) {
|
||||
return
|
||||
}
|
||||
activateExplorerTabForCheckout(draftExplorerCheckout)
|
||||
}, [activateExplorerTabForCheckout, draftExplorerCheckout])
|
||||
}, [activateExplorerTabForCheckout, draftExplorerCheckout, isFocused])
|
||||
useEffect(() => {
|
||||
if (canOpenExplorer || !isExplorerOpen) {
|
||||
if (!isFocused || canOpenExplorer || !isExplorerOpen) {
|
||||
return
|
||||
}
|
||||
closeFileExplorer()
|
||||
}, [canOpenExplorer, closeFileExplorer, isExplorerOpen])
|
||||
}, [canOpenExplorer, closeFileExplorer, isExplorerOpen, isFocused])
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setActiveExplorerCheckout(null)
|
||||
@@ -1146,7 +1136,7 @@ function DraftAgentScreenContent({
|
||||
accessibilityState={{ expanded: isExplorerOpen }}
|
||||
>
|
||||
<PanelRight
|
||||
size={16}
|
||||
size={theme.iconSize.md}
|
||||
color={isExplorerOpen ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||
/>
|
||||
</HeaderToggleButton>
|
||||
@@ -1179,7 +1169,7 @@ function DraftAgentScreenContent({
|
||||
value={displayWorkingDir}
|
||||
placeholder="/path/to/project"
|
||||
onPress={() => setIsWorkingDirOpen(true)}
|
||||
icon={<Folder size={16} color={theme.colors.foregroundMuted} />}
|
||||
icon={<Folder size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||
showLabel={false}
|
||||
valueEllipsizeMode="middle"
|
||||
testID="working-directory-select"
|
||||
@@ -1226,7 +1216,7 @@ function DraftAgentScreenContent({
|
||||
value={worktreeTriggerValue}
|
||||
placeholder="Select worktree"
|
||||
onPress={() => setIsWorktreePickerOpen(true)}
|
||||
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
|
||||
icon={<GitBranch size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||
showLabel={false}
|
||||
valueEllipsizeMode="middle"
|
||||
/>
|
||||
@@ -1239,7 +1229,7 @@ function DraftAgentScreenContent({
|
||||
placeholder="From branch"
|
||||
onPress={() => setIsBranchOpen(true)}
|
||||
disabled={repoInfoStatus === 'loading'}
|
||||
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
|
||||
icon={<GitBranch size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||
showLabel={false}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user