refactor: simplify workspace setup dialog to single-step chat-first flow and stabilize worktree PASEO_HOME
This commit is contained in:
@@ -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
@@ -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 "══════════════════════════════════════════════════════"
|
||||
|
||||
Reference in New Issue
Block a user