diff --git a/packages/app/e2e/draft-explorer-sidebar.spec.ts b/packages/app/e2e/draft-explorer-sidebar.spec.ts
new file mode 100644
index 000000000..040043be3
--- /dev/null
+++ b/packages/app/e2e/draft-explorer-sidebar.spec.ts
@@ -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();
+ }
+});
diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx
index feb26a8c9..60fd70fe2 100644
--- a/packages/app/src/screens/agent/draft-agent-screen.tsx
+++ b/packages/app/src/screens/agent/draft-agent-screen.tsx
@@ -13,12 +13,15 @@ import { useLocalSearchParams, useRouter } from "expo-router";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
-import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
-import { Folder, GitBranch } from "lucide-react-native";
+import { Gesture, GestureDetector } from "react-native-gesture-handler";
+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 { HeaderToggleButton } from "@/components/headers/header-toggle-button";
import { AgentInputArea } from "@/components/agent-input-area";
import { AgentStreamView } from "@/components/agent-stream-view";
import { AgentConfigRow, FormSelectTrigger } from "@/components/agent-form/agent-form-dropdowns";
+import { ExplorerSidebar } from "@/components/explorer-sidebar";
import { Combobox } from "@/components/ui/combobox";
import { FileDropZone } from "@/components/file-drop-zone";
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 { useSessionStore } from "@/stores/session-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 { WelcomeScreen } from "@/components/welcome-screen";
import type { Agent } from "@/contexts/session-context";
@@ -125,6 +132,22 @@ export function DraftAgentScreen({
isVisible = true,
onCreateFlowActiveChange,
forcedServerId,
+}: DraftAgentScreenProps = {}) {
+ return (
+
+
+
+ );
+}
+
+function DraftAgentScreenContent({
+ isVisible = true,
+ onCreateFlowActiveChange,
+ forcedServerId,
}: DraftAgentScreenProps = {}) {
const { theme } = useUnistyles();
const router = useRouter();
@@ -229,6 +252,16 @@ export function DraftAgentScreen({
: undefined;
const isMobile =
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 [baseBranch, setBaseBranch] = useState("");
@@ -690,6 +723,72 @@ export function DraftAgentScreen({
const selectedWorktreeLabel =
worktreeOptions.find((option) => option.path === selectedWorktreePath)?.label ?? "";
+ const explorerCwd = useMemo(
+ () =>
+ (
+ isAttachWorktree && selectedWorktreePath ? selectedWorktreePath : workingDir
+ ).trim(),
+ [isAttachWorktree, selectedWorktreePath, workingDir]
+ );
+ const draftExplorerCheckout = useMemo(() => {
+ 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 workingDirSearchError =
directorySuggestionsQuery.error instanceof Error
@@ -893,6 +992,26 @@ export function DraftAgentScreen({
selectedWorktreePath,
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(
async ({
@@ -1082,9 +1201,11 @@ export function DraftAgentScreen({
);
}
- return (
-
-
+ const explorerServerId = draftExplorerCheckout?.serverId ?? null;
+ const explorerIsGit = draftExplorerCheckout?.isGit ?? false;
+ const mainContent = (
+
+
-
-
-
-
- {machine.tag === "creating" && draftAgent && selectedServerId ? (
-
-
-
- ) : (
-
-
-
- setIsWorkingDirOpen(true)}
- icon={}
- showLabel={false}
- valueEllipsizeMode="middle"
- testID="working-directory-select"
- />
-
- {isDirectoryNotExists && (
-
-
- Directory does not exist on the selected host
-
-
- )}
- {isMobile ? : null}
-
- {isMobile && trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
-
- ) : null}
- {trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
-
- setIsWorktreePickerOpen(true)}
- icon={}
- showLabel={false}
- valueEllipsizeMode="middle"
- />
- {worktreeMode === "create" ? (
- setIsBranchOpen(true)}
- disabled={repoInfoStatus === "loading"}
- icon={}
- showLabel={false}
+
+
+ {!isMobile && canOpenExplorer ? (
+
+
- ) : null}
-
- ) : null}
- {baseBranchError ? {baseBranchError} : null}
- {repoInfoError ? {repoInfoError} : null}
- {gitBlockingError ? {gitBlockingError} : null}
- {attachWorktreeError ? {attachWorktreeError} : null}
- {worktreeOptionsError ? {worktreeOptionsError} : null}
-
- {
- if (id === "__create_new__") {
- setWorktreeMode("create");
- if (!worktreeSlug) {
- setWorktreeSlug(createNameId());
- }
- setSelectedWorktreePath("");
- return;
- }
- if (id === "__none__") {
- setWorktreeMode("none");
- setSelectedWorktreePath("");
- return;
- }
- handleSelectWorktreePath(id);
- setWorktreeMode("attach");
- }}
- title="Select worktree"
- searchPlaceholder="Search worktrees..."
- open={isWorktreePickerOpen}
- onOpenChange={setIsWorktreePickerOpen}
- emptyText="No worktrees found"
- anchorRef={worktreeAnchorRef}
- />
-
-
-
- {
- setIsBranchOpen(nextOpen);
- if (!nextOpen) {
- setBranchSearchQuery("");
- }
- }}
- anchorRef={branchAnchorRef}
- />
-
- {formErrorMessage ? (
-
- {formErrorMessage}
+
+ ) : null}
- ) : null}
-
- )}
-
-
- dispatch({ type: "DRAFT_SET_PROMPT", text: next })}
- autoFocus={machine.tag === "draft"}
- onAddImages={handleAddImagesCallback}
- commandDraftConfig={draftCommandConfig}
- />
+
+
+
+ {machine.tag === "creating" && draftAgent && selectedServerId ? (
+
+
+
+ ) : (
+
+
+
+ setIsWorkingDirOpen(true)}
+ icon={}
+ showLabel={false}
+ valueEllipsizeMode="middle"
+ testID="working-directory-select"
+ />
+
+ {isDirectoryNotExists && (
+
+
+ Directory does not exist on the selected host
+
+
+ )}
+ {isMobile ? : null}
+
+ {isMobile && trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
+
+ ) : null}
+ {trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
+
+ setIsWorktreePickerOpen(true)}
+ icon={}
+ showLabel={false}
+ valueEllipsizeMode="middle"
+ />
+ {worktreeMode === "create" ? (
+ setIsBranchOpen(true)}
+ disabled={repoInfoStatus === "loading"}
+ icon={}
+ showLabel={false}
+ />
+ ) : null}
+
+ ) : null}
+ {baseBranchError ? {baseBranchError} : null}
+ {repoInfoError ? {repoInfoError} : null}
+ {gitBlockingError ? {gitBlockingError} : null}
+ {attachWorktreeError ? {attachWorktreeError} : null}
+ {worktreeOptionsError ? {worktreeOptionsError} : null}
+
+ {
+ if (id === "__create_new__") {
+ setWorktreeMode("create");
+ if (!worktreeSlug) {
+ setWorktreeSlug(createNameId());
+ }
+ setSelectedWorktreePath("");
+ return;
+ }
+ if (id === "__none__") {
+ setWorktreeMode("none");
+ setSelectedWorktreePath("");
+ return;
+ }
+ handleSelectWorktreePath(id);
+ setWorktreeMode("attach");
+ }}
+ title="Select worktree"
+ searchPlaceholder="Search worktrees..."
+ open={isWorktreePickerOpen}
+ onOpenChange={setIsWorktreePickerOpen}
+ emptyText="No worktrees found"
+ anchorRef={worktreeAnchorRef}
+ />
+
+
+
+ {
+ setIsBranchOpen(nextOpen);
+ if (!nextOpen) {
+ setBranchSearchQuery("");
+ }
+ }}
+ anchorRef={branchAnchorRef}
+ />
+
+ {formErrorMessage ? (
+
+ {formErrorMessage}
+
+ ) : null}
+
+ )}
+
+
+ dispatch({ type: "DRAFT_SET_PROMPT", text: next })}
+ autoFocus={machine.tag === "draft"}
+ onAddImages={handleAddImagesCallback}
+ commandDraftConfig={draftCommandConfig}
+ />
+
+
+ {!isMobile && isExplorerOpen && explorerServerId && draftExplorerCheckout ? (
+
+ ) : null}
-
+ );
+
+ return (
+
+ <>
+ {isMobile ? (
+
+ {mainContent}
+
+ ) : (
+ mainContent
+ )}
+
+ {isMobile && explorerServerId && draftExplorerCheckout ? (
+
+ ) : null}
+ >
+
);
}
@@ -1293,13 +1473,25 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
backgroundColor: theme.colors.surface0,
},
+ outerContainer: {
+ flex: 1,
+ flexDirection: "row",
+ },
agentPanel: {
flex: 1,
},
menuToggleContainer: {
paddingHorizontal: 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: {
flex: 1,
diff --git a/packages/app/src/stores/panel-store.ts b/packages/app/src/stores/panel-store.ts
index f94d59a8e..d258df8d5 100644
--- a/packages/app/src/stores/panel-store.ts
+++ b/packages/app/src/stores/panel-store.ts
@@ -69,6 +69,7 @@ interface PanelState {
// Actions
openAgentList: () => void;
openFileExplorer: () => void;
+ closeFileExplorer: () => void;
closeToAgent: () => void;
toggleAgentList: () => void;
toggleFileExplorer: () => void;
@@ -147,6 +148,14 @@ export const usePanelStore = create()(
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
};
}),
+ closeFileExplorer: () =>
+ set((state) => ({
+ mobileView: state.mobileView === "file-explorer" ? "agent" : state.mobileView,
+ desktop: {
+ ...state.desktop,
+ fileExplorerOpen: false,
+ },
+ })),
closeToAgent: () =>
set((state) => ({