diff --git a/package-lock.json b/package-lock.json
index 9db70c76b..9918148e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15952,6 +15952,12 @@
"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": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
@@ -21275,6 +21281,7 @@
"expo-updates": "~29.0.12",
"expo-web-browser": "~15.0.8",
"lucide-react-native": "^0.546.0",
+ "mnemonic-id": "^3.2.7",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "^0.81.5",
@@ -21335,6 +21342,7 @@
"dotenv": "^17.2.3",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
+ "mnemonic-id": "^3.2.7",
"openai": "^4.20.0",
"playwright": "^1.56.1",
"tiny-invariant": "^1.3.3",
diff --git a/packages/app/package.json b/packages/app/package.json
index 9198ecf49..548a597f5 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -15,7 +15,6 @@
"test": "vitest run"
},
"dependencies": {
- "@paseo/server": "*",
"@boudra/expo-two-way-audio": "^0.1.3",
"@expo/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.2.6",
@@ -27,6 +26,7 @@
"@lezer/json": "^1.0.3",
"@lezer/markdown": "^1.6.2",
"@lezer/python": "^1.1.18",
+ "@paseo/server": "*",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
@@ -55,6 +55,7 @@
"expo-updates": "~29.0.12",
"expo-web-browser": "~15.0.8",
"lucide-react-native": "^0.546.0",
+ "mnemonic-id": "^3.2.7",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "^0.81.5",
diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx
index eb71b1cde..ca9c665f4 100644
--- a/packages/app/src/app/index.tsx
+++ b/packages/app/src/app/index.tsx
@@ -1,10 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { createNameId } from "mnemonic-id";
import type { ImageAttachment } from "@/components/message-input";
import {
View,
Text,
Pressable,
ScrollView,
+ ActivityIndicator,
} from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -164,6 +166,8 @@ export default function HomeScreen() {
const [isLoading, setIsLoading] = useState(false);
const [promptText, setPromptText] = useState("");
const [useWorktree, setUseWorktree] = useState(false);
+ const [baseBranch, setBaseBranch] = useState("");
+ const [worktreeSlug, setWorktreeSlug] = useState("");
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
@@ -305,12 +309,12 @@ export default function HomeScreen() {
? "No git repository detected. Git options are disabled for this directory."
: null;
- 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 } => {
@@ -344,11 +348,10 @@ export default function HomeScreen() {
if (!useWorktree || isNonGitDirectory) {
return null;
}
- const slug = slugifyWorktreeName(promptText);
- 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"
@@ -358,11 +361,29 @@ export default function HomeScreen() {
}, [
useWorktree,
isNonGitDirectory,
- promptText,
- 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);
+ }, []);
+
useEffect(() => {
if (!shouldInspectRepo) {
cancelRepoInfo();
@@ -447,6 +468,10 @@ export default function HomeScreen() {
setErrorMessage(gitBlockingError);
throw new Error(gitBlockingError);
}
+ if (baseBranchError) {
+ setErrorMessage(baseBranchError);
+ throw new Error(baseBranchError);
+ }
if (isLoading) {
throw new Error("Already loading");
}
@@ -464,11 +489,12 @@ export default function HomeScreen() {
...(modeId ? { modeId } : {}),
...(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;
@@ -487,8 +513,11 @@ export default function HomeScreen() {
},
[
useWorktree,
- slugifyWorktreeName,
+ baseBranch,
+ worktreeSlug,
+ repoInfo?.currentBranch,
gitBlockingError,
+ baseBranchError,
isDirectoryNotExists,
isLoading,
isNonGitDirectory,
@@ -608,12 +637,16 @@ export default function HomeScreen() {
{trimmedWorkingDir.length > 0 && !isNonGitDirectory ? (
) : null}
@@ -780,6 +813,15 @@ export default function HomeScreen() {
/>
+
+ {isLoading ? (
+
+
+
+ Creating agent...
+
+
+ ) : null}
);
@@ -925,4 +967,23 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
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,
+ },
}));
diff --git a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx
index 6209fdc72..bcd195cbd 100644
--- a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx
+++ b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx
@@ -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(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 (
@@ -781,6 +822,42 @@ export function GitOptionsSection({
+ {useWorktree ? (
+
+ Base branch:
+ {isEditingBranch ? (
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ {displayBranch}
+
+
+ )}
+
+ ) : null}
+
+ {baseBranchError ? (
+ {baseBranchError}
+ ) : null}
+
{repoError ? (
{repoError}
) : 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,
},
diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx
index 3cc2022a0..8efb946d8 100644
--- a/packages/app/src/components/create-agent-modal.tsx
+++ b/packages/app/src/components/create-agent-modal.tsx
@@ -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(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 ? (
) : null}
diff --git a/packages/server/package.json b/packages/server/package.json
index 42cfd6951..ae315beec 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -36,6 +36,7 @@
"dotenv": "^17.2.3",
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
+ "mnemonic-id": "^3.2.7",
"openai": "^4.20.0",
"playwright": "^1.56.1",
"tiny-invariant": "^1.3.3",
diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts
index 193be9844..91eeb34cb 100644
--- a/packages/server/src/utils/worktree.ts
+++ b/packages/server/src/utils/worktree.ts
@@ -2,6 +2,7 @@ import { exec } from "child_process";
import { promisify } from "util";
import { existsSync, readFileSync, rmSync } from "fs";
import { join, basename, dirname } from "path";
+import { createNameId } from "mnemonic-id";
interface PaseoConfig {
worktree?: {
@@ -180,9 +181,8 @@ export function slugify(input: string): string {
return truncated.replace(/-+$/, "");
}
-function sanitizeWorktreeSlug(input: string): string {
- const slug = slugify(input);
- return slug.length > 0 ? slug : "worktree";
+function generateWorktreeSlug(): string {
+ return createNameId();
}
@@ -206,7 +206,7 @@ export async function createWorktree({
// Determine worktree directory based on repo type
let worktreePath: string;
- const desiredSlug = sanitizeWorktreeSlug(worktreeSlug ?? branchName);
+ const desiredSlug = worktreeSlug || generateWorktreeSlug();
if (repoInfo.type === "bare") {
worktreePath = join(repoInfo.path, desiredSlug);
@@ -263,8 +263,8 @@ export async function createWorktree({
await execAsync(command, { cwd: repoInfo.path });
worktreePath = finalWorktreePath;
- // Run setup commands from paseo.json if present
- const paseoConfigPath = join(repoInfo.path, "paseo.json");
+ // Run setup commands from paseo.json if present (look in source worktree, not bare repo)
+ const paseoConfigPath = join(cwd, "paseo.json");
if (existsSync(paseoConfigPath)) {
let config: PaseoConfig;
try {
@@ -277,7 +277,7 @@ export async function createWorktree({
if (setupCommands && setupCommands.length > 0) {
const setupEnv = {
...process.env,
- PASEO_ROOT_PATH: repoInfo.path,
+ PASEO_ROOT_PATH: cwd,
PASEO_WORKTREE_PATH: worktreePath,
PASEO_BRANCH_NAME: newBranchName,
};