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;
|
||||||
<View style={styles.container}>
|
const mainContent = (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.outerContainer}>
|
||||||
<View style={styles.agentPanel}>
|
<View style={styles.agentPanel}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -1092,199 +1213,258 @@ export function DraftAgentScreen({
|
|||||||
isMobile ? { paddingTop: insets.top + theme.spacing[2] } : null,
|
isMobile ? { paddingTop: insets.top + theme.spacing[2] } : null,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<SidebarMenuToggle />
|
<View style={styles.menuToggleRow}>
|
||||||
</View>
|
<SidebarMenuToggle />
|
||||||
|
{!isMobile && canOpenExplorer ? (
|
||||||
<Animated.View style={[styles.contentContainer, animatedKeyboardStyle]}>
|
<HeaderToggleButton
|
||||||
{machine.tag === "creating" && draftAgent && selectedServerId ? (
|
onPress={handleToggleExplorer}
|
||||||
<View style={styles.streamContainer}>
|
tooltipLabel="Toggle explorer"
|
||||||
<AgentStreamView
|
tooltipKeys={["mod", "E"]}
|
||||||
agentId={DRAFT_AGENT_ID}
|
tooltipSide="left"
|
||||||
serverId={selectedServerId}
|
style={styles.menuButton}
|
||||||
agent={draftAgent}
|
accessible
|
||||||
streamItems={optimisticStreamItems}
|
accessibilityRole="button"
|
||||||
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
|
accessibilityLabel={isExplorerOpen ? "Close explorer" : "Open explorer"}
|
||||||
/>
|
accessibilityState={{ expanded: isExplorerOpen }}
|
||||||
</View>
|
>
|
||||||
) : (
|
<PanelRight
|
||||||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}>
|
size={16}
|
||||||
<View style={styles.configSection}>
|
color={
|
||||||
<View style={isMobile ? styles.stackedSelectorGroup : styles.topSelectorRow}>
|
isExplorerOpen
|
||||||
<FormSelectTrigger
|
? theme.colors.foreground
|
||||||
controlRef={workingDirAnchorRef}
|
: theme.colors.foregroundMuted
|
||||||
containerStyle={styles.fullSelector}
|
}
|
||||||
label="Working directory"
|
|
||||||
value={displayWorkingDir}
|
|
||||||
placeholder="/path/to/project"
|
|
||||||
onPress={() => setIsWorkingDirOpen(true)}
|
|
||||||
icon={<Folder size={16} color={theme.colors.foregroundMuted} />}
|
|
||||||
showLabel={false}
|
|
||||||
valueEllipsizeMode="middle"
|
|
||||||
testID="working-directory-select"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
{isDirectoryNotExists && (
|
|
||||||
<View style={styles.warningContainer}>
|
|
||||||
<Text style={styles.warningText}>
|
|
||||||
Directory does not exist on the selected host
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{isMobile ? <View style={styles.formSeparator} /> : null}
|
|
||||||
<AgentConfigRow
|
|
||||||
providerDefinitions={providerDefinitions}
|
|
||||||
selectedProvider={selectedProvider}
|
|
||||||
onSelectProvider={setProviderFromUser}
|
|
||||||
modeOptions={modeOptions}
|
|
||||||
selectedMode={selectedMode}
|
|
||||||
onSelectMode={setModeFromUser}
|
|
||||||
models={availableModels}
|
|
||||||
selectedModel={selectedModel}
|
|
||||||
isModelLoading={isModelLoading}
|
|
||||||
onSelectModel={setModelFromUser}
|
|
||||||
thinkingOptions={availableThinkingOptions}
|
|
||||||
selectedThinkingOptionId={selectedThinkingOptionId}
|
|
||||||
onSelectThinkingOption={setThinkingOptionFromUser}
|
|
||||||
/>
|
|
||||||
{isMobile && trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
|
|
||||||
<View style={styles.formSeparator} />
|
|
||||||
) : null}
|
|
||||||
{trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
|
|
||||||
<View style={isMobile ? styles.stackedSelectorGroup : styles.topSelectorRow}>
|
|
||||||
<FormSelectTrigger
|
|
||||||
controlRef={worktreeAnchorRef}
|
|
||||||
containerStyle={
|
|
||||||
isMobile
|
|
||||||
? styles.fullSelector
|
|
||||||
: worktreeMode === "create"
|
|
||||||
? styles.halfSelector
|
|
||||||
: styles.topSelectorPrimary
|
|
||||||
}
|
|
||||||
label="Worktree"
|
|
||||||
value={worktreeTriggerValue}
|
|
||||||
placeholder="Select worktree"
|
|
||||||
onPress={() => setIsWorktreePickerOpen(true)}
|
|
||||||
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
|
|
||||||
showLabel={false}
|
|
||||||
valueEllipsizeMode="middle"
|
|
||||||
/>
|
|
||||||
{worktreeMode === "create" ? (
|
|
||||||
<FormSelectTrigger
|
|
||||||
controlRef={branchAnchorRef}
|
|
||||||
containerStyle={isMobile ? styles.fullSelector : styles.halfSelector}
|
|
||||||
label="Base branch"
|
|
||||||
value={baseBranch}
|
|
||||||
placeholder="From branch"
|
|
||||||
onPress={() => setIsBranchOpen(true)}
|
|
||||||
disabled={repoInfoStatus === "loading"}
|
|
||||||
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
|
|
||||||
showLabel={false}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
</HeaderToggleButton>
|
||||||
</View>
|
) : null}
|
||||||
) : null}
|
|
||||||
{baseBranchError ? <Text style={styles.errorInlineText}>{baseBranchError}</Text> : null}
|
|
||||||
{repoInfoError ? <Text style={styles.errorInlineText}>{repoInfoError}</Text> : null}
|
|
||||||
{gitBlockingError ? <Text style={styles.errorInlineText}>{gitBlockingError}</Text> : null}
|
|
||||||
{attachWorktreeError ? <Text style={styles.errorInlineText}>{attachWorktreeError}</Text> : null}
|
|
||||||
{worktreeOptionsError ? <Text style={styles.errorInlineText}>{worktreeOptionsError}</Text> : null}
|
|
||||||
</View>
|
|
||||||
<Combobox
|
|
||||||
options={worktreeComboOptions}
|
|
||||||
value={
|
|
||||||
worktreeMode === "create"
|
|
||||||
? "__create_new__"
|
|
||||||
: worktreeMode === "attach"
|
|
||||||
? selectedWorktreePath
|
|
||||||
: "__none__"
|
|
||||||
}
|
|
||||||
onSelect={(id) => {
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Combobox
|
|
||||||
options={workingDirComboOptions}
|
|
||||||
value={workingDir}
|
|
||||||
onSelect={setWorkingDirFromUser}
|
|
||||||
onSearchQueryChange={setWorkingDirSearchQuery}
|
|
||||||
searchPlaceholder="Search directories..."
|
|
||||||
emptyText={workingDirEmptyText}
|
|
||||||
allowCustomValue
|
|
||||||
customValuePrefix=""
|
|
||||||
customValueKind="directory"
|
|
||||||
optionsPosition="above-search"
|
|
||||||
title="Working directory"
|
|
||||||
open={isWorkingDirOpen}
|
|
||||||
onOpenChange={setIsWorkingDirOpen}
|
|
||||||
anchorRef={workingDirAnchorRef}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Combobox
|
|
||||||
options={branchComboOptions}
|
|
||||||
value={baseBranch}
|
|
||||||
onSelect={handleBaseBranchChange}
|
|
||||||
onSearchQueryChange={setBranchSearchQuery}
|
|
||||||
searchPlaceholder="Choose a base branch..."
|
|
||||||
allowCustomValue
|
|
||||||
customValuePrefix="Use"
|
|
||||||
customValueDescription="Use this branch name"
|
|
||||||
title="Select base branch"
|
|
||||||
open={isBranchOpen}
|
|
||||||
onOpenChange={(nextOpen) => {
|
|
||||||
setIsBranchOpen(nextOpen);
|
|
||||||
if (!nextOpen) {
|
|
||||||
setBranchSearchQuery("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
anchorRef={branchAnchorRef}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{formErrorMessage ? (
|
|
||||||
<View style={styles.errorContainer}>
|
|
||||||
<Text style={styles.errorText}>{formErrorMessage}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
</View>
|
||||||
</ScrollView>
|
|
||||||
)}
|
<Animated.View style={[styles.contentContainer, animatedKeyboardStyle]}>
|
||||||
</Animated.View>
|
{machine.tag === "creating" && draftAgent && selectedServerId ? (
|
||||||
<View style={styles.inputAreaWrapper}>
|
<View style={styles.streamContainer}>
|
||||||
<AgentInputArea
|
<AgentStreamView
|
||||||
agentId={DRAFT_AGENT_ID}
|
agentId={DRAFT_AGENT_ID}
|
||||||
serverId={selectedServerId ?? ""}
|
serverId={selectedServerId}
|
||||||
onSubmitMessage={handleCreateFromInput}
|
agent={draftAgent}
|
||||||
isSubmitLoading={isSubmitting}
|
streamItems={optimisticStreamItems}
|
||||||
blurOnSubmit={true}
|
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
|
||||||
value={promptValue}
|
/>
|
||||||
onChangeText={(next) => dispatch({ type: "DRAFT_SET_PROMPT", text: next })}
|
</View>
|
||||||
autoFocus={machine.tag === "draft"}
|
) : (
|
||||||
onAddImages={handleAddImagesCallback}
|
<ScrollView
|
||||||
commandDraftConfig={draftCommandConfig}
|
style={styles.scrollView}
|
||||||
/>
|
contentContainerStyle={styles.configScrollContent}
|
||||||
|
>
|
||||||
|
<View style={styles.configSection}>
|
||||||
|
<View style={isMobile ? styles.stackedSelectorGroup : styles.topSelectorRow}>
|
||||||
|
<FormSelectTrigger
|
||||||
|
controlRef={workingDirAnchorRef}
|
||||||
|
containerStyle={styles.fullSelector}
|
||||||
|
label="Working directory"
|
||||||
|
value={displayWorkingDir}
|
||||||
|
placeholder="/path/to/project"
|
||||||
|
onPress={() => setIsWorkingDirOpen(true)}
|
||||||
|
icon={<Folder size={16} color={theme.colors.foregroundMuted} />}
|
||||||
|
showLabel={false}
|
||||||
|
valueEllipsizeMode="middle"
|
||||||
|
testID="working-directory-select"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{isDirectoryNotExists && (
|
||||||
|
<View style={styles.warningContainer}>
|
||||||
|
<Text style={styles.warningText}>
|
||||||
|
Directory does not exist on the selected host
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{isMobile ? <View style={styles.formSeparator} /> : null}
|
||||||
|
<AgentConfigRow
|
||||||
|
providerDefinitions={providerDefinitions}
|
||||||
|
selectedProvider={selectedProvider}
|
||||||
|
onSelectProvider={setProviderFromUser}
|
||||||
|
modeOptions={modeOptions}
|
||||||
|
selectedMode={selectedMode}
|
||||||
|
onSelectMode={setModeFromUser}
|
||||||
|
models={availableModels}
|
||||||
|
selectedModel={selectedModel}
|
||||||
|
isModelLoading={isModelLoading}
|
||||||
|
onSelectModel={setModelFromUser}
|
||||||
|
thinkingOptions={availableThinkingOptions}
|
||||||
|
selectedThinkingOptionId={selectedThinkingOptionId}
|
||||||
|
onSelectThinkingOption={setThinkingOptionFromUser}
|
||||||
|
/>
|
||||||
|
{isMobile && trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
|
||||||
|
<View style={styles.formSeparator} />
|
||||||
|
) : null}
|
||||||
|
{trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
|
||||||
|
<View style={isMobile ? styles.stackedSelectorGroup : styles.topSelectorRow}>
|
||||||
|
<FormSelectTrigger
|
||||||
|
controlRef={worktreeAnchorRef}
|
||||||
|
containerStyle={
|
||||||
|
isMobile
|
||||||
|
? styles.fullSelector
|
||||||
|
: worktreeMode === "create"
|
||||||
|
? styles.halfSelector
|
||||||
|
: styles.topSelectorPrimary
|
||||||
|
}
|
||||||
|
label="Worktree"
|
||||||
|
value={worktreeTriggerValue}
|
||||||
|
placeholder="Select worktree"
|
||||||
|
onPress={() => setIsWorktreePickerOpen(true)}
|
||||||
|
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
|
||||||
|
showLabel={false}
|
||||||
|
valueEllipsizeMode="middle"
|
||||||
|
/>
|
||||||
|
{worktreeMode === "create" ? (
|
||||||
|
<FormSelectTrigger
|
||||||
|
controlRef={branchAnchorRef}
|
||||||
|
containerStyle={isMobile ? styles.fullSelector : styles.halfSelector}
|
||||||
|
label="Base branch"
|
||||||
|
value={baseBranch}
|
||||||
|
placeholder="From branch"
|
||||||
|
onPress={() => setIsBranchOpen(true)}
|
||||||
|
disabled={repoInfoStatus === "loading"}
|
||||||
|
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
{baseBranchError ? <Text style={styles.errorInlineText}>{baseBranchError}</Text> : null}
|
||||||
|
{repoInfoError ? <Text style={styles.errorInlineText}>{repoInfoError}</Text> : null}
|
||||||
|
{gitBlockingError ? <Text style={styles.errorInlineText}>{gitBlockingError}</Text> : null}
|
||||||
|
{attachWorktreeError ? <Text style={styles.errorInlineText}>{attachWorktreeError}</Text> : null}
|
||||||
|
{worktreeOptionsError ? <Text style={styles.errorInlineText}>{worktreeOptionsError}</Text> : null}
|
||||||
|
</View>
|
||||||
|
<Combobox
|
||||||
|
options={worktreeComboOptions}
|
||||||
|
value={
|
||||||
|
worktreeMode === "create"
|
||||||
|
? "__create_new__"
|
||||||
|
: worktreeMode === "attach"
|
||||||
|
? selectedWorktreePath
|
||||||
|
: "__none__"
|
||||||
|
}
|
||||||
|
onSelect={(id) => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Combobox
|
||||||
|
options={workingDirComboOptions}
|
||||||
|
value={workingDir}
|
||||||
|
onSelect={setWorkingDirFromUser}
|
||||||
|
onSearchQueryChange={setWorkingDirSearchQuery}
|
||||||
|
searchPlaceholder="Search directories..."
|
||||||
|
emptyText={workingDirEmptyText}
|
||||||
|
allowCustomValue
|
||||||
|
customValuePrefix=""
|
||||||
|
customValueKind="directory"
|
||||||
|
optionsPosition="above-search"
|
||||||
|
title="Working directory"
|
||||||
|
open={isWorkingDirOpen}
|
||||||
|
onOpenChange={setIsWorkingDirOpen}
|
||||||
|
anchorRef={workingDirAnchorRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Combobox
|
||||||
|
options={branchComboOptions}
|
||||||
|
value={baseBranch}
|
||||||
|
onSelect={handleBaseBranchChange}
|
||||||
|
onSearchQueryChange={setBranchSearchQuery}
|
||||||
|
searchPlaceholder="Choose a base branch..."
|
||||||
|
allowCustomValue
|
||||||
|
customValuePrefix="Use"
|
||||||
|
customValueDescription="Use this branch name"
|
||||||
|
title="Select base branch"
|
||||||
|
open={isBranchOpen}
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
setIsBranchOpen(nextOpen);
|
||||||
|
if (!nextOpen) {
|
||||||
|
setBranchSearchQuery("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
anchorRef={branchAnchorRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formErrorMessage ? (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Text style={styles.errorText}>{formErrorMessage}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
|
<View style={styles.inputAreaWrapper}>
|
||||||
|
<AgentInputArea
|
||||||
|
agentId={DRAFT_AGENT_ID}
|
||||||
|
serverId={selectedServerId ?? ""}
|
||||||
|
onSubmitMessage={handleCreateFromInput}
|
||||||
|
isSubmitLoading={isSubmitting}
|
||||||
|
blurOnSubmit={true}
|
||||||
|
value={promptValue}
|
||||||
|
onChangeText={(next) => dispatch({ type: "DRAFT_SET_PROMPT", text: next })}
|
||||||
|
autoFocus={machine.tag === "draft"}
|
||||||
|
onAddImages={handleAddImagesCallback}
|
||||||
|
commandDraftConfig={draftCommandConfig}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{!isMobile && isExplorerOpen && explorerServerId && draftExplorerCheckout ? (
|
||||||
|
<ExplorerSidebar
|
||||||
|
serverId={explorerServerId}
|
||||||
|
agentId={DRAFT_AGENT_ID}
|
||||||
|
cwd={draftExplorerCheckout.cwd}
|
||||||
|
isGit={explorerIsGit}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</FileDropZone>
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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