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 { ChevronLeft, MessagesSquare, SquareTerminal } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { createNameId } from "mnemonic-id";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
@@ -28,7 +27,6 @@ export function WorkspaceSetupDialog() {
const mergeWorkspaces = useSessionStore((state) => state.mergeWorkspaces);
const setHasHydratedWorkspaces = useSessionStore((state) => state.setHasHydratedWorkspaces);
const setAgents = useSessionStore((state) => state.setAgents);
const [step, setStep] = useState<"choose" | "chat">("choose");
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [createdWorkspace, setCreatedWorkspace] = useState<ReturnType<
typeof normalizeWorkspaceDescriptor
@@ -59,7 +57,6 @@ export function WorkspaceSetupDialog() {
}
useEffect(() => {
setStep("choose");
setErrorMessage(null);
setCreatedWorkspace(null);
setPendingAction(null);
@@ -265,142 +262,59 @@ export function WorkspaceSetupDialog() {
<Text style={styles.workspacePath}>{workspacePath}</Text>
</View>
{step === "choose" ? (
<View style={styles.section}>
<Text style={styles.sectionTitle}>What do you want to open?</Text>
<View style={styles.choiceGrid}>
<ChoiceCard
title="Chat Agent"
description="Open this workspace with a prompt-first chat agent."
Icon={MessagesSquare}
disabled={pendingAction !== null}
onPress={() => {
setErrorMessage(null);
setStep("chat");
}}
/>
<ChoiceCard
title="Terminal"
description="Create the workspace, then open a standalone terminal tab."
Icon={SquareTerminal}
disabled={pendingAction !== null}
pending={pendingAction === "terminal"}
onPress={() => {
void handleCreateTerminal();
}}
/>
</View>
</View>
) : null}
<View style={styles.section}>
<Composer
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>
{step === "chat" ? (
<View style={styles.section}>
<StepHeader
title="Chat Agent"
onBack={() => {
setErrorMessage(null);
setStep("choose");
}}
/>
<Text style={styles.helper}>
Start with a prompt and optional images. The workspace is created first, then the agent launches, then navigation happens.
</Text>
<View style={styles.composerCard}>
<Composer
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}
<Pressable
accessibilityRole="button"
disabled={pendingAction !== null}
onPress={() => void handleCreateTerminal()}
style={[
styles.terminalLink,
pendingAction !== null && pendingAction !== "terminal" ? { opacity: 0.5 } : undefined,
]}
>
{pendingAction === "terminal" ? (
<ActivityIndicator size="small" color={theme.colors.foregroundMuted} />
) : null}
<Text style={styles.terminalLinkText}>{"\u2192"} Open terminal</Text>
</Pressable>
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
</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) => ({
header: {
gap: theme.spacing[1],
},
workspaceTitle: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold,
fontWeight: theme.fontWeight.medium,
color: theme.colors.foreground,
},
workspacePath: {
@@ -409,78 +323,17 @@ const styles = StyleSheet.create((theme) => ({
},
section: {
gap: theme.spacing[3],
marginHorizontal: -theme.spacing[6],
},
sectionTitle: {
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: {
terminalLink: {
flexDirection: "row",
alignItems: "center",
alignSelf: "flex-end",
gap: theme.spacing[2],
},
backButton: {
width: 28,
height: 28,
borderRadius: theme.borderRadius.md,
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.colors.surface2,
terminalLinkText: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
},
errorText: {
fontSize: theme.fontSize.sm,
+13 -3
View File
@@ -5,11 +5,21 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
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
export PASEO_HOME
PASEO_HOME="$(mktemp -d "${TMPDIR:-/tmp}/paseo-dev.XXXXXX")"
trap "rm -rf '$PASEO_HOME'" EXIT
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || true)"
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
echo "══════════════════════════════════════════════════════"