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:
@@ -11,7 +11,7 @@ import {
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
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 type {
|
||||
AgentMode,
|
||||
@@ -741,9 +741,13 @@ export interface GitOptionsSectionProps {
|
||||
onUseWorktreeChange: (value: boolean) => void;
|
||||
worktreeSlug: string;
|
||||
currentBranch: string | null;
|
||||
baseBranch: string;
|
||||
onBaseBranchChange: (value: string) => void;
|
||||
branches: Array<{ name: string; isCurrent: boolean }>;
|
||||
status: "idle" | "loading" | "ready" | "error";
|
||||
repoError: string | null;
|
||||
gitValidationError: string | null;
|
||||
baseBranchError: string | null;
|
||||
}
|
||||
|
||||
export function GitOptionsSection({
|
||||
@@ -751,11 +755,48 @@ export function GitOptionsSection({
|
||||
onUseWorktreeChange,
|
||||
worktreeSlug,
|
||||
currentBranch,
|
||||
baseBranch,
|
||||
onBaseBranchChange,
|
||||
branches,
|
||||
status,
|
||||
repoError,
|
||||
gitValidationError,
|
||||
baseBranchError,
|
||||
}: GitOptionsSectionProps): ReactElement {
|
||||
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 (
|
||||
<View style={styles.gitOptionsContainer}>
|
||||
@@ -781,6 +822,42 @@ export function GitOptionsSection({
|
||||
</View>
|
||||
</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 ? (
|
||||
<Text style={styles.errorText}>{repoError}</Text>
|
||||
) : null}
|
||||
@@ -1044,6 +1121,46 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.mutedForeground,
|
||||
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: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { createNameId } from "mnemonic-id";
|
||||
import type { ReactElement, ReactNode } from "react";
|
||||
import {
|
||||
View,
|
||||
@@ -338,6 +339,8 @@ function AgentFlowModal({
|
||||
const [isMounted, setIsMounted] = useState(isVisible);
|
||||
const [initialPrompt, setInitialPrompt] = useState("");
|
||||
const [useWorktree, setUseWorktree] = useState(false);
|
||||
const [baseBranch, setBaseBranch] = useState("");
|
||||
const [worktreeSlug, setWorktreeSlug] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [openDropdown, setOpenDropdown] = useState<DropdownKey | null>(null);
|
||||
@@ -386,6 +389,8 @@ function AgentFlowModal({
|
||||
const resetFormState = useCallback(() => {
|
||||
setInitialPrompt("");
|
||||
setUseWorktree(false);
|
||||
setBaseBranch("");
|
||||
setWorktreeSlug("");
|
||||
setErrorMessage("");
|
||||
setIsLoading(false);
|
||||
resetRepoInfo();
|
||||
@@ -527,12 +532,12 @@ function AgentFlowModal({
|
||||
};
|
||||
}, [backdropOpacity]);
|
||||
|
||||
const slugifyWorktreeName = useCallback((input: string): string => {
|
||||
return input
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}, []);
|
||||
const handleUseWorktreeChange = useCallback((value: boolean) => {
|
||||
setUseWorktree(value);
|
||||
if (value && !worktreeSlug) {
|
||||
setWorktreeSlug(createNameId());
|
||||
}
|
||||
}, [worktreeSlug]);
|
||||
|
||||
const validateWorktreeName = useCallback(
|
||||
(name: string): { valid: boolean; error?: string } => {
|
||||
@@ -659,11 +664,10 @@ function AgentFlowModal({
|
||||
if (!useWorktree || isNonGitDirectory) {
|
||||
return null;
|
||||
}
|
||||
const slug = slugifyWorktreeName(initialPrompt);
|
||||
if (!slug) {
|
||||
if (!worktreeSlug) {
|
||||
return null;
|
||||
}
|
||||
const validation = validateWorktreeName(slug);
|
||||
const validation = validateWorktreeName(worktreeSlug);
|
||||
if (!validation.valid) {
|
||||
return `Invalid worktree name: ${
|
||||
validation.error ?? "Must use lowercase letters, numbers, or hyphens"
|
||||
@@ -673,11 +677,29 @@ function AgentFlowModal({
|
||||
}, [
|
||||
useWorktree,
|
||||
isNonGitDirectory,
|
||||
initialPrompt,
|
||||
slugifyWorktreeName,
|
||||
worktreeSlug,
|
||||
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 trimmedPath = workingDir.trim();
|
||||
if (!trimmedPath) {
|
||||
@@ -700,6 +722,11 @@ function AgentFlowModal({
|
||||
return;
|
||||
}
|
||||
|
||||
if (baseBranchError) {
|
||||
setErrorMessage(baseBranchError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!createAgent || !isTargetDaemonReady) {
|
||||
logOfflineDaemonAction("create");
|
||||
setErrorMessage(
|
||||
@@ -733,11 +760,12 @@ function AgentFlowModal({
|
||||
...(trimmedModel ? { model: trimmedModel } : {}),
|
||||
};
|
||||
|
||||
const worktreeSlug = slugifyWorktreeName(trimmedPrompt);
|
||||
const effectiveBaseBranch = baseBranch.trim() || repoInfo?.currentBranch || undefined;
|
||||
const gitOptions = useWorktree && !isNonGitDirectory && worktreeSlug
|
||||
? {
|
||||
createWorktree: true,
|
||||
worktreeSlug,
|
||||
baseBranch: effectiveBaseBranch,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -759,7 +787,9 @@ function AgentFlowModal({
|
||||
workingDir,
|
||||
initialPrompt,
|
||||
useWorktree,
|
||||
slugifyWorktreeName,
|
||||
baseBranch,
|
||||
worktreeSlug,
|
||||
repoInfo?.currentBranch,
|
||||
selectedMode,
|
||||
modeOptions,
|
||||
logOfflineDaemonAction,
|
||||
@@ -772,6 +802,7 @@ function AgentFlowModal({
|
||||
selectedDaemonId,
|
||||
isNonGitDirectory,
|
||||
gitBlockingError,
|
||||
baseBranchError,
|
||||
selectedModel,
|
||||
]);
|
||||
|
||||
@@ -834,6 +865,7 @@ function AgentFlowModal({
|
||||
workingDirIsEmpty ||
|
||||
promptIsEmpty ||
|
||||
Boolean(gitBlockingError) ||
|
||||
Boolean(baseBranchError) ||
|
||||
isLoading ||
|
||||
!isTargetDaemonReady;
|
||||
const headerPaddingTop = useMemo(
|
||||
@@ -991,12 +1023,16 @@ function AgentFlowModal({
|
||||
{!isNonGitDirectory ? (
|
||||
<GitOptionsSection
|
||||
useWorktree={useWorktree}
|
||||
onUseWorktreeChange={setUseWorktree}
|
||||
worktreeSlug={slugifyWorktreeName(initialPrompt)}
|
||||
onUseWorktreeChange={handleUseWorktreeChange}
|
||||
worktreeSlug={worktreeSlug}
|
||||
currentBranch={repoInfo?.currentBranch ?? null}
|
||||
baseBranch={baseBranch}
|
||||
onBaseBranchChange={handleBaseBranchChange}
|
||||
branches={repoInfo?.branches ?? []}
|
||||
status={repoInfoStatus}
|
||||
repoError={repoInfoError}
|
||||
gitValidationError={gitBlockingError}
|
||||
baseBranchError={baseBranchError}
|
||||
/>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
|
||||
Reference in New Issue
Block a user