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:
Mohamed Boudra
2026-01-09 19:00:41 +07:00
parent b90c17a919
commit 2d03e86bcb
7 changed files with 263 additions and 39 deletions
+8
View File
@@ -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",
+2 -1
View File
@@ -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",
+76 -15
View File
@@ -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>
+1
View File
@@ -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",
+7 -7
View File
@@ -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,
}; };