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 { 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
@@ -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 "══════════════════════════════════════════════════════"
|
||||||
|
|||||||
Reference in New Issue
Block a user