refactor: simplify workspace setup dialog to single-step chat-first flow and stabilize worktree PASEO_HOME

This commit is contained in:
Mohamed Boudra
2026-04-02 14:36:42 +07:00
parent b730d83943
commit 34c27fe77e
2 changed files with 60 additions and 197 deletions
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo, useState, type ComponentType } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { ActivityIndicator, Pressable, Text, View } from "react-native"; import { ActivityIndicator, Pressable, Text, View } from "react-native";
import { ChevronLeft, MessagesSquare, SquareTerminal } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { createNameId } from "mnemonic-id"; import { createNameId } from "mnemonic-id";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
@@ -28,7 +27,6 @@ export function WorkspaceSetupDialog() {
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces); const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
const setHasHydratedWorkspaces = useSessionStore((state) => state.setHasHydratedWorkspaces); const setHasHydratedWorkspaces = useSessionStore((state) => state.setHasHydratedWorkspaces);
const setAgents = useSessionStore((state) => state.setAgents); const setAgents = useSessionStore((state) => state.setAgents);
const [step, setStep] = useState<"choose" | "chat">("choose");
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [createdWorkspace, setCreatedWorkspace] = useState<ReturnType< const [createdWorkspace, setCreatedWorkspace] = useState<ReturnType<
typeof normalizeWorkspaceDescriptor typeof normalizeWorkspaceDescriptor
@@ -59,7 +57,6 @@ export function WorkspaceSetupDialog() {
} }
useEffect(() => { useEffect(() => {
setStep("choose");
setErrorMessage(null); setErrorMessage(null);
setCreatedWorkspace(null); setCreatedWorkspace(null);
setPendingAction(null); setPendingAction(null);
@@ -265,142 +262,59 @@ export function WorkspaceSetupDialog() {
<Text style={styles.workspacePath}>{workspacePath}</Text> <Text style={styles.workspacePath}>{workspacePath}</Text>
</View> </View>
{step === "choose" ? ( <View style={styles.section}>
<View style={styles.section}> <Composer
<Text style={styles.sectionTitle}>What do you want to open?</Text> agentId={`workspace-setup:${serverId}:${sourceDirectory}`}
<View style={styles.choiceGrid}> serverId={serverId}
<ChoiceCard isInputActive={true}
title="Chat Agent" onSubmitMessage={handleCreateChatAgent}
description="Open this workspace with a prompt-first chat agent." isSubmitLoading={pendingAction === "chat"}
Icon={MessagesSquare} blurOnSubmit={true}
disabled={pendingAction !== null} value={chatDraft.text}
onPress={() => { onChangeText={chatDraft.setText}
setErrorMessage(null); images={chatDraft.images}
setStep("chat"); onChangeImages={chatDraft.setImages}
}} clearDraft={chatDraft.clear}
/> autoFocus
<ChoiceCard commandDraftConfig={composerState?.commandDraftConfig}
title="Terminal" statusControls={
description="Create the workspace, then open a standalone terminal tab." composerState
Icon={SquareTerminal} ? {
disabled={pendingAction !== null} ...composerState.statusControls,
pending={pendingAction === "terminal"} disabled: pendingAction !== null,
onPress={() => { }
void handleCreateTerminal(); : undefined
}} }
/> />
</View> </View>
</View>
) : null}
{step === "chat" ? ( <Pressable
<View style={styles.section}> accessibilityRole="button"
<StepHeader disabled={pendingAction !== null}
title="Chat Agent" onPress={() => void handleCreateTerminal()}
onBack={() => { style={[
setErrorMessage(null); styles.terminalLink,
setStep("choose"); pendingAction !== null && pendingAction !== "terminal" ? { opacity: 0.5 } : undefined,
}} ]}
/> >
<Text style={styles.helper}> {pendingAction === "terminal" ? (
Start with a prompt and optional images. The workspace is created first, then the agent launches, then navigation happens. <ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
</Text> ) : null}
<View style={styles.composerCard}> <Text style={styles.terminalLinkText}>{"\u2192"} Open terminal</Text>
<Composer </Pressable>
agentId={`workspace-setup:${serverId}:${sourceDirectory}`}
serverId={serverId}
isInputActive={true}
onSubmitMessage={handleCreateChatAgent}
isSubmitLoading={pendingAction === "chat"}
blurOnSubmit={true}
value={chatDraft.text}
onChangeText={chatDraft.setText}
images={chatDraft.images}
onChangeImages={chatDraft.setImages}
clearDraft={chatDraft.clear}
autoFocus
commandDraftConfig={composerState?.commandDraftConfig}
statusControls={
composerState
? {
...composerState.statusControls,
disabled: pendingAction !== null,
}
: undefined
}
/>
</View>
</View>
) : null}
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null} {errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
</AdaptiveModalSheet> </AdaptiveModalSheet>
); );
} }
function StepHeader({ title, onBack }: { title: string; onBack: () => void }) {
const { theme } = useUnistyles();
return (
<View style={styles.stepHeader}>
<Pressable accessibilityRole="button" onPress={onBack} style={styles.backButton}>
<ChevronLeft size={16} color={theme.colors.foregroundMuted} />
</Pressable>
<Text style={styles.sectionTitle}>{title}</Text>
</View>
);
}
function ChoiceCard({
title,
description,
Icon,
disabled,
pending = false,
onPress,
}: {
title: string;
description: string;
Icon: ComponentType<{ size: number; color: string }>;
disabled: boolean;
pending?: boolean;
onPress: () => void;
}) {
const { theme } = useUnistyles();
return (
<Pressable
accessibilityRole="button"
disabled={disabled}
onPress={onPress}
style={({ hovered, pressed }) => [
styles.choiceCard,
(hovered || pressed) && !disabled ? styles.choiceCardHovered : null,
disabled ? styles.cardDisabled : null,
]}
>
<View style={styles.choiceIconWrap}>
{pending ? (
<ActivityIndicator size="small" color={theme.colors.foreground} />
) : (
<Icon size={16} color={theme.colors.foreground} />
)}
</View>
<View style={styles.choiceBody}>
<Text style={styles.choiceTitle}>{title}</Text>
<Text numberOfLines={1} style={styles.choiceDescription}>{description}</Text>
</View>
</Pressable>
);
}
const styles = StyleSheet.create((theme) => ({ const styles = StyleSheet.create((theme) => ({
header: { header: {
gap: theme.spacing[1], gap: theme.spacing[1],
}, },
workspaceTitle: { workspaceTitle: {
fontSize: theme.fontSize.base, fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold, fontWeight: theme.fontWeight.medium,
color: theme.colors.foreground, color: theme.colors.foreground,
}, },
workspacePath: { workspacePath: {
@@ -409,78 +323,17 @@ const styles = StyleSheet.create((theme) => ({
}, },
section: { section: {
gap: theme.spacing[3], gap: theme.spacing[3],
marginHorizontal: -theme.spacing[6],
}, },
sectionTitle: { terminalLink: {
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium,
color: theme.colors.foregroundMuted,
},
helper: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
lineHeight: 20,
},
choiceGrid: {
gap: theme.spacing[2],
},
choiceCard: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[3],
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.surface1,
paddingVertical: theme.spacing[3],
paddingHorizontal: theme.spacing[3],
},
choiceCardHovered: {
backgroundColor: theme.colors.surface2,
},
cardDisabled: {
opacity: theme.opacity[50],
},
choiceIconWrap: {
width: 32,
height: 32,
borderRadius: theme.borderRadius.md,
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.colors.surface2,
},
choiceBody: {
flex: 1,
gap: 2,
},
choiceTitle: {
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium,
color: theme.colors.foreground,
},
choiceDescription: {
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
},
composerCard: {
minHeight: 180,
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.surface0,
overflow: "hidden",
},
stepHeader: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
alignSelf: "flex-end",
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
backButton: { terminalLinkText: {
width: 28, fontSize: theme.fontSize.sm,
height: 28, color: theme.colors.foregroundMuted,
borderRadius: theme.borderRadius.md,
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.colors.surface2,
}, },
errorText: { errorText: {
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
+13 -3
View File
@@ -5,11 +5,21 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PATH="$SCRIPT_DIR/../node_modules/.bin:$PATH" export PATH="$SCRIPT_DIR/../node_modules/.bin:$PATH"
# Use a temporary PASEO_HOME to avoid conflicts between dev instances # Derive PASEO_HOME: stable name for worktrees, temporary dir otherwise
if [ -z "${PASEO_HOME}" ]; then if [ -z "${PASEO_HOME}" ]; then
export PASEO_HOME export PASEO_HOME
PASEO_HOME="$(mktemp -d "${TMPDIR:-/tmp}/paseo-dev.XXXXXX")" GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || true)"
trap "rm -rf '$PASEO_HOME'" EXIT GIT_COMMON_DIR="$(git rev-parse --git-common-dir 2>/dev/null || true)"
if [ -n "$GIT_DIR" ] && [ -n "$GIT_COMMON_DIR" ] && [ "$GIT_DIR" != "$GIT_COMMON_DIR" ]; then
# Inside a worktree — derive a stable home from the worktree name
WORKTREE_ROOT="$(git rev-parse --show-toplevel)"
WORKTREE_NAME="$(basename "$WORKTREE_ROOT" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g; s/--*/-/g; s/^-//; s/-$//')"
PASEO_HOME="$HOME/.paseo-${WORKTREE_NAME}"
mkdir -p "$PASEO_HOME"
else
PASEO_HOME="$(mktemp -d "${TMPDIR:-/tmp}/paseo-dev.XXXXXX")"
trap "rm -rf '$PASEO_HOME'" EXIT
fi
fi fi
echo "══════════════════════════════════════════════════════" echo "══════════════════════════════════════════════════════"