Enable explorer sidebar on draft screen after cwd selection (#54)
* fix: simplify tmux session creation in paseo.json * feat(app): enable draft explorer sidebar from working directory * chore: align paseo config with main
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
import { test, expect } from "./fixtures";
|
||||||
|
import { ensureHostSelected, gotoHome, setWorkingDirectory } from "./helpers/app";
|
||||||
|
import { createTempGitRepo } from "./helpers/workspace";
|
||||||
|
|
||||||
|
test("draft enables explorer after selecting a working directory", async ({ page }) => {
|
||||||
|
const repo = await createTempGitRepo("paseo-e2e-draft-explorer-");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await gotoHome(page);
|
||||||
|
await ensureHostSelected(page);
|
||||||
|
|
||||||
|
const newAgentButton = page.getByTestId("sidebar-new-agent").first();
|
||||||
|
await expect(newAgentButton).toBeVisible({ timeout: 30000 });
|
||||||
|
await newAgentButton.click();
|
||||||
|
await expect(page).toHaveURL(/\/agent\/?$/, { timeout: 30000 });
|
||||||
|
|
||||||
|
await setWorkingDirectory(page, repo.path);
|
||||||
|
|
||||||
|
const toggle = page
|
||||||
|
.getByRole("button", {
|
||||||
|
name: /open explorer|close explorer|toggle explorer/i,
|
||||||
|
})
|
||||||
|
.first();
|
||||||
|
await expect(toggle).toBeVisible({ timeout: 30000 });
|
||||||
|
|
||||||
|
await toggle.click();
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="explorer-header"]:visible').first()
|
||||||
|
).toBeVisible({ timeout: 30000 });
|
||||||
|
|
||||||
|
const terminalsTab = page
|
||||||
|
.locator('[data-testid="explorer-tab-terminals"]:visible')
|
||||||
|
.first();
|
||||||
|
await expect(terminalsTab).toBeVisible({ timeout: 30000 });
|
||||||
|
await terminalsTab.click();
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-testid="terminal-surface"]:visible').first()
|
||||||
|
).toBeVisible({ timeout: 30000 });
|
||||||
|
} finally {
|
||||||
|
await repo.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -13,12 +13,15 @@ import { useLocalSearchParams, useRouter } from "expo-router";
|
|||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||||
import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import { Folder, GitBranch } from "lucide-react-native";
|
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
||||||
|
import { Folder, GitBranch, PanelRight } from "lucide-react-native";
|
||||||
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
||||||
|
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
|
||||||
import { AgentInputArea } from "@/components/agent-input-area";
|
import { AgentInputArea } from "@/components/agent-input-area";
|
||||||
import { AgentStreamView } from "@/components/agent-stream-view";
|
import { AgentStreamView } from "@/components/agent-stream-view";
|
||||||
import { AgentConfigRow, FormSelectTrigger } from "@/components/agent-form/agent-form-dropdowns";
|
import { AgentConfigRow, FormSelectTrigger } from "@/components/agent-form/agent-form-dropdowns";
|
||||||
|
import { ExplorerSidebar } from "@/components/explorer-sidebar";
|
||||||
import { Combobox } from "@/components/ui/combobox";
|
import { Combobox } from "@/components/ui/combobox";
|
||||||
import { FileDropZone } from "@/components/file-drop-zone";
|
import { FileDropZone } from "@/components/file-drop-zone";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -37,6 +40,10 @@ import { collectAgentWorkingDirectorySuggestions } from "@/utils/agent-working-d
|
|||||||
import { buildWorkingDirectorySuggestions } from "@/utils/working-directory-suggestions";
|
import { buildWorkingDirectorySuggestions } from "@/utils/working-directory-suggestions";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||||
|
import {
|
||||||
|
ExplorerSidebarAnimationProvider,
|
||||||
|
} from "@/contexts/explorer-sidebar-animation-context";
|
||||||
|
import { usePanelStore, type ExplorerCheckoutContext } from "@/stores/panel-store";
|
||||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
import { MAX_CONTENT_WIDTH } 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";
|
||||||
@@ -125,6 +132,22 @@ export function DraftAgentScreen({
|
|||||||
isVisible = true,
|
isVisible = true,
|
||||||
onCreateFlowActiveChange,
|
onCreateFlowActiveChange,
|
||||||
forcedServerId,
|
forcedServerId,
|
||||||
|
}: DraftAgentScreenProps = {}) {
|
||||||
|
return (
|
||||||
|
<ExplorerSidebarAnimationProvider>
|
||||||
|
<DraftAgentScreenContent
|
||||||
|
isVisible={isVisible}
|
||||||
|
onCreateFlowActiveChange={onCreateFlowActiveChange}
|
||||||
|
forcedServerId={forcedServerId}
|
||||||
|
/>
|
||||||
|
</ExplorerSidebarAnimationProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DraftAgentScreenContent({
|
||||||
|
isVisible = true,
|
||||||
|
onCreateFlowActiveChange,
|
||||||
|
forcedServerId,
|
||||||
}: DraftAgentScreenProps = {}) {
|
}: DraftAgentScreenProps = {}) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -229,6 +252,16 @@ export function DraftAgentScreen({
|
|||||||
: undefined;
|
: undefined;
|
||||||
const isMobile =
|
const isMobile =
|
||||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
|
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||||
|
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
|
||||||
|
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
|
||||||
|
const closeFileExplorer = usePanelStore((state) => state.closeFileExplorer);
|
||||||
|
const setActiveExplorerCheckout = usePanelStore((state) => state.setActiveExplorerCheckout);
|
||||||
|
const activateExplorerTabForCheckout = usePanelStore(
|
||||||
|
(state) => state.activateExplorerTabForCheckout
|
||||||
|
);
|
||||||
|
const isExplorerOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
|
||||||
|
|
||||||
const [worktreeMode, setWorktreeMode] = useState<"none" | "create" | "attach">("none");
|
const [worktreeMode, setWorktreeMode] = useState<"none" | "create" | "attach">("none");
|
||||||
const [baseBranch, setBaseBranch] = useState("");
|
const [baseBranch, setBaseBranch] = useState("");
|
||||||
@@ -690,6 +723,72 @@ export function DraftAgentScreen({
|
|||||||
|
|
||||||
const selectedWorktreeLabel =
|
const selectedWorktreeLabel =
|
||||||
worktreeOptions.find((option) => option.path === selectedWorktreePath)?.label ?? "";
|
worktreeOptions.find((option) => option.path === selectedWorktreePath)?.label ?? "";
|
||||||
|
const explorerCwd = useMemo(
|
||||||
|
() =>
|
||||||
|
(
|
||||||
|
isAttachWorktree && selectedWorktreePath ? selectedWorktreePath : workingDir
|
||||||
|
).trim(),
|
||||||
|
[isAttachWorktree, selectedWorktreePath, workingDir]
|
||||||
|
);
|
||||||
|
const draftExplorerCheckout = useMemo<ExplorerCheckoutContext | null>(() => {
|
||||||
|
if (!selectedServerId || !explorerCwd) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
serverId: selectedServerId,
|
||||||
|
cwd: explorerCwd,
|
||||||
|
isGit: isAttachWorktree && selectedWorktreePath ? true : checkout?.isGit === true,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
selectedServerId,
|
||||||
|
explorerCwd,
|
||||||
|
isAttachWorktree,
|
||||||
|
selectedWorktreePath,
|
||||||
|
checkout?.isGit,
|
||||||
|
]);
|
||||||
|
const canOpenExplorer = draftExplorerCheckout !== null;
|
||||||
|
const openExplorerForDraftCheckout = useCallback(() => {
|
||||||
|
if (!draftExplorerCheckout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activateExplorerTabForCheckout(draftExplorerCheckout);
|
||||||
|
openFileExplorer();
|
||||||
|
}, [activateExplorerTabForCheckout, draftExplorerCheckout, openFileExplorer]);
|
||||||
|
const handleToggleExplorer = useCallback(() => {
|
||||||
|
if (!canOpenExplorer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isExplorerOpen) {
|
||||||
|
toggleFileExplorer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 hasWorkingDirectorySearch = debouncedWorkingDirSearchQuery.length > 0;
|
const hasWorkingDirectorySearch = debouncedWorkingDirSearchQuery.length > 0;
|
||||||
const workingDirSearchError =
|
const workingDirSearchError =
|
||||||
directorySuggestionsQuery.error instanceof Error
|
directorySuggestionsQuery.error instanceof Error
|
||||||
@@ -893,6 +992,26 @@ export function DraftAgentScreen({
|
|||||||
selectedWorktreePath,
|
selectedWorktreePath,
|
||||||
workingDir,
|
workingDir,
|
||||||
]);
|
]);
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveExplorerCheckout(draftExplorerCheckout);
|
||||||
|
}, [draftExplorerCheckout, setActiveExplorerCheckout]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!draftExplorerCheckout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activateExplorerTabForCheckout(draftExplorerCheckout);
|
||||||
|
}, [activateExplorerTabForCheckout, draftExplorerCheckout]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (canOpenExplorer || !isExplorerOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeFileExplorer();
|
||||||
|
}, [canOpenExplorer, closeFileExplorer, isExplorerOpen]);
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setActiveExplorerCheckout(null);
|
||||||
|
};
|
||||||
|
}, [setActiveExplorerCheckout]);
|
||||||
|
|
||||||
const handleCreateFromInput = useCallback(
|
const handleCreateFromInput = useCallback(
|
||||||
async ({
|
async ({
|
||||||
@@ -1082,9 +1201,11 @@ export function DraftAgentScreen({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const explorerServerId = draftExplorerCheckout?.serverId ?? null;
|
||||||
<FileDropZone onFilesDropped={handleFilesDropped}>
|
const explorerIsGit = draftExplorerCheckout?.isGit ?? false;
|
||||||
|
const mainContent = (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
<View style={styles.outerContainer}>
|
||||||
<View style={styles.agentPanel}>
|
<View style={styles.agentPanel}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -1092,7 +1213,31 @@ export function DraftAgentScreen({
|
|||||||
isMobile ? { paddingTop: insets.top + theme.spacing[2] } : null,
|
isMobile ? { paddingTop: insets.top + theme.spacing[2] } : null,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
<View style={styles.menuToggleRow}>
|
||||||
<SidebarMenuToggle />
|
<SidebarMenuToggle />
|
||||||
|
{!isMobile && canOpenExplorer ? (
|
||||||
|
<HeaderToggleButton
|
||||||
|
onPress={handleToggleExplorer}
|
||||||
|
tooltipLabel="Toggle explorer"
|
||||||
|
tooltipKeys={["mod", "E"]}
|
||||||
|
tooltipSide="left"
|
||||||
|
style={styles.menuButton}
|
||||||
|
accessible
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={isExplorerOpen ? "Close explorer" : "Open explorer"}
|
||||||
|
accessibilityState={{ expanded: isExplorerOpen }}
|
||||||
|
>
|
||||||
|
<PanelRight
|
||||||
|
size={16}
|
||||||
|
color={
|
||||||
|
isExplorerOpen
|
||||||
|
? theme.colors.foreground
|
||||||
|
: theme.colors.foregroundMuted
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</HeaderToggleButton>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Animated.View style={[styles.contentContainer, animatedKeyboardStyle]}>
|
<Animated.View style={[styles.contentContainer, animatedKeyboardStyle]}>
|
||||||
@@ -1107,7 +1252,10 @@ export function DraftAgentScreen({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}>
|
<ScrollView
|
||||||
|
style={styles.scrollView}
|
||||||
|
contentContainerStyle={styles.configScrollContent}
|
||||||
|
>
|
||||||
<View style={styles.configSection}>
|
<View style={styles.configSection}>
|
||||||
<View style={isMobile ? styles.stackedSelectorGroup : styles.topSelectorRow}>
|
<View style={isMobile ? styles.stackedSelectorGroup : styles.topSelectorRow}>
|
||||||
<FormSelectTrigger
|
<FormSelectTrigger
|
||||||
@@ -1283,7 +1431,39 @@ export function DraftAgentScreen({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{!isMobile && isExplorerOpen && explorerServerId && draftExplorerCheckout ? (
|
||||||
|
<ExplorerSidebar
|
||||||
|
serverId={explorerServerId}
|
||||||
|
agentId={DRAFT_AGENT_ID}
|
||||||
|
cwd={draftExplorerCheckout.cwd}
|
||||||
|
isGit={explorerIsGit}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileDropZone onFilesDropped={handleFilesDropped}>
|
||||||
|
<>
|
||||||
|
{isMobile ? (
|
||||||
|
<GestureDetector gesture={explorerOpenGesture} touchAction="pan-y">
|
||||||
|
{mainContent}
|
||||||
|
</GestureDetector>
|
||||||
|
) : (
|
||||||
|
mainContent
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isMobile && explorerServerId && draftExplorerCheckout ? (
|
||||||
|
<ExplorerSidebar
|
||||||
|
serverId={explorerServerId}
|
||||||
|
agentId={DRAFT_AGENT_ID}
|
||||||
|
cwd={draftExplorerCheckout.cwd}
|
||||||
|
isGit={explorerIsGit}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
</FileDropZone>
|
</FileDropZone>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1293,13 +1473,25 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
},
|
},
|
||||||
|
outerContainer: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
},
|
||||||
agentPanel: {
|
agentPanel: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
menuToggleContainer: {
|
menuToggleContainer: {
|
||||||
paddingHorizontal: theme.spacing[2],
|
paddingHorizontal: theme.spacing[2],
|
||||||
paddingTop: theme.spacing[2],
|
paddingTop: theme.spacing[2],
|
||||||
alignItems: "flex-start",
|
},
|
||||||
|
menuToggleRow: {
|
||||||
|
width: "100%",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
menuButton: {
|
||||||
|
marginLeft: theme.spacing[2],
|
||||||
},
|
},
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ interface PanelState {
|
|||||||
// Actions
|
// Actions
|
||||||
openAgentList: () => void;
|
openAgentList: () => void;
|
||||||
openFileExplorer: () => void;
|
openFileExplorer: () => void;
|
||||||
|
closeFileExplorer: () => void;
|
||||||
closeToAgent: () => void;
|
closeToAgent: () => void;
|
||||||
toggleAgentList: () => void;
|
toggleAgentList: () => void;
|
||||||
toggleFileExplorer: () => void;
|
toggleFileExplorer: () => void;
|
||||||
@@ -147,6 +148,14 @@ export const usePanelStore = create<PanelState>()(
|
|||||||
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
|
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
closeFileExplorer: () =>
|
||||||
|
set((state) => ({
|
||||||
|
mobileView: state.mobileView === "file-explorer" ? "agent" : state.mobileView,
|
||||||
|
desktop: {
|
||||||
|
...state.desktop,
|
||||||
|
fileExplorerOpen: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
closeToAgent: () =>
|
closeToAgent: () =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user