feat: use mnemonic-id for worktree names
Replace prompt-based slugifying with mnemonic-id package for generating human-readable worktree names like "hungry-hippo". Names are generated when the worktree toggle is enabled rather than derived from the prompt.
This commit is contained in:
Generated
+8
@@ -15952,6 +15952,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mnemonic-id": {
|
||||||
|
"version": "3.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/mnemonic-id/-/mnemonic-id-3.2.7.tgz",
|
||||||
|
"integrity": "sha512-kysx9gAGbvrzuFYxKkcRjnsg/NK61ovJOV4F1cHTRl9T5leg+bo6WI0pWIvOFh1Z/yDL0cjA5R3EEGPPLDv/XA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.30.1",
|
"version": "2.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
@@ -21275,6 +21281,7 @@
|
|||||||
"expo-updates": "~29.0.12",
|
"expo-updates": "~29.0.12",
|
||||||
"expo-web-browser": "~15.0.8",
|
"expo-web-browser": "~15.0.8",
|
||||||
"lucide-react-native": "^0.546.0",
|
"lucide-react-native": "^0.546.0",
|
||||||
|
"mnemonic-id": "^3.2.7",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "^0.81.5",
|
"react-native": "^0.81.5",
|
||||||
@@ -21335,6 +21342,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
|
"mnemonic-id": "^3.2.7",
|
||||||
"openai": "^4.20.0",
|
"openai": "^4.20.0",
|
||||||
"playwright": "^1.56.1",
|
"playwright": "^1.56.1",
|
||||||
"tiny-invariant": "^1.3.3",
|
"tiny-invariant": "^1.3.3",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@paseo/server": "*",
|
|
||||||
"@boudra/expo-two-way-audio": "^0.1.3",
|
"@boudra/expo-two-way-audio": "^0.1.3",
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
"@gorhom/bottom-sheet": "^5.2.6",
|
"@gorhom/bottom-sheet": "^5.2.6",
|
||||||
@@ -27,6 +26,7 @@
|
|||||||
"@lezer/json": "^1.0.3",
|
"@lezer/json": "^1.0.3",
|
||||||
"@lezer/markdown": "^1.6.2",
|
"@lezer/markdown": "^1.6.2",
|
||||||
"@lezer/python": "^1.1.18",
|
"@lezer/python": "^1.1.18",
|
||||||
|
"@paseo/server": "*",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
"@react-navigation/elements": "^2.6.3",
|
"@react-navigation/elements": "^2.6.3",
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"expo-updates": "~29.0.12",
|
"expo-updates": "~29.0.12",
|
||||||
"expo-web-browser": "~15.0.8",
|
"expo-web-browser": "~15.0.8",
|
||||||
"lucide-react-native": "^0.546.0",
|
"lucide-react-native": "^0.546.0",
|
||||||
|
"mnemonic-id": "^3.2.7",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "^0.81.5",
|
"react-native": "^0.81.5",
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { createNameId } from "mnemonic-id";
|
||||||
import type { ImageAttachment } from "@/components/message-input";
|
import type { ImageAttachment } from "@/components/message-input";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
Pressable,
|
Pressable,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
ActivityIndicator,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
@@ -164,6 +166,8 @@ export default function HomeScreen() {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [promptText, setPromptText] = useState("");
|
const [promptText, setPromptText] = useState("");
|
||||||
const [useWorktree, setUseWorktree] = useState(false);
|
const [useWorktree, setUseWorktree] = useState(false);
|
||||||
|
const [baseBranch, setBaseBranch] = useState("");
|
||||||
|
const [worktreeSlug, setWorktreeSlug] = useState("");
|
||||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
||||||
|
|
||||||
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
|
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
|
||||||
@@ -305,12 +309,12 @@ export default function HomeScreen() {
|
|||||||
? "No git repository detected. Git options are disabled for this directory."
|
? "No git repository detected. Git options are disabled for this directory."
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const slugifyWorktreeName = useCallback((input: string): string => {
|
const handleUseWorktreeChange = useCallback((value: boolean) => {
|
||||||
return input
|
setUseWorktree(value);
|
||||||
.toLowerCase()
|
if (value && !worktreeSlug) {
|
||||||
.replace(/[^a-z0-9]+/g, "-")
|
setWorktreeSlug(createNameId());
|
||||||
.replace(/^-+|-+$/g, "");
|
}
|
||||||
}, []);
|
}, [worktreeSlug]);
|
||||||
|
|
||||||
const validateWorktreeName = useCallback(
|
const validateWorktreeName = useCallback(
|
||||||
(name: string): { valid: boolean; error?: string } => {
|
(name: string): { valid: boolean; error?: string } => {
|
||||||
@@ -344,11 +348,10 @@ export default function HomeScreen() {
|
|||||||
if (!useWorktree || isNonGitDirectory) {
|
if (!useWorktree || isNonGitDirectory) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const slug = slugifyWorktreeName(promptText);
|
if (!worktreeSlug) {
|
||||||
if (!slug) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const validation = validateWorktreeName(slug);
|
const validation = validateWorktreeName(worktreeSlug);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
return `Invalid worktree name: ${
|
return `Invalid worktree name: ${
|
||||||
validation.error ?? "Must use lowercase letters, numbers, or hyphens"
|
validation.error ?? "Must use lowercase letters, numbers, or hyphens"
|
||||||
@@ -358,11 +361,29 @@ export default function HomeScreen() {
|
|||||||
}, [
|
}, [
|
||||||
useWorktree,
|
useWorktree,
|
||||||
isNonGitDirectory,
|
isNonGitDirectory,
|
||||||
promptText,
|
worktreeSlug,
|
||||||
slugifyWorktreeName,
|
|
||||||
validateWorktreeName,
|
validateWorktreeName,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const baseBranchError = useMemo(() => {
|
||||||
|
if (!useWorktree || isNonGitDirectory || !baseBranch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const branches = repoInfo?.branches ?? [];
|
||||||
|
if (branches.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const branchExists = branches.some((b) => b.name === baseBranch);
|
||||||
|
if (!branchExists) {
|
||||||
|
return `Branch "${baseBranch}" not found in repository`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [useWorktree, isNonGitDirectory, baseBranch, repoInfo?.branches]);
|
||||||
|
|
||||||
|
const handleBaseBranchChange = useCallback((value: string) => {
|
||||||
|
setBaseBranch(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldInspectRepo) {
|
if (!shouldInspectRepo) {
|
||||||
cancelRepoInfo();
|
cancelRepoInfo();
|
||||||
@@ -447,6 +468,10 @@ export default function HomeScreen() {
|
|||||||
setErrorMessage(gitBlockingError);
|
setErrorMessage(gitBlockingError);
|
||||||
throw new Error(gitBlockingError);
|
throw new Error(gitBlockingError);
|
||||||
}
|
}
|
||||||
|
if (baseBranchError) {
|
||||||
|
setErrorMessage(baseBranchError);
|
||||||
|
throw new Error(baseBranchError);
|
||||||
|
}
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
throw new Error("Already loading");
|
throw new Error("Already loading");
|
||||||
}
|
}
|
||||||
@@ -464,11 +489,12 @@ export default function HomeScreen() {
|
|||||||
...(modeId ? { modeId } : {}),
|
...(modeId ? { modeId } : {}),
|
||||||
...(trimmedModel ? { model: trimmedModel } : {}),
|
...(trimmedModel ? { model: trimmedModel } : {}),
|
||||||
};
|
};
|
||||||
const worktreeSlug = slugifyWorktreeName(trimmedPrompt);
|
const effectiveBaseBranch = baseBranch.trim() || repoInfo?.currentBranch || undefined;
|
||||||
const gitOptions = useWorktree && !isNonGitDirectory && worktreeSlug
|
const gitOptions = useWorktree && !isNonGitDirectory && worktreeSlug
|
||||||
? {
|
? {
|
||||||
createWorktree: true,
|
createWorktree: true,
|
||||||
worktreeSlug,
|
worktreeSlug,
|
||||||
|
baseBranch: effectiveBaseBranch,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -487,8 +513,11 @@ export default function HomeScreen() {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
useWorktree,
|
useWorktree,
|
||||||
slugifyWorktreeName,
|
baseBranch,
|
||||||
|
worktreeSlug,
|
||||||
|
repoInfo?.currentBranch,
|
||||||
gitBlockingError,
|
gitBlockingError,
|
||||||
|
baseBranchError,
|
||||||
isDirectoryNotExists,
|
isDirectoryNotExists,
|
||||||
isLoading,
|
isLoading,
|
||||||
isNonGitDirectory,
|
isNonGitDirectory,
|
||||||
@@ -608,12 +637,16 @@ export default function HomeScreen() {
|
|||||||
{trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
|
{trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
|
||||||
<GitOptionsSection
|
<GitOptionsSection
|
||||||
useWorktree={useWorktree}
|
useWorktree={useWorktree}
|
||||||
onUseWorktreeChange={setUseWorktree}
|
onUseWorktreeChange={handleUseWorktreeChange}
|
||||||
worktreeSlug={slugifyWorktreeName(promptText)}
|
worktreeSlug={worktreeSlug}
|
||||||
currentBranch={repoInfo?.currentBranch ?? null}
|
currentBranch={repoInfo?.currentBranch ?? null}
|
||||||
|
baseBranch={baseBranch}
|
||||||
|
onBaseBranchChange={handleBaseBranchChange}
|
||||||
|
branches={repoInfo?.branches ?? []}
|
||||||
status={repoInfoStatus}
|
status={repoInfoStatus}
|
||||||
repoError={repoInfoError}
|
repoError={repoInfoError}
|
||||||
gitValidationError={gitBlockingError}
|
gitValidationError={gitBlockingError}
|
||||||
|
baseBranchError={baseBranchError}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
@@ -780,6 +813,15 @@ export default function HomeScreen() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<View style={styles.loadingOverlay}>
|
||||||
|
<View style={styles.loadingContent}>
|
||||||
|
<ActivityIndicator size="large" color={theme.colors.foreground} />
|
||||||
|
<Text style={styles.loadingText}>Creating agent...</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</FileDropZone>
|
</FileDropZone>
|
||||||
);
|
);
|
||||||
@@ -925,4 +967,23 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
marginBottom: theme.spacing[2],
|
marginBottom: theme.spacing[2],
|
||||||
},
|
},
|
||||||
|
loadingOverlay: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
loadingContent: {
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[4],
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontSize: theme.fontSize.base,
|
||||||
|
fontWeight: theme.fontWeight.medium,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react-native";
|
import { ChevronDown, ChevronRight, Pencil, Check, X } from "lucide-react-native";
|
||||||
import { theme as defaultTheme } from "@/styles/theme";
|
import { theme as defaultTheme } from "@/styles/theme";
|
||||||
import type {
|
import type {
|
||||||
AgentMode,
|
AgentMode,
|
||||||
@@ -741,9 +741,13 @@ export interface GitOptionsSectionProps {
|
|||||||
onUseWorktreeChange: (value: boolean) => void;
|
onUseWorktreeChange: (value: boolean) => void;
|
||||||
worktreeSlug: string;
|
worktreeSlug: string;
|
||||||
currentBranch: string | null;
|
currentBranch: string | null;
|
||||||
|
baseBranch: string;
|
||||||
|
onBaseBranchChange: (value: string) => void;
|
||||||
|
branches: Array<{ name: string; isCurrent: boolean }>;
|
||||||
status: "idle" | "loading" | "ready" | "error";
|
status: "idle" | "loading" | "ready" | "error";
|
||||||
repoError: string | null;
|
repoError: string | null;
|
||||||
gitValidationError: string | null;
|
gitValidationError: string | null;
|
||||||
|
baseBranchError: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GitOptionsSection({
|
export function GitOptionsSection({
|
||||||
@@ -751,11 +755,48 @@ export function GitOptionsSection({
|
|||||||
onUseWorktreeChange,
|
onUseWorktreeChange,
|
||||||
worktreeSlug,
|
worktreeSlug,
|
||||||
currentBranch,
|
currentBranch,
|
||||||
|
baseBranch,
|
||||||
|
onBaseBranchChange,
|
||||||
|
branches,
|
||||||
status,
|
status,
|
||||||
repoError,
|
repoError,
|
||||||
gitValidationError,
|
gitValidationError,
|
||||||
|
baseBranchError,
|
||||||
}: GitOptionsSectionProps): ReactElement {
|
}: GitOptionsSectionProps): ReactElement {
|
||||||
const isLoading = status === "loading";
|
const isLoading = status === "loading";
|
||||||
|
const [isEditingBranch, setIsEditingBranch] = useState(false);
|
||||||
|
const [editedBranch, setEditedBranch] = useState(baseBranch);
|
||||||
|
const inputRef = useRef<TextInput>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEditedBranch(baseBranch);
|
||||||
|
}, [baseBranch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditingBranch) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [isEditingBranch]);
|
||||||
|
|
||||||
|
const handleStartEdit = useCallback(() => {
|
||||||
|
setEditedBranch(baseBranch);
|
||||||
|
setIsEditingBranch(true);
|
||||||
|
}, [baseBranch]);
|
||||||
|
|
||||||
|
const handleConfirmEdit = useCallback(() => {
|
||||||
|
const trimmed = editedBranch.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
onBaseBranchChange(trimmed);
|
||||||
|
}
|
||||||
|
setIsEditingBranch(false);
|
||||||
|
}, [editedBranch, onBaseBranchChange]);
|
||||||
|
|
||||||
|
const handleCancelEdit = useCallback(() => {
|
||||||
|
setEditedBranch(baseBranch);
|
||||||
|
setIsEditingBranch(false);
|
||||||
|
}, [baseBranch]);
|
||||||
|
|
||||||
|
const displayBranch = baseBranch || currentBranch || "HEAD";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.gitOptionsContainer}>
|
<View style={styles.gitOptionsContainer}>
|
||||||
@@ -781,6 +822,42 @@ export function GitOptionsSection({
|
|||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
|
{useWorktree ? (
|
||||||
|
<View style={styles.baseBranchRow}>
|
||||||
|
<Text style={styles.baseBranchLabel}>Base branch:</Text>
|
||||||
|
{isEditingBranch ? (
|
||||||
|
<View style={styles.baseBranchEditRow}>
|
||||||
|
<TextInput
|
||||||
|
ref={inputRef}
|
||||||
|
style={styles.baseBranchInput}
|
||||||
|
value={editedBranch}
|
||||||
|
onChangeText={setEditedBranch}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
placeholder="branch name"
|
||||||
|
placeholderTextColor={defaultTheme.colors.mutedForeground}
|
||||||
|
onSubmitEditing={handleConfirmEdit}
|
||||||
|
/>
|
||||||
|
<Pressable onPress={handleConfirmEdit} hitSlop={8} style={styles.baseBranchIconButton}>
|
||||||
|
<Check size={16} color={defaultTheme.colors.palette.green[500]} />
|
||||||
|
</Pressable>
|
||||||
|
<Pressable onPress={handleCancelEdit} hitSlop={8} style={styles.baseBranchIconButton}>
|
||||||
|
<X size={16} color={defaultTheme.colors.mutedForeground} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Pressable onPress={handleStartEdit} style={styles.baseBranchValueRow}>
|
||||||
|
<Text style={styles.baseBranchValue}>{displayBranch}</Text>
|
||||||
|
<Pencil size={14} color={defaultTheme.colors.mutedForeground} />
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{baseBranchError ? (
|
||||||
|
<Text style={styles.errorText}>{baseBranchError}</Text>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{repoError ? (
|
{repoError ? (
|
||||||
<Text style={styles.errorText}>{repoError}</Text>
|
<Text style={styles.errorText}>{repoError}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -1044,6 +1121,46 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.mutedForeground,
|
color: theme.colors.mutedForeground,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
|
baseBranchRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
paddingHorizontal: theme.spacing[4],
|
||||||
|
},
|
||||||
|
baseBranchLabel: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
},
|
||||||
|
baseBranchValueRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
|
baseBranchValue: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: theme.fontWeight.medium,
|
||||||
|
},
|
||||||
|
baseBranchEditRow: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
|
baseBranchInput: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
borderRadius: theme.borderRadius.md,
|
||||||
|
paddingVertical: theme.spacing[1],
|
||||||
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
},
|
||||||
|
baseBranchIconButton: {
|
||||||
|
padding: theme.spacing[1],
|
||||||
|
},
|
||||||
desktopDropdownOverlay: {
|
desktopDropdownOverlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useCallback,
|
useCallback,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { createNameId } from "mnemonic-id";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
@@ -338,6 +339,8 @@ function AgentFlowModal({
|
|||||||
const [isMounted, setIsMounted] = useState(isVisible);
|
const [isMounted, setIsMounted] = useState(isVisible);
|
||||||
const [initialPrompt, setInitialPrompt] = useState("");
|
const [initialPrompt, setInitialPrompt] = useState("");
|
||||||
const [useWorktree, setUseWorktree] = useState(false);
|
const [useWorktree, setUseWorktree] = useState(false);
|
||||||
|
const [baseBranch, setBaseBranch] = useState("");
|
||||||
|
const [worktreeSlug, setWorktreeSlug] = useState("");
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [openDropdown, setOpenDropdown] = useState<DropdownKey | null>(null);
|
const [openDropdown, setOpenDropdown] = useState<DropdownKey | null>(null);
|
||||||
@@ -386,6 +389,8 @@ function AgentFlowModal({
|
|||||||
const resetFormState = useCallback(() => {
|
const resetFormState = useCallback(() => {
|
||||||
setInitialPrompt("");
|
setInitialPrompt("");
|
||||||
setUseWorktree(false);
|
setUseWorktree(false);
|
||||||
|
setBaseBranch("");
|
||||||
|
setWorktreeSlug("");
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
resetRepoInfo();
|
resetRepoInfo();
|
||||||
@@ -527,12 +532,12 @@ function AgentFlowModal({
|
|||||||
};
|
};
|
||||||
}, [backdropOpacity]);
|
}, [backdropOpacity]);
|
||||||
|
|
||||||
const slugifyWorktreeName = useCallback((input: string): string => {
|
const handleUseWorktreeChange = useCallback((value: boolean) => {
|
||||||
return input
|
setUseWorktree(value);
|
||||||
.toLowerCase()
|
if (value && !worktreeSlug) {
|
||||||
.replace(/[^a-z0-9]+/g, "-")
|
setWorktreeSlug(createNameId());
|
||||||
.replace(/^-+|-+$/g, "");
|
}
|
||||||
}, []);
|
}, [worktreeSlug]);
|
||||||
|
|
||||||
const validateWorktreeName = useCallback(
|
const validateWorktreeName = useCallback(
|
||||||
(name: string): { valid: boolean; error?: string } => {
|
(name: string): { valid: boolean; error?: string } => {
|
||||||
@@ -659,11 +664,10 @@ function AgentFlowModal({
|
|||||||
if (!useWorktree || isNonGitDirectory) {
|
if (!useWorktree || isNonGitDirectory) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const slug = slugifyWorktreeName(initialPrompt);
|
if (!worktreeSlug) {
|
||||||
if (!slug) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const validation = validateWorktreeName(slug);
|
const validation = validateWorktreeName(worktreeSlug);
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
return `Invalid worktree name: ${
|
return `Invalid worktree name: ${
|
||||||
validation.error ?? "Must use lowercase letters, numbers, or hyphens"
|
validation.error ?? "Must use lowercase letters, numbers, or hyphens"
|
||||||
@@ -673,11 +677,29 @@ function AgentFlowModal({
|
|||||||
}, [
|
}, [
|
||||||
useWorktree,
|
useWorktree,
|
||||||
isNonGitDirectory,
|
isNonGitDirectory,
|
||||||
initialPrompt,
|
worktreeSlug,
|
||||||
slugifyWorktreeName,
|
|
||||||
validateWorktreeName,
|
validateWorktreeName,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const baseBranchError = useMemo(() => {
|
||||||
|
if (!useWorktree || isNonGitDirectory || !baseBranch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const branches = repoInfo?.branches ?? [];
|
||||||
|
if (branches.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const branchExists = branches.some((b) => b.name === baseBranch);
|
||||||
|
if (!branchExists) {
|
||||||
|
return `Branch "${baseBranch}" not found in repository`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [useWorktree, isNonGitDirectory, baseBranch, repoInfo?.branches]);
|
||||||
|
|
||||||
|
const handleBaseBranchChange = useCallback((value: string) => {
|
||||||
|
setBaseBranch(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCreate = useCallback(async () => {
|
const handleCreate = useCallback(async () => {
|
||||||
const trimmedPath = workingDir.trim();
|
const trimmedPath = workingDir.trim();
|
||||||
if (!trimmedPath) {
|
if (!trimmedPath) {
|
||||||
@@ -700,6 +722,11 @@ function AgentFlowModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (baseBranchError) {
|
||||||
|
setErrorMessage(baseBranchError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!createAgent || !isTargetDaemonReady) {
|
if (!createAgent || !isTargetDaemonReady) {
|
||||||
logOfflineDaemonAction("create");
|
logOfflineDaemonAction("create");
|
||||||
setErrorMessage(
|
setErrorMessage(
|
||||||
@@ -733,11 +760,12 @@ function AgentFlowModal({
|
|||||||
...(trimmedModel ? { model: trimmedModel } : {}),
|
...(trimmedModel ? { model: trimmedModel } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const worktreeSlug = slugifyWorktreeName(trimmedPrompt);
|
const effectiveBaseBranch = baseBranch.trim() || repoInfo?.currentBranch || undefined;
|
||||||
const gitOptions = useWorktree && !isNonGitDirectory && worktreeSlug
|
const gitOptions = useWorktree && !isNonGitDirectory && worktreeSlug
|
||||||
? {
|
? {
|
||||||
createWorktree: true,
|
createWorktree: true,
|
||||||
worktreeSlug,
|
worktreeSlug,
|
||||||
|
baseBranch: effectiveBaseBranch,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -759,7 +787,9 @@ function AgentFlowModal({
|
|||||||
workingDir,
|
workingDir,
|
||||||
initialPrompt,
|
initialPrompt,
|
||||||
useWorktree,
|
useWorktree,
|
||||||
slugifyWorktreeName,
|
baseBranch,
|
||||||
|
worktreeSlug,
|
||||||
|
repoInfo?.currentBranch,
|
||||||
selectedMode,
|
selectedMode,
|
||||||
modeOptions,
|
modeOptions,
|
||||||
logOfflineDaemonAction,
|
logOfflineDaemonAction,
|
||||||
@@ -772,6 +802,7 @@ function AgentFlowModal({
|
|||||||
selectedDaemonId,
|
selectedDaemonId,
|
||||||
isNonGitDirectory,
|
isNonGitDirectory,
|
||||||
gitBlockingError,
|
gitBlockingError,
|
||||||
|
baseBranchError,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -834,6 +865,7 @@ function AgentFlowModal({
|
|||||||
workingDirIsEmpty ||
|
workingDirIsEmpty ||
|
||||||
promptIsEmpty ||
|
promptIsEmpty ||
|
||||||
Boolean(gitBlockingError) ||
|
Boolean(gitBlockingError) ||
|
||||||
|
Boolean(baseBranchError) ||
|
||||||
isLoading ||
|
isLoading ||
|
||||||
!isTargetDaemonReady;
|
!isTargetDaemonReady;
|
||||||
const headerPaddingTop = useMemo(
|
const headerPaddingTop = useMemo(
|
||||||
@@ -991,12 +1023,16 @@ function AgentFlowModal({
|
|||||||
{!isNonGitDirectory ? (
|
{!isNonGitDirectory ? (
|
||||||
<GitOptionsSection
|
<GitOptionsSection
|
||||||
useWorktree={useWorktree}
|
useWorktree={useWorktree}
|
||||||
onUseWorktreeChange={setUseWorktree}
|
onUseWorktreeChange={handleUseWorktreeChange}
|
||||||
worktreeSlug={slugifyWorktreeName(initialPrompt)}
|
worktreeSlug={worktreeSlug}
|
||||||
currentBranch={repoInfo?.currentBranch ?? null}
|
currentBranch={repoInfo?.currentBranch ?? null}
|
||||||
|
baseBranch={baseBranch}
|
||||||
|
onBaseBranchChange={handleBaseBranchChange}
|
||||||
|
branches={repoInfo?.branches ?? []}
|
||||||
status={repoInfoStatus}
|
status={repoInfoStatus}
|
||||||
repoError={repoInfoError}
|
repoError={repoInfoError}
|
||||||
gitValidationError={gitBlockingError}
|
gitValidationError={gitBlockingError}
|
||||||
|
baseBranchError={baseBranchError}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
|
"mnemonic-id": "^3.2.7",
|
||||||
"openai": "^4.20.0",
|
"openai": "^4.20.0",
|
||||||
"playwright": "^1.56.1",
|
"playwright": "^1.56.1",
|
||||||
"tiny-invariant": "^1.3.3",
|
"tiny-invariant": "^1.3.3",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { exec } from "child_process";
|
|||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { existsSync, readFileSync, rmSync } from "fs";
|
import { existsSync, readFileSync, rmSync } from "fs";
|
||||||
import { join, basename, dirname } from "path";
|
import { join, basename, dirname } from "path";
|
||||||
|
import { createNameId } from "mnemonic-id";
|
||||||
|
|
||||||
interface PaseoConfig {
|
interface PaseoConfig {
|
||||||
worktree?: {
|
worktree?: {
|
||||||
@@ -180,9 +181,8 @@ export function slugify(input: string): string {
|
|||||||
return truncated.replace(/-+$/, "");
|
return truncated.replace(/-+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeWorktreeSlug(input: string): string {
|
function generateWorktreeSlug(): string {
|
||||||
const slug = slugify(input);
|
return createNameId();
|
||||||
return slug.length > 0 ? slug : "worktree";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ export async function createWorktree({
|
|||||||
|
|
||||||
// Determine worktree directory based on repo type
|
// Determine worktree directory based on repo type
|
||||||
let worktreePath: string;
|
let worktreePath: string;
|
||||||
const desiredSlug = sanitizeWorktreeSlug(worktreeSlug ?? branchName);
|
const desiredSlug = worktreeSlug || generateWorktreeSlug();
|
||||||
|
|
||||||
if (repoInfo.type === "bare") {
|
if (repoInfo.type === "bare") {
|
||||||
worktreePath = join(repoInfo.path, desiredSlug);
|
worktreePath = join(repoInfo.path, desiredSlug);
|
||||||
@@ -263,8 +263,8 @@ export async function createWorktree({
|
|||||||
await execAsync(command, { cwd: repoInfo.path });
|
await execAsync(command, { cwd: repoInfo.path });
|
||||||
worktreePath = finalWorktreePath;
|
worktreePath = finalWorktreePath;
|
||||||
|
|
||||||
// Run setup commands from paseo.json if present
|
// Run setup commands from paseo.json if present (look in source worktree, not bare repo)
|
||||||
const paseoConfigPath = join(repoInfo.path, "paseo.json");
|
const paseoConfigPath = join(cwd, "paseo.json");
|
||||||
if (existsSync(paseoConfigPath)) {
|
if (existsSync(paseoConfigPath)) {
|
||||||
let config: PaseoConfig;
|
let config: PaseoConfig;
|
||||||
try {
|
try {
|
||||||
@@ -277,7 +277,7 @@ export async function createWorktree({
|
|||||||
if (setupCommands && setupCommands.length > 0) {
|
if (setupCommands && setupCommands.length > 0) {
|
||||||
const setupEnv = {
|
const setupEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
PASEO_ROOT_PATH: repoInfo.path,
|
PASEO_ROOT_PATH: cwd,
|
||||||
PASEO_WORKTREE_PATH: worktreePath,
|
PASEO_WORKTREE_PATH: worktreePath,
|
||||||
PASEO_BRANCH_NAME: newBranchName,
|
PASEO_BRANCH_NAME: newBranchName,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user